- 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.1 KiB
Operations
Running it, watching it, backing it up, upgrading it.
Health at a glance
The container writes /data/health.json at the end of every tick, and the Docker HEALTHCHECK (healthcheck.py) reads it every 60 s:
docker exec carousell-monitor cat /data/health.json
{
"last_run_epoch": 1791275045,
"ok": true,
"error": "",
"watch_count": 3,
"new_this_tick": 0,
"failed_watches": 0
}
| Field | Meaning |
|---|---|
ok |
the last tick completed without exceeding the failure threshold |
error |
empty, or which watches failed and why ([partial 1/3] … when the ratio threshold absorbed it) |
last_run_epoch |
when the tick ended (Unix seconds, UTC) |
watch_count |
how many enabled watches were read from Settings |
new_this_tick |
listings fetched this tick that were not already known — not how many alerts were sent |
Two traps worth internalising:
ok: trueonly proves the fetch loop ran. Notification failures are retried on the next tick rather than reported, so an archive that fills up happily can still be silent in Telegram. To check the notify path, unticknotifiedon a row and watch it flip back totrue(that flip only happens after Telegram answered 200).new_this_tick: 0is not evidence of anything — it counts fetched rows, not delivered messages.
The container's own health is the other half:
docker compose -f docker-compose.allinone.yml ps # State / Health column
docker inspect --format '{{.State.Health.Status}}' carousell-monitor
unhealthy = the last tick failed or is older than HEALTH_STALE_SECONDS (600 s).
Logs
docker compose -f docker-compose.allinone.yml logs -f --tail 100 carousell-monitor
The loop is deliberately quiet: one ready: line at startup, then nothing unless something fails. Per-watch failures are logged to stderr, and hard failures appear in health.json. If you are debugging "why no alert", logs are the wrong place — use the decision tree in TROUBLESHOOTING.md.
Alerting on the monitor itself
ERROR_ALERT_AFTER (default 3) consecutive failed ticks trigger one Telegram message:
🚨 carousell-monitor 故障
连续失败 3 次
错误: Uniform: carousell fetch HTTP 403
容器将标记为 unhealthy
and one recovery message when the next good tick arrives. The debounce state lives in /data/alert_state.json, so a container restart does not re-fire an alert you already saw.
The alert strings are Chinese in the current code (monitor.py → alert_on_health()); change the two msg = (…) literals if you want English.
Backups
| What | Where | How |
|---|---|---|
| All listings, settings and filters | volume nocodb-data |
stop the stack, tar the volume; or use NocoDB's own Export base |
| Health + alert state | volume carousell-data |
disposable — do not bother |
| This repo's config | .env / the Portainer stack file |
keep a copy in your password manager |
Nothing else is stateful. NocoDB is the single source of truth.
Upgrading
The monitor (code change in this repo):
git pull
docker compose -f docker-compose.allinone.yml up -d --build
The rebuild re-creates the container. The schema bootstrap and the seen-set are idempotent, so nothing is duplicated.
NocoDB — change the image tag in the compose file and:
docker compose -f docker-compose.allinone.yml up -d
NocoDB migrates its own database on start. Back up nocodb-data first. The monitor's schema bootstrap talks to NocoDB's meta API, which does change between majors: the tag in docker-compose.allinone.yml is the version range this code is verified against, so upgrade it deliberately (and check the table columns in the UI afterwards).
Portainer users: same thing through the UI — edit the stack file, Update the stack. Be careful with Re-pull image on a stack whose env values were entered through Portainer's panel (see the README's masking warning).
Tests
python test_pagination.py # stdlib only, no network, exit 0 = pass
Covers the things that have actually broken: NocoDB paging past 1000 rows, the Telegram HTTP verb bug, the failure-ratio threshold maths, and the fetch-gap timing.
Tuning notes
| Goal | Change |
|---|---|
| More search coverage | add rows to Settings, keep check_interval_minutes ≥ 5 |
| React faster | lower check_interval_minutes (not TICK_SECONDS below ~30 s) |
| Be gentler on Carousell | raise check_interval_minutes, keep FETCH_GAP_SECONDS ≥ 1 |
| Alert inbox too noisy | set notify = false on a watch, or add entries to IgnoredKeywords / IgnoredSellers |
| Fewer failure alerts | raise ERROR_ALERT_AFTER, or let FAILURE_RATIO_THRESHOLD stay at 1.0 |
Data safety rules
.envand your NocoDB token are credentials. Never commit them, never paste them into an issue.- The NocoDB token is scoped to a workspace: regenerate it in NocoDB and update the stack if it leaks.
- The monitor never deletes or modifies archived listings, apart from the
notified/skip_notifyflags. Cleaning up the archive is your job — NocoDB's grid view deletes rows fine.