The user chose direction B of the redesign: a full-screen operations console.
It is built next to the current dashboard, served by ServeDir at
/static/console.html (noindex, links back to /), and replaces dashboard.html
once it covers every feature (docs/DASHBOARD_REDESIGN.md, phase 4).
Phase 1:
- top bar: the Chiang Mai verdict (P.1 level + 24 h forecast, the current
dashboard's rule), gauges reporting, highest 24 h flood chance, data age
("overnight pause" instead of "stale" during RID's nightly gap, as in the
notifier), EN/TH, light/dark (dark default), Get alerts
- gauge list: one status rule for list, map and panel (normal, watch within
0.5 m or >= 25 % chance, warning, danger, no data), margin to the warning
level, 72 h sparkline, 6 h change; filter; sort by river order, closest to
the warning level, rising; arrow keys
- map: Ping mainstem and tributaries, status markers with code + level,
layers Rain 24 h (on by default only with heavy rain) and Flood zones
(auto-shown while P.1 is at a stage), one legend
- station panel: level, change, discharge (last RID value when the newest
hour is level-only), warning/danger levels with margins, capacity, level
scale with now and 24 h peak, 72 h chart with the forecast peaks, forecast
table with the model and issue time, the P.1 inundation stages, data links
- alerts drawer with the ntfy topics; selection in the URL (#P.84); refresh
every 5 minutes
- Inter and Noto Sans Thai self-hosted (src/static/fonts, 75 KB, OFL), no
emoji, Thai first, Bangkok time
tests/test_console.py: routed fetch paths, EN/TH parity, translated aria
labels, no emoji, fonts and licences, noindex. dev_proxy.py serves
src/static with real content types (it sent HTML as octet-stream).
9.2 KiB
Dashboard redesign: the console
Status: phase 1 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 pullon 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 | gauge list: status bar, code, name, margin to the warning level, 72 h sparkline, level and 6 h change; filter; sort by river order, closest to the warning level, rising; status legend |
| Centre | map: Ping mainstem and tributaries, gauges coloured by status with code + level labels, layers Rain 24 h and Flood zones, one legend |
| Right | selected gauge: level, 6 h change, discharge, warning/danger level with margins, capacity; level scale with now and 24 h peak; 72 h chart with the forecast peaks; forecast table (peak, chance of warning/danger at 6/12/24 h, model, issue time, disclaimer); for P.1 the seven inundation stages and the zones toggle; data links |
| Status bar | data credits, 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). - 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-* |
List tab + map layer | 2 |
| custom markers | Map layer | 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:
#1d6fd6light,#4c9bf0dark. - Status: colours are used only for status, each with an
-inkvariant for text (WCAG AA on white).
- Accent:
-
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_warningDanger level ≥ threshold_dangerNo 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.
Phases
Each phase is one commit, checked with screenshots: 1440, 1100 and 390 px; light and dark; EN and TH.
- 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.pyservessrc/staticwith proper content types.
- Map and basin.
- Basemap: a self-hosted Protomaps extract of the Ping basin (PMTiles in
src/static/, labels in Thai or English following the page language, light/dark themes). This replaces the dimmed OSM raster tiles, whose baked-in labels are the noisiest part of the screen. - Server: check that ServeDir answers HTTP range requests (PMTiles needs them); add range support to
dev_proxy.py. - ThaiWater/HII stations:
- A tab in the list.
- A map layer: % of bank height,
/api/hii/waterlevel/latest, falling back to/sensors/thaiwater.
- Custom markers:
custom-markers.json, with their flood-zone popup.
- Basemap: a self-hosted Protomaps extract of the Ping basin (PMTiles in
- Station depth.
- History in the panel: range 24 h / 7 d / 30 d / 90 d / all / custom dates, level and discharge, issued 24 h forecasts overlaid, flood bands; Chart.js or SVG.
- P.1 forecast skill ("Is the model getting better?") as a panel section, from
/api/forecast/skill. - Data statistics: a status-bar popover, from
/api/stats.
- 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.htmlbecomesdashboard.html; the old file and its tests go.tests/test_console.pyabsorbs the remaining checks oftests/test_dashboard.py(one P.1 level, SEO tags; the robotsnoindexis removed). - Docs: README screenshot and the CLAUDE.md rule.
- 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/latesthad level 1.30 m with no discharge, while/measurements/historyhad 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.