- History in the station panel (inline SVG): 3 d / 7 d / 30 d / 90 d / 1 y / all or a date range; level and discharge, the 24 h peaks as the model issued them, warning/danger bands, hover/touch values. Up to 7 d the chart ends in the current forecast. Above 2,500 readings it shows the daily mean with the daily low-high band, so a flood peak is not averaged away; gaps (RID's nightly pause) break the line instead of being bridged. Days are Bangkok days computed arithmetically: Intl per reading took a minute for "All" (67,000 rows), now 2 s. - "How accurate is the forecast?" for P.1 (/api/forecast/skill), loaded when opened because a cold skill query takes up to 20 s. - Data statistics (/api/stats) behind "2.7M readings since 2018" in the status bar. fix(dashboard): "All time" history ended in October 2024. The history route returns the oldest rows first and stops at 50,000 by default; P.1 has about 67,000 hours. Both dashboards now ask for limit=100000 (the route's maximum).
154 lines
11 KiB
Markdown
154 lines
11 KiB
Markdown
# 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.
|
||
|
||
- **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`):
|
||
- 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.
|
||
|
||
## Why: the current page
|
||
|
||
Observed on 2026-10-19 at 1440 px and on a 390 px phone.
|
||
|
||
- **No clear first answer.** "Is Chiang Mai flooding?" is a thin green banner above
|
||
four KPI cards of equal weight. Two of them answer nothing for a resident: "Most
|
||
stressed gauge 16%" and "Chiang Mai river flow 53.9" (no unit in the number). At the
|
||
bottom there are five more (2,700,706 datapoints, 3,002 days, …).
|
||
- **The colours answer the wrong question.** Markers and river lines are coloured by
|
||
discharge class (m³/s), not by distance to the warning level, and the station list
|
||
shows discharge.
|
||
- **The map is noisy.** OSM terrain with Thai labels, numbered markers and animated
|
||
dashes. The legend covers about a third of the map, and more than half of the first
|
||
phone screen.
|
||
- **The header is busy.** It has six buttons with emoji (↻ 🌙 🔔 ▶); on a phone they
|
||
wrap into three rows.
|
||
- **Internal detail on the main page.** The model-skill table lists seven model hashes;
|
||
the history card is an empty 360 px box until a station is clicked.
|
||
- **Inconsistent styling.** A gradient background, several card and pill styles, three
|
||
greens.
|
||
|
||
## Decision
|
||
|
||
Two directions were sketched on 2026-10-19 against live data (throwaway HTML, not kept):
|
||
|
||
- **A:** a public information page.
|
||
- **B:** an operations console.
|
||
|
||
The user chose **B**.
|
||
|
||
## Layout
|
||
|
||
One screen, no page scroll on a desktop:
|
||
|
||
| Area | Content |
|
||
|---|---|
|
||
| Top bar | brand · **Chiang Mai verdict** (P.1 level + 24 h forecast, always visible, click selects P.1) · gauges reporting · highest 24 h flood chance · data age · EN/ไทย · light/dark · Get alerts |
|
||
| Left | two lists: **RID gauges** (status bar, code, name, margin to the warning level, 72 h sparkline, level and 6 h change; sort by river order, closest to the warning level, rising) and **Basin stations** (ThaiWater/HII: % of bank height, river and agency; sort fullest first or by name); filter; legend |
|
||
| Centre | map on a self-hosted vector basemap (labels in the page language): Ping mainstem and tributaries, gauges coloured by status with code + level labels, layers Basin stations (diamonds), Rain 24 h and Flood zones, the team's markers from zoom 12, one legend |
|
||
| Right | selected RID gauge: level, 6 h change, discharge, warning/danger level with margins, capacity; level scale with now and 24 h peak; history (3 d / 7 d / 30 d / 90 d / 1 y / all or dates: level, discharge, the 24 h peaks as issued, warning/danger bands, hover values; up to 7 d it ends in the current forecast); forecast table (peak, chance of warning/danger at 6/12/24 h, model, issue time, disclaimer); for P.1 the seven inundation stages, the zones toggle and "How accurate is the forecast?"; data links. A basin station shows level (m MSL), % of bank height with ThaiWater's class, distance to and height of the bank top, gauge reading, agency (latest reading only: no history is kept for them) |
|
||
| Status bar | data credits, "2.7M readings since 2018" (opens the data statistics), API, source code |
|
||
|
||
**Narrower screens:**
|
||
|
||
| Width | Layout |
|
||
|---|---|
|
||
| < 1280 px | The panel moves below list and map, in columns |
|
||
| < 860 px | One column: verdict, map, list, panel |
|
||
|
||
**Behaviour:**
|
||
- Selection is in the URL (`#P.84`, `#hii-1198299`); opening a basin station switches to its list.
|
||
- Arrow keys move through the list.
|
||
- Data refreshes every 5 minutes.
|
||
|
||
## Where every current element goes
|
||
|
||
| Current (`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 |
|
||
| `river-flow`, `peak-station` (P.1 flow, most stressed gauge) | Discharge and capacity in the panel; "Closest to warning level" sort | 1 |
|
||
| `station-map`, `map-legend`, `legend-toggle`, `rain-toggle`, `zones-toggle` | Map, layer buttons, one legend | 1 |
|
||
| side list, `station-search` | Gauge list with filter and sorts | 1 |
|
||
| `forecast-card`, `p1-outlook`, `p1-peak`, `p1-stages`, `forecast-grid` | Panel: forecast table for every gauge, stages for P.1 | 1 |
|
||
| `alerts-panel`, `alerts-*` | Alerts drawer | 1 |
|
||
| `refresh-button` | Removed: refresh every 5 min, age in the top bar | 1 |
|
||
| `lang-toggle`, `theme-toggle` | EN / ไทย segmented control, icon button | 1 |
|
||
| `error`, `loading` | Map overlay with retry; "Loading" in the list | 1 |
|
||
| `thaiwater-sensors`, `sensors-*` | Basin stations list + map layer | 2 |
|
||
| custom markers | Pins from zoom 12, popup with their flood zone | 2 |
|
||
| `history-card`, `history-*` | Panel history with ranges | 3 |
|
||
| `skill-panel`, `skill-*` | Panel section for P.1 | 3 |
|
||
| `db-stats`, `db-*` | Status-bar popover | 3 |
|
||
| `replay-2024`, `replay-*`, demo hooks | Replay/simulation mode with banner and mode pill | 4 |
|
||
|
||
## 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).
|
||
- **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.
|
||
|
||
| Status | Rule |
|
||
|---|---|
|
||
| Normal | none of the rules below |
|
||
| Watch | within 0.5 m of the warning level, or 24 h warning chance ≥ 25 % |
|
||
| Warning | level ≥ `threshold_warning` |
|
||
| 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.
|
||
- **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).
|
||
- **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.
|
||
- **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
|
||
|
||
Each phase is one commit, checked with screenshots: 1440, 1100 and 390 px; light and dark; EN and TH.
|
||
|
||
1. **Done — Console shell with live data.**
|
||
- Layout, design tokens, fonts, EN/TH.
|
||
- Verdict and KPIs, gauge list with sparklines and sorts.
|
||
- 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.
|
||
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.
|
||
- **ThaiWater/HII stations:** the Basin stations list and map layer, from `/api/hii/waterlevel/latest`, falling back to `/sensors/thaiwater`.
|
||
- **Custom markers:** `custom-markers.json` as pins, with their flood-zone popup.
|
||
3. **Done — Station depth.**
|
||
- **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.**
|
||
- **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).
|
||
- **Docs:** README screenshot and the CLAUDE.md rule.
|
||
5. **Polish.**
|
||
- Phone layout pass.
|
||
- Native-speaker review of the Thai strings.
|
||
- Keyboard and screen reader pass.
|
||
- Lighthouse ≥ 90 (performance, accessibility).
|
||
- No console errors.
|
||
|
||
## 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.84 spike.** A one-hour spike at 13:00 the same day: 0.73 → 1.05 → 0.78 m.
|