hoelee b88f6d0963 Fix NameError in load_ignored_keywords from pagination refactor
The previous commit renamed the record list to 'rows' but missed the loop in
load_ignored_keywords, leaving a bare 'j' -> 'tick error: name j is not
defined' every tick. The loop died, health.json went ok:false, the container
reported unhealthy, and alert_on_health fired against a live failure.

- fix the stale reference
- use real emoji instead of literal backslash escapes in alert text
- test: exercise the real loaders end-to-end against a fake NocoDB so a
  missed rename is caught (mutation-verified: reintroducing the bug fails 3
  checks)
2026-09-22 03:44:24 +08:00

carousell-monitor

Watches Carousell search pages (sorted by recent) for new listings, archives every listing to a NocoDB base (with image URL + thumbnail), and alerts Telegram.

How it works

  • monitor.py runs in a Docker container on DSM, self-bootstrapping its NocoDB schema (Listings + Settings + IgnoredSellers + IgnoredKeywords tables) and looping forever.
  • Every TICK_SECONDS it reads the watch list from the Settings table and polls each enabled watch's URL on its own check_interval_minutes.
  • Dedupe key = product_url (param-less listing URL). First run per watch = seed archive only (no Telegram). After that, new listings are archived and alerted as "<title>: N new listings".
  • The container marks itself unhealthy (Docker healthcheck) if a tick fails to extract / gets rate-limited / crashes.

Schema

Listings — product_url (unique), title, price (numeric), condition (SingleSelect), image_url, image (Attachment → thumbnail), seller_name, seller_url, search_title, search_url, listed_at, first_seen_at, notified, skip_notify.

Settings — title, url, enabled, notify, check_interval_minutes, last_checked_at. Add/remove watches here from the NocoDB UI; no redeploy needed.

IgnoredSellers — seller_name. Add/remove sellers here to suppress Telegram alerts for their listings (still archived, marked skip_notify=true).

IgnoredKeywords — watch (Link → Settings) + keyword. Per-watch title blocklist: pick the watch from a dropdown, add one keyword per row. A keyword only applies to listings from the linked watch; case-insensitive substring match against the title. Still archived.

Run

# local (against LAN NocoDB)
NOCODB_URL=http://192.168.137.2:10380 \
NOCODB_TOKEN=... TELEGRAM_BOT_TOKEN=... TELEGRAM_CHAT_ID=... \
python monitor.py

# docker
docker build -t hoelee/carousell-monitor:latest .
docker compose up -d

Deploy (DSM via Portainer stack 240)

Private build — no registry. The container is owned by Portainer stack 240 (standalone; compose + build context at /volume1/docker/portainer/compose/240/ on DSM), deployed with secrets passed as stack environment variables.

# on DSM — rebuild the image only when monitor.py / Dockerfile changed
sudo /usr/local/bin/docker build -t carousell-monitor:latest \
  /volume1/docker/portainer/compose/240

Then update stack 240 via the Portainer API (PUT /api/stacks/240?endpointId=2, repo compose as stackFileContent + current env array — see portainer-api skill; ⚠ never echo masked *** env values back). The old /volume1/docker/carousell-monitor dir is legacy — do not compose up there.

Files

  • monitor.py — main loop, schema bootstrap, fetch/parse, NocoDB IO, Telegram.
  • healthcheck.py — Docker HEALTHCHECK probe (/data/health.json).
  • Dockerfile, docker-compose.yml, .env.example.
  • COMPOSE-SETUP.md — stack anatomy: compose file, Dockerfile, networking, deploy paths.
  • AGENTS.md — AI-agent entry. SECRETS.md — credentials (private repo).
S
Description
Watches Carousell search pages for new listings, archives every result to NocoDB with photos, and pushes Telegram alerts. Python stdlib, self-hosted Docker/Portainer stack (NocoDB + monitor in one compose file).
Readme MIT
489 KiB
Languages
Python 99.6%
Dockerfile 0.4%