# 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`. ## 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; the API serves its `forecast_current`. Rewrite plan: `docs/RUST_REWRITE.md`. - `docs/FLOOD_FORECASTING.md` is the authoritative model write-up; `docs/DATA_SOURCES.md` the source catalog.