Files
Northern-Thailand-Ping-Rive…/CLAUDE.md
T
grabowski 779728e38b
CI / Format & lint (push) Successful in 26s
CI / Test suite (push) Successful in 1m9s
Docs / Validate documentation (push) Successful in 25s
Rust / fmt, clippy, test, build (push) Successful in 11m46s
feat(web): the website and API in ping-collector; Matrix gets the ntfy transitions
Phase 3 of docs/RUST_REWRITE.md: rust/collector/src/web (axum) serves what
the FastAPI app (src/web_api.py) served, so one process collects, forecasts,
notifies and serves. With WEB_LISTEN=0.0.0.0:8000 it runs inside
`ping-collector run`; `ping-collector serve` runs it alone.

Same routes, parameters, JSON fields and field order, status codes,
{"detail": ...} errors and pydantic-style 422 lists, Cache-Control, gzip
(>500 B), CORS, Umami API tracking, the dashboard and static files, and the
stale-while-revalidate response caches (now with byte budgets: a history
response can be 10 MB). /docs is the same Swagger UI (+ /redoc); its OpenAPI
document is built from the route table, whose per-route parameter
declarations also validate the requests, so docs and behaviour cannot drift.
Python output conventions are kept: isoformat(), round(), numpy's pairwise
sums (catchment means), Python's summation order (skill metrics).

scripts/api_parity.py compares the two servers route by route on one
database (53 cases incl. errors, admin and static files; query values vary
per run so FastAPI's caches answer fresh). Locally: 53 identical; the
station CRUD writes byte-identical stations.json files; the skill/trend
logic matches on 9 station/horizon pairs with synthetic model versions.
Intended differences: /health counts the ThaiWater telemetry of the same
gauges while RID lags (as the notifier); /scraping/status adds the collector
version and last forecast run; an old stored forecast is served marked
X-Data-Stale instead of recomputed; /sensors/thaiwater is cached a minute;
.geojson is served as application/geo+json. Load (local, 16 clients): the
dashboard's routes 1375 vs 552 req/s, uncached history 714 vs 53 req/s;
25 MB resident when serving only.

Matrix (MATRIX_*): the notifier sends the room the transitions ntfy gets
(per station, outlook, feed; not ntfy's per-topic summaries), each once,
with its own matrix: keys in notification_state, seeded from ntfy's on first
use so nothing already announced repeats. Checked against a fake homeserver.
It replaces the `run.py --alert-check` cron job (repeated alerts every 15
min), which install_collector.sh now comments out (crontab backup kept).

install_collector.sh: --check-api (ping-collector serve on :8001 next to
FastAPI, api_parity.py), --cutover-api (refuses on a difference; sets
WEB_LISTEN, stops and disables water-monitor.service), --rollback-api;
--rollback-forecast and --rollback bring the Python app back as server.
ping-collector.service: MemoryMax 512M. Config::from_vars for tests; route
tests against a throwaway schema.
2026-10-18 16:08:06 +07:00

3.8 KiB

CLAUDE.md

Guidance for AI coding agents working in this repository.

What this is

Flood monitoring and forecasting for the Ping River, Chiang Mai. Public dashboard and API at https://water.buildfor.life/ (never publish the server's private/Tailscale IP). Production: one systemd unit on a small VPS, /opt/thailand-water-monitor, user water-monitor, interpreter .venv/bin/python (uv-managed), updated by git pull.

Rules

  • Python 3.11 only. uv sync --python 3.11; run everything as uv run ....
  • make format (black 88 / isort black profile, config in pyproject.toml) before committing; CI fails on formatting. make test must stay green — tests are synthetic-data only, never add one that needs the DB or network.
  • Timestamps everywhere are Asia/Bangkok wall-clock with no offset. The dashboard parses them with parseTs() and renders with timeZone: TZ; keep it that way.
  • Model changes go through the rolling-origin harness (scripts/evaluate_variants.py) and are judged on first-alert LEAD and false alarms, not MAE. Record results, positive or negative, in docs/FLOOD_FORECASTING.md section 5. Do not change what is deployed (rise_rain / hgb-v3) without a harness result that beats it on lead.
  • train_all() must never silently produce a gauge-only (v2) model; the guard that raises RainUnavailableError stays.
  • No git add -A: zero-byte shell-accident files (#, $(wc, ...) have been committed before. Stage files by name.
  • Do not add Co-Authored-By trailers.
  • The dashboard is a single file, src/static/dashboard.html, EN + TH via the t() table: every user-visible string needs both languages.
  • Rust (rust/, see rust/README.md): cargo fmt --all, cargo clippy --all-targets -- -D warnings, cargo test (database tests need TEST_DATABASE_URL). The Rust collector writes the same tables as the Python collectors: a change to a table, upsert or parser on one side must be made on the other until the Python side is retired (check with a live run of both into two databases).
  • The forecast is served by Rust (rust/forecast) from models trained in Python. A change to src/ml/features.py, predict.py, the HII fill or the dam frame must be ported in the same change: regenerate rust/forecast/testdata/gen_forecast_fixtures.py and keep cargo test -p ping-forecast green. Changing thresholds, stages, leads or notify constants: uv run python -m src.ml.export --config (a test fails otherwise) and rebuild the collector. Notify texts: rust/collector/testdata/gen_notify_cases.py.
  • API routes live in rust/collector/src/web (phase 3). Until the FastAPI app is removed, a route change goes into both, and scripts/api_parity.py (both servers on one database) must report no differences. A route's parameters are declared once (web/params.rs); that list validates and documents (/docs).

Where things are

  • src/web_api.py FastAPI app; src/water_scraper_v3.py RID collector; src/hii_collector.py ThaiWater/HII; src/ml/ features/train/evaluate/predict, rain.py (Open-Meteo), dam.py, hii_rain.py.
  • scripts/retrain.sh + water-monitor-retrain.timer: monthly retrain with staged promote. scripts/dev_proxy.py: serve the working-copy dashboard against the live API.
  • rust/collector (ping-collector): Rust data collector; with it deployed the Python app runs COLLECTION_ENABLED=false. With FORECAST_IN_COLLECTOR=true it also forecasts (rust/forecast) and sends ntfy + Matrix; with WEB_LISTEN it serves the website and API (rust/collector/src/web, the port of src/web_api.py; the dashboard stays src/static/dashboard.html). Rewrite plan: docs/RUST_REWRITE.md.
  • docs/FLOOD_FORECASTING.md is the authoritative model write-up; docs/DATA_SOURCES.md the source catalog.