feat(dashboard): console phase 4 - 2024 replay, simulation, replaces the old page
The console is now the dashboard at / (src/static/dashboard.html); the old page and console.html are gone. - Replay of the October 2024 flood (flood-2024-all.json): the page's clock, verdict, list, markers, flood zones and panel follow the replay; the panel charts the season on fixed axes with the 2024 model's 24 h peak. Hourly while the model alerts or P.1 is above 3.50 m, faster through quiet days; pause and drag. Rain, basin stations and the 5-minute refresh are off during the replay (today's data). - ?demo_level= and ?demo_rise= simulate P.1. A violet REPLAY / amber SIMULATION bar, the top bar's KPI and the P.1 panel say so; the mode is drawn from state.mode alone, so a refresh or language switch cannot relabel either as live. - History: rows without discharge_percent are ThaiWater's copy of the gauge (night hours RID publishes late), with a different rating (P.1: 26 against RID's 54 m3/s); their discharge is not drawn for gauges with RID discharge. The discharge strip has headroom so its label stays clear of the line. - Swap: the old page's title, description, Open Graph and JSON-LD tags; no noindex; tests/test_dashboard.py = console tests + the old checks that still apply (one P.1 level, SEO files) + a replay/simulation labelling test. - Docs: README screenshot (docs/img/dashboard.jpg, replacing two unused images), CLAUDE.md, CONTRIBUTING.md, FLOOD_FORECASTING.md (where the model shows), DASHBOARD_REDESIGN.md (phase 4 done, mode rules). Deploy needs a restart: the server reads dashboard.html at start.
This commit is contained in:
1 parent
ffd3030352
commit
cf10f2e4c8
15 files changed
+2419
-4599
No files matched your search
+20
-18
@@ -1,19 +1,19 @@
|
||||
# Dashboard redesign: the console
|
||||
|
||||
Status: **phase 3 done** (2026-10-19). The new dashboard is built next to the current
|
||||
one and replaces it in phase 4.
|
||||
Status: **phase 4 done** (2026-10-20). The console is the dashboard: it replaced the
|
||||
old page at `/` (`src/static/dashboard.html`; the server reads it at start, so a
|
||||
change needs a restart). Phase 5 (polish) is open.
|
||||
|
||||
- **Preview:** <https://water.buildfor.life/static/console.html>, after `git pull` on the
|
||||
server (static files are read per request, no restart). It is not indexed
|
||||
(`noindex`) and links back to the current dashboard.
|
||||
- **Locally:** `uv run python scripts/dev_proxy.py 8765`, then
|
||||
<http://localhost:8765/static/console.html>. The API is proxied to production.
|
||||
- **Rules:** the same as for `dashboard.html`, plus no emoji (`tests/test_console.py`):
|
||||
- **Locally:** `uv run python scripts/dev_proxy.py 8765`, then <http://localhost:8765/>.
|
||||
The API is proxied to production.
|
||||
- **Rules** (`tests/test_dashboard.py`):
|
||||
- every path it fetches is a Rust route;
|
||||
- every string exists in EN and TH, and Thai is the default;
|
||||
- timestamps are Bangkok wall-clock time.
|
||||
- no emoji;
|
||||
- timestamps are Bangkok wall-clock time;
|
||||
- a replay or a simulation is never shown as live data.
|
||||
|
||||
## Why: the current page
|
||||
## Why: the old page
|
||||
|
||||
Observed on 2026-10-19 at 1440 px and on a 390 px phone.
|
||||
|
||||
@@ -69,7 +69,7 @@ One screen, no page scroll on a desktop:
|
||||
|
||||
## Where every current element goes
|
||||
|
||||
| Current (`dashboard.html` id) | Console | Phase |
|
||||
| Old page (`dashboard.html` id) | Console | Phase |
|
||||
|---|---|---|
|
||||
| `flood-verdict`, `verdict-*` | Verdict in the top bar | 1 |
|
||||
| `station-count`, `last-updated`, `data-age`, `live-pill` | Top-bar KPIs (reporting, data age) | 1 |
|
||||
@@ -91,7 +91,7 @@ One screen, no page scroll on a desktop:
|
||||
## Design rules
|
||||
|
||||
- **Type:** Inter (Latin) and Noto Sans Thai, self-hosted in `src/static/fonts/` (75 KB, OFL). Numbers use tabular figures.
|
||||
- **Colours:** CSS variables for light and dark, with dark the default; a saved choice wins (`ping-river-theme`, shared with the current dashboard).
|
||||
- **Colours:** CSS variables for light and dark, with dark the default; a saved choice wins (`ping-river-theme`, the old dashboard's key, so the choice carried over).
|
||||
- **Accent:** `#1d6fd6` light, `#4c9bf0` dark.
|
||||
- **Status:** colours are used only for status, each with an `-ink` variant for text (WCAG AA on white).
|
||||
- **One status rule** (`statusOf`) for the map, list and panel. It is display-only; the notifier keeps its own transitions.
|
||||
@@ -104,14 +104,16 @@ One screen, no page scroll on a desktop:
|
||||
| Danger | level ≥ `threshold_danger` |
|
||||
| No data | no reading for 24 h |
|
||||
|
||||
- **City verdict:** the current dashboard's rule — flooding from P.1 ≥ 3.70 m; "likely" at danger ≥ 50 % or warning ≥ 70 %; watch at warning ≥ 20 % or level ≥ 3.20 m.
|
||||
- **City verdict:** the old dashboard's rule — flooding from P.1 ≥ 3.70 m; "likely" at danger ≥ 50 % or warning ≥ 70 %; watch at warning ≥ 20 % or level ≥ 3.20 m.
|
||||
- **Data age:** "feed stale" after 3 h, except during RID's overnight pause (21:00 to 09:00 with an evening reading). The pause shows as "overnight pause", the same rule as the notifier's `FEED_NIGHT_UNTIL_HOUR`.
|
||||
- **Discharge:** comes with RID's report. When the newest hour is a level-only reading, the panel shows the last RID discharge with its time ("at 14:00").
|
||||
- **Rain layer:** off by default unless any station has heavy rain (> 35 mm / 24 h); an explicit choice is remembered.
|
||||
- **Flood zones:** shown automatically while P.1 is at or above a stage, unless hidden by the user.
|
||||
- **Basin stations:** ThaiWater's classes for the level as a share of the bank height (critically low ≤ 10 %, low ≤ 30 %, normal ≤ 70 %, high ≤ 100 %, over the bank). On the map and in the list over the bank is danger, high is watch, the rest normal; no data after 24 h. Gauges RID already lists are shown once, as RID gauges. The layer is off until the list is opened or the button pressed.
|
||||
- **History:** above 2,500 readings (30 d and longer for most gauges) the chart shows the daily mean with the daily low–high band, so a flood peak is not averaged away; gaps longer than 3.5 h (2.5 days when daily) break the line rather than bridging RID's nightly pause. Short ranges reload with every refresh, long ones at most hourly. "All" asks for `limit=100000`: `/measurements/history` returns the oldest rows first and stops at its 50,000 default, which made the current dashboard's "All time" for P.1 end in October 2024 (fixed there too).
|
||||
- **History:** above 2,500 readings (30 d and longer for most gauges) the chart shows the daily mean with the daily low–high band, so a flood peak is not averaged away; gaps longer than 3.5 h (2.5 days when daily) break the line rather than bridging RID's nightly pause. Short ranges reload with every refresh, long ones at most hourly. "All" asks for `limit=100000`: `/measurements/history` returns the oldest rows first and stops at its 50,000 default, which made the old dashboard's "All time" for P.1 end in October 2024.
|
||||
- **Forecast accuracy:** loaded when the section is opened (a cold `/api/forecast/skill` query takes up to 20 s), cached for 15 minutes; bias and persistence error are in each row's tooltip.
|
||||
- **Replay of October 2024:** `src/static/flood-2024-all.json` (hourly readings of the 16 gauges, 15 September – 12 October 2024, and the 24 h warning chance and peak of a model trained on data before 1 September 2024). The page's clock, verdict, list, markers, flood zones and panel follow the replay; the panel's chart shows the season on fixed axes up to the replay hour. It plays hour by hour while the model alerts or P.1 is above 3.50 m, faster through quiet days, and can be paused and dragged. Rain, the basin stations and the 5-minute refresh are off during the replay, since they are today's data. A violet bar (REPLAY, date, P.1 level, the model's message) and the top bar's "Replay time" replace the live data age.
|
||||
- **Simulation:** `?demo_level=4.4` pins P.1 at 4.40 m; `?demo_rise=1` raises it by 5 cm every 0.7 s from the real level to the 2024 record (5.30 m). An amber SIMULATION bar names the real level; the top bar and the P.1 panel say "simulated". Only P.1 is simulated: the forecasts and other gauges stay live.
|
||||
- **Basemap:** `src/static/basemap/ping-basin.pmtiles`, a Protomaps extract (bbox 97.85–99.95 E, 16.85–20.2 N, zoom 0–12, 15 MB, OpenStreetMap data), drawn by protomaps-leaflet 5.1.0 in its `light`/`dark` style with `name:th`/`name:en` labels. Refresh instructions in `src/static/basemap/README.md`. The map cannot be panned far outside the basin. OSM raster tiles are the fallback if the renderer does not load.
|
||||
|
||||
## Phases
|
||||
@@ -124,7 +126,7 @@ Each phase is one commit, checked with screenshots: 1440, 1100 and 390 px; light
|
||||
- Map with status markers, rain and flood-zone layers.
|
||||
- Station panel (readings, scale, 72 h chart, forecast table, P.1 stages).
|
||||
- Alerts drawer (ntfy topics); URL selection; 5-minute refresh.
|
||||
- `tests/test_console.py`; `dev_proxy.py` serves `src/static` with proper content types.
|
||||
- `tests/test_console.py` (now `tests/test_dashboard.py`); `dev_proxy.py` serves `src/static` with proper content types.
|
||||
2. **Done — Map and basin.**
|
||||
- **Basemap:** the self-hosted Protomaps extract replaces the dimmed OSM raster tiles, whose baked-in labels were the noisiest part of the screen. Labels follow the page language; light and dark styles.
|
||||
- **Server:** ServeDir answers HTTP range requests (checked: 206); `dev_proxy.py` now does too.
|
||||
@@ -134,10 +136,10 @@ Each phase is one commit, checked with screenshots: 1440, 1100 and 390 px; light
|
||||
- **History in the panel** (inline SVG, no chart library): ranges and dates, level and discharge, issued 24 h forecasts overlaid, warning/danger bands, hover/touch values.
|
||||
- **P.1 forecast accuracy** as a collapsible panel section, from `/api/forecast/skill`.
|
||||
- **Data statistics:** a status-bar popover, from `/api/stats`.
|
||||
4. **Modes and the swap.**
|
||||
4. **Done — Modes and the swap.**
|
||||
- **October 2024 replay:** banner, clock, markers and verdict follow the replay; zones auto-show.
|
||||
- **Demo hooks:** `?demo_level=`, `?demo_rise=`. A mode pill (LIVE / REPLAY / SIMULATION) never relabels a replay or simulation as live.
|
||||
- **The swap:** `console.html` becomes `dashboard.html`; the old file and its tests go. `tests/test_console.py` absorbs the remaining checks of `tests/test_dashboard.py` (one P.1 level, SEO tags; the robots `noindex` is removed).
|
||||
- **The swap:** `console.html` became `dashboard.html`, with the old page's title, description, Open Graph and JSON-LD tags; the robots `noindex` is gone. `tests/test_dashboard.py` is the console's tests plus the old ones that still apply (one P.1 level, SEO and indexing files).
|
||||
- **Docs:** README screenshot and the CLAUDE.md rule.
|
||||
5. **Polish.**
|
||||
- Phone layout pass.
|
||||
@@ -149,5 +151,5 @@ Each phase is one commit, checked with screenshots: 1440, 1100 and 390 px; light
|
||||
## Data notes from building phase 1
|
||||
|
||||
- **Negative levels.** P.4A (−1.28 m) and P.67 (−0.59 m) report negative levels: gauge datums, not errors. The panel's scale starts at the lowest of 0 and the station's 72 h readings.
|
||||
- **P.1 discharge disagrees between endpoints.** At 15:00 on 2026-10-19, `/measurements/latest` had level 1.30 m with no discharge, while `/measurements/history` had 26.0 m³/s with no capacity. RID's 14:00 report said 53.9 m³/s. The console shows the last RID discharge. Worth checking where the 26.0 comes from.
|
||||
- **P.1 discharge disagrees between endpoints.** At 15:00 on 2026-10-19, `/measurements/latest` had level 1.30 m with no discharge, while `/measurements/history` had 26.0 m³/s with no capacity. RID's 14:00 report said 53.9 m³/s. The rows without capacity (`discharge_percent`) are ThaiWater's copy of the gauge, which fills hours RID has not published yet (its night hours); their discharge comes from a different rating, about half of RID's at P.1. The panel shows the last RID discharge, and the history chart drops the discharge of those rows for gauges RID reports discharge for.
|
||||
- **P.84 spike.** A one-hour spike at 13:00 the same day: 0.73 → 1.05 → 0.78 m.
|
||||
Reference in new issue
Block a user