docs: rewrite the README for the Rust service
CI / Format & lint (push) Successful in 22s
CI / Test suite (push) Successful in 54s
Docs / Validate documentation (push) Successful in 8s
Rust / fmt, clippy, test, build (push) Successful in 10m41s
Security / Dependency vulnerabilities (push) Failing after 44s
Security / Static analysis (push) Successful in 23s
Security / License report (push) Successful in 1m16s
CI / Format & lint (push) Successful in 22s
CI / Test suite (push) Successful in 54s
Docs / Validate documentation (push) Successful in 8s
Rust / fmt, clippy, test, build (push) Successful in 10m41s
Security / Dependency vulnerabilities (push) Failing after 44s
Security / Static analysis (push) Successful in 23s
Security / License report (push) Successful in 1m16s
This commit is contained in:
1 parent
7e642555b4
commit
6fe2e287e5
1 file changed
+109
-75
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
Live water levels, discharge, rainfall and machine-learning flood forecasts for the
|
Live water levels, discharge, rainfall and machine-learning flood forecasts for the
|
||||||
Ping River basin around Chiang Mai. Collects hourly gauge data from public sources,
|
Ping River basin around Chiang Mai. Collects hourly gauge data from public sources,
|
||||||
keeps the full history in PostgreSQL, and serves a bilingual dashboard, an open REST
|
keeps the full history in PostgreSQL, serves a bilingual dashboard and an open REST
|
||||||
API, and 6/12/24-hour flood-risk forecasts per gauge.
|
API, forecasts the flood risk per gauge 6/12/24 hours ahead, and pushes alerts when
|
||||||
|
a gauge crosses its warning or danger level.
|
||||||
|
|
||||||
**Live: [water.buildfor.life](https://water.buildfor.life/)** · API reference at
|
**Live: [water.buildfor.life](https://water.buildfor.life/)** · API reference at
|
||||||
[/docs](https://water.buildfor.life/docs) · built by [buildfor.life](https://buildfor.life)
|
[/docs](https://water.buildfor.life/docs) · built by [buildfor.life](https://buildfor.life)
|
||||||
@@ -11,9 +12,9 @@ after the [October 2024 flood](https://buildfor.life/blog/chiang-mai-flood-2024/
|
|||||||
background in [Teaching a Model to See the Ping River Rise 13 Hours Early](https://buildfor.life/blog/ping-river-monitor/).
|
background in [Teaching a Model to See the Ping River Rise 13 Hours Early](https://buildfor.life/blog/ping-river-monitor/).
|
||||||
|
|
||||||
[](https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor/actions)
|
[](https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor/actions)
|
||||||
|
[](https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor/actions)
|
||||||
[](https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor/actions)
|
[](https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor/actions)
|
||||||
[](https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor/actions)
|
[](https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor/actions)
|
||||||
[](https://python.org)
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
@@ -21,11 +22,12 @@ background in [Teaching a Model to See the Ping River Rise 13 Hours Early](https
|
|||||||
- **Collects** hourly water level and discharge from 16 Royal Irrigation Department
|
- **Collects** hourly water level and discharge from 16 Royal Irrigation Department
|
||||||
(RID) telemetry gauges, Chiang Dao to the southern basin, since 2018-08; hourly
|
(RID) telemetry gauges, Chiang Dao to the southern basin, since 2018-08; hourly
|
||||||
rainfall and water level from 400+ ThaiWater/HII stations; Open-Meteo catchment
|
rainfall and water level from 400+ ThaiWater/HII stations; Open-Meteo catchment
|
||||||
rainfall (archive + 48 h forecast); daily Mae Ngat reservoir state. Every source and
|
rainfall (archive + 48 h forecast); daily state of the large RID dams, Mae Ngat
|
||||||
its quirks: [docs/DATA_SOURCES.md](docs/DATA_SOURCES.md).
|
above all. Every source and its quirks: [docs/DATA_SOURCES.md](docs/DATA_SOURCES.md).
|
||||||
- **Fills gaps.** The raw RID grid had readings for ~56 % of hours; a full-history
|
- **Fills gaps.** The raw RID grid had readings for ~56 % of hours; a full-history
|
||||||
re-fetch plus HII cross-fill brought it to ~93 %. `GET /api/stats` reports the
|
re-fetch plus HII cross-fill brought it to ~93 %. RID's report often arrives
|
||||||
current figure.
|
hours late in the evening, so late readings are always stored and holes are
|
||||||
|
re-fetched. `GET /api/stats` reports the current coverage.
|
||||||
- **Forecasts.** Per gauge and horizon, a gradient-boosted model predicts the rise
|
- **Forecasts.** Per gauge and horizon, a gradient-boosted model predicts the rise
|
||||||
within 6/12/24 h and the probability of crossing the station's warning and danger
|
within 6/12/24 h and the probability of crossing the station's warning and danger
|
||||||
levels. Trained on the monitor's own history plus catchment rain; evaluated
|
levels. Trained on the monitor's own history plus catchment rain; evaluated
|
||||||
@@ -39,42 +41,38 @@ background in [Teaching a Model to See the Ping River Rise 13 Hours Early](https
|
|||||||
- **Notifies.** Public push alerts over a self-hosted [ntfy](https://ntfy.sh): one
|
- **Notifies.** Public push alerts over a self-hosted [ntfy](https://ntfy.sh): one
|
||||||
message when a gauge crosses its warning or danger level, one all-clear on the
|
message when a gauge crosses its warning or danger level, one all-clear on the
|
||||||
way down, an opt-in early-warning topic from the model, nothing in between.
|
way down, an opt-in early-warning topic from the model, nothing in between.
|
||||||
Subscribe from the free app, no account. Matrix room alerts for a team are
|
Subscribe from the free app, no account. A Matrix room can get the same
|
||||||
also supported.
|
messages for a team. [docs/NOTIFICATIONS.md](docs/NOTIFICATIONS.md).
|
||||||
|
|
||||||
## Quick start
|
## How it runs
|
||||||
|
|
||||||
Python **3.11** (3.13 breaks the pinned `psycopg2-binary`), PostgreSQL for anything
|
```text
|
||||||
beyond a quick look, [uv](https://docs.astral.sh/uv/).
|
RID gauges · ThaiWater/HII · RID dams · Open-Meteo
|
||||||
|
│ hourly
|
||||||
```bash
|
▼
|
||||||
git clone https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor.git
|
┌──────────────────────────────────────┐ ┌───────────────────────────┐
|
||||||
cd Northern-Thailand-Ping-River-Monitor
|
│ ping-collector (Rust, one process) │ │ monthly retrain (Python) │
|
||||||
uv sync --python 3.11
|
│ collect → forecast → notify │◄───────│ scikit-learn, exports │
|
||||||
cp .env.example .env # DB_TYPE, POSTGRES_CONNECTION_STRING, optional MATRIX_*
|
│ website + API on :8000 │ models │ models/flood_*.json.gz │
|
||||||
uv run python run.py --web-api # dashboard + API on http://localhost:8000
|
└──────┬──────────────┬────────────────┘ └─────────────┬─────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ │
|
||||||
|
ntfy topics, PostgreSQL ◄──────────── history ────────────┘
|
||||||
|
Matrix room (gauges, rain, dams, forecasts, alert state)
|
||||||
```
|
```
|
||||||
|
|
||||||
`DB_TYPE=sqlite` works for the dashboard and API; the forecasting path expects the
|
- **The service** is one Rust binary, `ping-collector` ([rust/README.md](rust/README.md)),
|
||||||
PostgreSQL history.
|
run by systemd. It polls RID at :05 past each hour and retries every 10 minutes
|
||||||
|
while the report is behind, fetches HII, the dams and the rain once an hour,
|
||||||
```bash
|
forecasts after every cycle that brought new readings, sends the notifications,
|
||||||
uv run python run.py --status # collector status
|
and serves the dashboard and API, in under 100 MB of memory with every model
|
||||||
uv run python run.py --test # one collection cycle
|
loaded.
|
||||||
uv run python run.py --fill-gaps 7 # re-fetch the last 7 days from RID
|
- **The models** are trained in Python (`src/ml`, scikit-learn
|
||||||
uv run python run.py --collect-hii # one ThaiWater/HII collection cycle
|
HistGradientBoosting) by a monthly timer and exported for the Rust forecaster,
|
||||||
uv run python run.py --alert-check # evaluate thresholds, notify Matrix
|
which reproduces Python's features bit for bit and reloads new models without
|
||||||
uv run python scripts/train_flood_model.py --stations all # retrain (~12 min)
|
a restart.
|
||||||
make test # pytest, synthetic data, no network
|
- Until October 2026 all of this was a Python/FastAPI app; how and why it moved:
|
||||||
make format # black + isort (the CI contract)
|
[docs/RUST_REWRITE.md](docs/RUST_REWRITE.md).
|
||||||
```
|
|
||||||
|
|
||||||
RID collection in `--web-api` mode: polled at :05 past each hour
|
|
||||||
(`RID_POLL_OFFSET_MINUTES`); while RID has not published the current hour yet,
|
|
||||||
retried every `RID_RETRY_MINUTES` (10). Every poll stores all readings the report
|
|
||||||
has, and yesterday's report is re-fetched while it still has holes (RID's evening
|
|
||||||
hours often arrive 1-2 h late). On startup the leader re-fetches holes from the
|
|
||||||
last `GAP_FILL_DAYS_ON_START` (14) days; for older history run `--fill-gaps all`.
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -88,76 +86,112 @@ Asia/Bangkok wall-clock without an offset suffix.
|
|||||||
| `GET /measurements/history/{code}?hours=N` | Hourly history; or `?start=YYYY-MM-DD&end=YYYY-MM-DD`; `limit` ≤ 100000 |
|
| `GET /measurements/history/{code}?hours=N` | Hourly history; or `?start=YYYY-MM-DD&end=YYYY-MM-DD`; `limit` ≤ 100000 |
|
||||||
| `GET /forecast` | Current flood-risk forecast, every station × horizon, with thresholds and P.1 inundation-stage probabilities |
|
| `GET /forecast` | Current flood-risk forecast, every station × horizon, with thresholds and P.1 inundation-stage probabilities |
|
||||||
| `GET /api/forecast/history/{code}?hours=N&horizon=24` | Forecasts as issued, for auditing lead time after the fact |
|
| `GET /api/forecast/history/{code}?hours=N&horizon=24` | Forecasts as issued, for auditing lead time after the fact |
|
||||||
|
| `GET /api/forecast/skill?station_code=P.1` | Issued forecasts vs what happened, per deployed model version |
|
||||||
| `GET /api/hii/rainfall/latest`, `/api/hii/waterlevel/latest` | Latest ThaiWater/HII gauge readings |
|
| `GET /api/hii/rainfall/latest`, `/api/hii/waterlevel/latest` | Latest ThaiWater/HII gauge readings |
|
||||||
| `GET /api/hii/rainfall/catchment?days=N` | HII gauge catchment-mean rain next to the Open-Meteo series the model uses |
|
| `GET /api/hii/rainfall/catchment?days=N` | HII gauge catchment-mean rain next to the Open-Meteo series the model uses |
|
||||||
| `GET /api/forecast/skill?station_code=P.1` | Issued forecasts vs what happened, per deployed model version |
|
|
||||||
| `GET /api/notifications` | ntfy server and topic names for the subscribe panel |
|
| `GET /api/notifications` | ntfy server and topic names for the subscribe panel |
|
||||||
| `GET /api/stats` | Row counts per source, date range, coverage |
|
| `GET /api/stats` | Row counts per source, date range, coverage |
|
||||||
| `GET /health` | DB / upstream / memory checks |
|
| `GET /health` | Database and data freshness (RID, ThaiWater as backup), the RID API, memory |
|
||||||
|
| `GET /scraping/status` | Collection runs, the last forecast run, service version |
|
||||||
|
|
||||||
Interactive reference with schemas: [water.buildfor.life/docs](https://water.buildfor.life/docs).
|
Interactive reference with schemas: [water.buildfor.life/docs](https://water.buildfor.life/docs)
|
||||||
Responses are cached briefly server-side; poll no faster than once a minute — the data
|
(also `/redoc`, `/openapi.json`). Responses are cached briefly server-side; poll no
|
||||||
changes hourly.
|
faster than once a minute — the data changes hourly. Station editing
|
||||||
|
(`POST/PUT/DELETE /stations`), `/config` and `POST /scrape/trigger` need the
|
||||||
|
server's `ADMIN_API_KEY` in an `X-API-Key` header.
|
||||||
|
|
||||||
|
## Run it locally
|
||||||
|
|
||||||
|
Needs Rust >= 1.88 ([rustup](https://rustup.rs); on Linux also `pkg-config` and
|
||||||
|
`libssl-dev`) and PostgreSQL. Python 3.11 with [uv](https://docs.astral.sh/uv/)
|
||||||
|
only for training models.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor.git
|
||||||
|
cd Northern-Thailand-Ping-River-Monitor
|
||||||
|
docker run -d --name ping-pg -e POSTGRES_PASSWORD=ping -e POSTGRES_DB=ping -p 55432:5432 postgres:16
|
||||||
|
cp .env.example .env
|
||||||
|
# POSTGRES_CONNECTION_STRING=postgresql://postgres:ping@localhost:55432/ping
|
||||||
|
# WEB_LISTEN=127.0.0.1:8000
|
||||||
|
cargo build --release --manifest-path rust/Cargo.toml
|
||||||
|
rust/target/release/ping-collector check-db # create the tables
|
||||||
|
rust/target/release/ping-collector fill-gaps 30 # the last 30 days from RID
|
||||||
|
rust/target/release/ping-collector run # collect, forecast, serve http://127.0.0.1:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Notifications stay off until `NTFY_SERVER` or `MATRIX_*` is set. Without trained
|
||||||
|
models the forecast is the persistence heuristic, marked `source: "heuristic"`.
|
||||||
|
Training needs years of history (the server's database, or `FLOOD_ML_DB_URL`) and
|
||||||
|
the Open-Meteo archive:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync --python 3.11
|
||||||
|
uv run python scripts/train_flood_model.py --stations all # writes models/flood_*
|
||||||
|
uv run python scripts/evaluate_variants.py --stations P.1 # the rolling-origin harness
|
||||||
|
```
|
||||||
|
|
||||||
|
`make help` lists the rest (`make check` runs every CI gate locally).
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
Production is a systemd unit on a small VPS behind Cloudflare, updated by `git pull`.
|
Production is one systemd unit on a small VPS behind Cloudflare; the app directory
|
||||||
`scripts/install.sh` (run as root from a checkout) creates the `water-monitor` user,
|
`/opt/thailand-water-monitor` is a git checkout. To update:
|
||||||
deploys to `/opt/thailand-water-monitor`, runs `uv sync` into `.venv`, installs
|
|
||||||
`water-monitor.service` and the monthly `water-monitor-retrain.timer`.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd /opt/thailand-water-monitor && git pull
|
||||||
|
cargo build --release --locked --manifest-path rust/Cargo.toml
|
||||||
sudo bash scripts/install.sh
|
sudo bash scripts/install.sh
|
||||||
sudo systemctl start water-monitor.service
|
|
||||||
systemctl list-timers water-monitor-retrain.timer
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The retrain timer runs `scripts/retrain.sh`, which trains into `models/.staging`,
|
`scripts/install.sh` (idempotent, also for a fresh host) installs the binary and
|
||||||
refuses to promote anything that is not a rain-enabled (`hgb-v3+`) set covering the
|
`ping-collector.service`, syncs the Python environment for the monthly
|
||||||
expected stations, and renames the bundles into place. Details and the operations
|
`water-monitor-retrain.timer`, runs a dry run of every source and a forecast check
|
||||||
runbook: [docs/FLOOD_FORECASTING.md](docs/FLOOD_FORECASTING.md) sections 6–8.
|
against the Python reference, restarts the service and waits for the website. The
|
||||||
|
retrain trains into `models/.staging`, refuses to promote anything that is not a
|
||||||
|
rain-enabled (`hgb-v3+`) set covering the expected stations, and renames the
|
||||||
|
bundles into place; the service picks them up on its next forecast. Details and
|
||||||
|
the operations runbook: [docs/FLOOD_FORECASTING.md](docs/FLOOD_FORECASTING.md)
|
||||||
|
sections 6–8. Configuration: [.env.example](.env.example).
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
```
|
```
|
||||||
src/ collector, API (web_api.py), dashboard (static/dashboard.html)
|
rust/collector/ ping-collector: collection, forecast run, ntfy/Matrix, website + API (src/web)
|
||||||
src/ml/ features, training, evaluation harness, prediction, rain/dam/HII loaders
|
rust/forecast/ forecast inference from the exported models; parity tests against Python
|
||||||
scripts/ train_flood_model.py, retrain.sh, evaluate_variants.py, install.sh, dev_proxy.py
|
src/static/ the dashboard (single file, EN/TH) and its map data
|
||||||
tests/ pytest suite (synthetic data; no DB or network)
|
src/data/ the bundled station list
|
||||||
docs/ FLOOD_FORECASTING.md, DATA_SOURCES.md, deployment and station guides
|
src/ml/ features, training, evaluation harness, export; predict.py is the reference
|
||||||
models/ trained bundles + metrics.json (gitignored) and evaluation results (tracked)
|
src/*.py HII and RID-dam clients/stores for the archive backfills
|
||||||
.gitea/workflows/ ci (format/lint/tests), security (pip-audit/bandit), docs (link + OpenAPI checks)
|
scripts/ install.sh, retrain.sh, train/evaluate/backfill tools, systemd units
|
||||||
|
tests/ pytest (synthetic data; database tests need TEST_DATABASE_URL)
|
||||||
|
docs/ model write-up, data sources, notifications, Matrix, rewrite history
|
||||||
|
models/ trained bundles (gitignored) and evaluation results
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [docs/FLOOD_FORECASTING.md](docs/FLOOD_FORECASTING.md) — the model: data, features, evaluation, measured performance, negatives, deployment, retraining
|
- [docs/FLOOD_FORECASTING.md](docs/FLOOD_FORECASTING.md) — the model: data, features, evaluation, measured performance, negatives, deployment, retraining
|
||||||
- [docs/DATA_SOURCES.md](docs/DATA_SOURCES.md) — every ingested and candidate source, endpoints, quirks
|
- [docs/DATA_SOURCES.md](docs/DATA_SOURCES.md) — every ingested and candidate source, endpoints, quirks
|
||||||
- [docs/STATION_MANAGEMENT_GUIDE.md](docs/STATION_MANAGEMENT_GUIDE.md) — adding/editing gauges
|
|
||||||
- [docs/DATABASE_DEPLOYMENT_GUIDE.md](docs/DATABASE_DEPLOYMENT_GUIDE.md), [POSTGRESQL_SETUP.md](POSTGRESQL_SETUP.md) — database setup
|
|
||||||
- [docs/NOTIFICATIONS.md](docs/NOTIFICATIONS.md) — public push alerts: topics, semantics, ntfy deployment
|
- [docs/NOTIFICATIONS.md](docs/NOTIFICATIONS.md) — public push alerts: topics, semantics, ntfy deployment
|
||||||
- [docs/MATRIX_QUICK_START.md](docs/MATRIX_QUICK_START.md) — Matrix room alerts for a team
|
- [docs/MATRIX_QUICK_START.md](docs/MATRIX_QUICK_START.md) — the same alerts in a Matrix room
|
||||||
- [docs/GAP_FILLING_GUIDE.md](docs/GAP_FILLING_GUIDE.md) — data integrity tooling
|
- [rust/README.md](rust/README.md) — the service: commands, configuration, develop, deploy
|
||||||
|
- [docs/RUST_REWRITE.md](docs/RUST_REWRITE.md) — why and how it moved from Python to Rust
|
||||||
- [docs/references/NOTABLE_DOCUMENTS.md](docs/references/NOTABLE_DOCUMENTS.md) — official Thai government resources
|
- [docs/references/NOTABLE_DOCUMENTS.md](docs/references/NOTABLE_DOCUMENTS.md) — official Thai government resources
|
||||||
- Public overview: [buildfor.life/docs/tooling/ping-river-monitor](https://buildfor.life/docs/tooling/ping-river-monitor/)
|
- Public overview: [buildfor.life/docs/tooling/ping-river-monitor](https://buildfor.life/docs/tooling/ping-river-monitor/)
|
||||||
|
|
||||||
Other database backends (VictoriaMetrics, InfluxDB, MySQL, SQLite) and the Grafana
|
|
||||||
dashboards under `grafana/` are supported by the adapters but not what production
|
|
||||||
runs; see [docs/VICTORIAMETRICS_SETUP.md](docs/VICTORIAMETRICS_SETUP.md) if you want them.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
`make format` before committing (black 88 columns, isort black profile — the CI gate),
|
`make format` before committing, `make check` must stay green; see
|
||||||
`make test` must stay green, tests use synthetic data only. See
|
|
||||||
[CONTRIBUTING.md](CONTRIBUTING.md). Issues and merge requests on
|
[CONTRIBUTING.md](CONTRIBUTING.md). Issues and merge requests on
|
||||||
[git.b4l.co.th](https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor).
|
[git.b4l.co.th](https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor).
|
||||||
|
|
||||||
## Data sources and thanks
|
## Data sources and thanks
|
||||||
|
|
||||||
Royal Irrigation Department (RID) gauge telemetry; Hydro-Informatics Institute (HII) /
|
Royal Irrigation Department (RID) gauge telemetry and reservoir reports;
|
||||||
ThaiWater open API; Open-Meteo; OpenStreetMap contributors for the river geometry;
|
Hydro-Informatics Institute (HII) / ThaiWater open API; Open-Meteo; OpenStreetMap
|
||||||
Chiang Mai Municipality for the inundation map the P.1 stages are keyed to. All
|
contributors for the river geometry; Chiang Mai Municipality for the inundation map
|
||||||
instruments are theirs; we aggregate, store, fill gaps and forecast.
|
the P.1 stages are keyed to. All instruments are theirs; we aggregate, store, fill
|
||||||
|
gaps and forecast.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user