- README rewritten around a copy-paste quick start (CLI and Portainer), verified alert test, day-to-day NocoDB operations, troubleshooting table, FAQ - new docker-compose.allinone.yml: NocoDB (pinned 2026.09.0, SQLite) + monitor on a private network, with healthchecks and the Telegram IPv4 pin documented - docs/: QUICKSTART-PORTAINER, TELEGRAM-SETUP, NOCODB-SETUP, ARCHITECTURE, OPERATIONS, TROUBLESHOOTING (replace DOCUMENTATION.md + COMPOSE-SETUP.md) - secrets: SECRETS.md is gitignored and untracked; tracked template is SECRETS.example.md; real base id / chat id removed from .env.example - LICENSE (MIT), .gitignore/.dockerignore tidied - AGENTS.md: layout, iron rules, verification gates; host-specific deploy details moved to the gitignored OPS-INTERNAL.md
5.3 KiB
AGENTS.md
Entry point for AI coding agents working on carousell-monitor: a Python-stdlib monitor that polls Carousell search pages, archives every listing to NocoDB, and alerts Telegram.
Read this file, then the doc that matches your task. Keep it updated when the layout or the deployment story changes.
What it does
monitor.py polls Carousell search URLs (sort_by=3 = recent), extracts listings
from the server-rendered <script type="application/json"> Redux state
(SearchListing.listingCards), dedupes by product_url (param-less), archives to
a NocoDB base, and alerts Telegram "<title>: N new listings".
Silencing: a listing whose seller_name is in IgnoredSellers is archived but
never alerted (skip_notify=true); likewise a listing whose title contains a
keyword listed for its watch in IgnoredKeywords (a real Link column → Settings,
case-insensitive substring).
Iron rules
- Secrets NEVER in code or compose — env vars (
.env, gitignored) or a stack's environment.SECRETS.example.mdis the tracked template; the real inventory is the gitignoredSECRETS.md. No default base id, token or chat id may be shipped. - Operational knobs (
enabled/notify/check_interval_minutes) live in the NocoDB Settings table, adjustable from the UI without a redeploy. Only credentials and tuning belong in the environment. - Dedupe key is
product_url(https://www.carousell.com.my/p/<id>/), never the raw listing id and never the query-string URL. - First run per watch seeds the archive with no Telegram alert
(
last_checked_atnull == unseeded). - Docker HEALTHCHECK: unhealthy when the last tick is older than
HEALTH_STALE_SECONDSor the last run failed. imageis an Attachment column storing the remote URL (NocoDB hotlinks it; the raw URL is also kept inimage_url).- Never change
docker-compose.ymlcasually: it is the production variant that joins an existing NocoDB network. The beginner/fresh-install path isdocker-compose.allinone.yml.
Layout
| File | Role |
|---|---|
monitor.py |
the whole loop (stdlib only) |
healthcheck.py |
Docker HEALTHCHECK probe over /data/health.json |
Dockerfile |
python:3.11-alpine, no dependencies |
docker-compose.allinone.yml |
NocoDB + monitor (fresh install) |
docker-compose.yml |
monitor only, existing NocoDB network (production) |
test_pagination.py |
stdlib regression suite — must pass before any commit |
docs/ |
long-form guides: QUICKSTART-PORTAINER, TELEGRAM-SETUP, NOCODB-SETUP, ARCHITECTURE, OPERATIONS, TROUBLESHOOTING |
OPS-INTERNAL.md |
gitignored: this deployment's hosts, paths, stack ids (may not exist) |
Verified facts (Carousell page shape, 2026-09)
- Search page: ~1.6 MB HTML, state blob ~1.27 MB, ~49
listingCardsper load. Card fields:listingID,title,price(e.g. "RM85"),thumbnailURL,seller.username,aboveFold[time_created].timestampContent.seconds.low,belowFoldparagraphs whereparagraph[1]= condition. https://www.carousell.com.my/p/<id>/301-redirects to the canonical slug URL.- Soft rate limiting returns HTTP 200 with
listingCards: null— treat as a retryable error, never as "no results". - NocoDB v2 API: records use column titles as JSON keys; an Attachment field
accepts a JSON string
[{"path","mimetype","title"}]and keeps the remote URL.
Working on the code
- Stdlib only. Do not add dependencies without a very good reason.
- Run
python test_pagination.py(exit 0 = pass) andpython -m pyflakes monitor.pybefore committing. pyflakes currently reports one cosmetic finding (_send_photo_multiparthas a deadparts = []local) — anything beyond that is yours. - New env knobs must be added to
monitor.py, both compose files,.env.exampleand the tables inREADME.md+docs/ARCHITECTURE.mdin the same commit. - Tests must execute the real loader functions, not mocks of them, and must be mutation-checked (reintroduce the bug, watch the check fail, restore).
monitor.pyis checked out with CRLF on Windows; prefer a Pythonread → replace → write(newline='')over fuzzy patch tools on big blocks.- Grepping pitfall: a
NameError/arity error inside a tick does not fail at import. The container keeps running and the only symptom is/data/health.jsonreportingok:falsewith atick error: …string.
Deploying
Generic:
git pull
docker compose -f docker-compose.allinone.yml up -d --build
This project's own deployment (host paths, Portainer stack id, NocoDB instance,
tunnel) is deliberately kept out of the repo: see the gitignored OPS-INTERNAL.md
or the operator's carousell-monitor skill. Two rules that were learned the hard
way:
- The build context is a plain directory on the host, not a git checkout:
the running container is built from the files copied next to the compose file.
Sync
monitor.py(anddocker-compose*.yml/Dockerfilewhen they change) and then rebuild — a stack PUT alone does not run--build. - Compare the live container's environment with the file you are about to deploy before rebuilding. Env drift between the compose file, the stack's stored env and the running container is the single most common cause of a "fixed" deploy that changes nothing.