diff --git a/docs/DASHBOARD_REDESIGN.md b/docs/DASHBOARD_REDESIGN.md index f3d5b8d..8906c80 100644 --- a/docs/DASHBOARD_REDESIGN.md +++ b/docs/DASHBOARD_REDESIGN.md @@ -1,6 +1,6 @@ # Dashboard redesign: the console -Status: **phase 2 done** (2026-10-19). The new dashboard is built next to the current +Status: **phase 3 done** (2026-10-19). The new dashboard is built next to the current one and replaces it in phase 4. - **Preview:** , after `git pull` on the @@ -52,8 +52,8 @@ One screen, no page scroll on a desktop: | 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; 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. 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, API, source code | +| 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:** @@ -110,6 +110,8 @@ One screen, no page scroll on a desktop: - **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 @@ -128,9 +130,9 @@ Each phase is one commit, checked with screenshots: 1440, 1100 and 390 px; light - **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. **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`. +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. diff --git a/src/static/console.html b/src/static/console.html index 75efb00..79ed960 100644 --- a/src/static/console.html +++ b/src/static/console.html @@ -36,7 +36,7 @@ --accent: #1d6fd6; --accent-soft: #e9f1fc; --on-accent: #ffffff; --normal: #16a34a; --watch: #d97706; --warning: #ea580c; --danger: #dc2626; --nodata: #9ca3af; --normal-ink: #15803d; --watch-ink: #b45309; --warning-ink: #c2410c; --danger-ink: #b91c1c; --nodata-ink: #6b7280; - --river: #2f7dd1; --river-2: #8cbbe8; --tiles: grayscale(1) brightness(1.06) contrast(.8); --tiles-opacity: .75; + --flow: #0d9488; --issued: #7c3aed; --river: #2f7dd1; --river-2: #8cbbe8; --tiles: grayscale(1) brightness(1.06) contrast(.8); --tiles-opacity: .75; --shadow: 0 8px 28px rgba(17, 24, 39, .14); } [data-theme="dark"] { @@ -45,7 +45,7 @@ --accent: #4c9bf0; --accent-soft: #14243a; --on-accent: #06101e; --normal: #34d399; --watch: #fbbf24; --warning: #fb923c; --danger: #f87171; --nodata: #5d6879; --normal-ink: #34d399; --watch-ink: #fbbf24; --warning-ink: #fb923c; --danger-ink: #f87171; --nodata-ink: #8b95a7; - --river: #4c9bf0; --river-2: #2e5f96; --tiles: grayscale(1) invert(1) brightness(.62) contrast(.85); --tiles-opacity: .8; + --flow: #2dd4bf; --issued: #a78bfa; --river: #4c9bf0; --river-2: #2e5f96; --tiles: grayscale(1) invert(1) brightness(.62) contrast(.85); --tiles-opacity: .8; --shadow: 0 8px 28px rgba(0, 0, 0, .5); } * { box-sizing: border-box; margin: 0; padding: 0; } @@ -206,7 +206,39 @@ .scale-ticks span { position: absolute; white-space: nowrap; } svg.chart { width: 100%; height: 180px; display: block; } svg.chart text { font-size: 10.5px; fill: var(--muted); font-family: inherit; } - .chart-legend { display: flex; gap: 12px; font-size: 11.5px; color: var(--muted); margin-top: 4px; } + .chart-legend { display: flex; gap: 4px 12px; flex-wrap: wrap; font-size: 11.5px; color: var(--muted); margin-top: 4px; } + .chart-box { position: relative; min-height: 250px; } + .chart-box svg { display: block; width: 100%; touch-action: pan-y; } + .chart-box svg text { font-size: 10.5px; fill: var(--muted); font-family: inherit; } + .chart-box svg text.halo { paint-order: stroke; stroke: var(--panel); stroke-width: 3px; stroke-linejoin: round; font-weight: 500; } + .tip { position: absolute; top: 4px; z-index: 5; pointer-events: none; padding: 7px 9px; border: 1px solid var(--line); border-radius: 7px; background: var(--panel); + box-shadow: var(--shadow); font-size: 11.5px; line-height: 1.5; white-space: nowrap; } + .tip b { font-weight: 600; } + .tip span { color: var(--muted); } + .hist-ctl { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin-bottom: 8px; } + .hist-ctl .seg button { padding: 3px 8px; } + .dates { display: inline-flex; align-items: center; gap: 4px; color: var(--muted); } + input[type="date"] { background: var(--panel-2); border: 1px solid var(--line-2); border-radius: 6px; color: var(--text); font: inherit; font-size: 12px; padding: 2px 5px; } + [data-theme="dark"] input[type="date"] { color-scheme: dark; } + details.sect > summary { cursor: pointer; list-style: none; display: flex; justify-content: space-between; align-items: center; } + details.sect > summary::-webkit-details-marker { display: none; } + details.sect > summary::after { content: "+"; font-size: 15px; font-weight: 500; color: var(--muted); } + details.sect[open] > summary::after { content: "−"; } + details.sect[open] > summary { margin-bottom: 9px; } + .headline { font-weight: 600; margin-bottom: 4px; } + .headline.better { color: var(--normal-ink); } .headline.worse { color: var(--warning-ink); } + .tablewrap { overflow-x: auto; margin-top: 8px; } + table.small { font-size: 11.5px; } + table.small td, table.small th { padding-right: 10px; white-space: nowrap; } + table.small tr.current td { font-weight: 600; } + table.small tr.young td { color: var(--muted); } + .linklike { color: var(--text-2); font-size: 11.5px; text-decoration: underline dotted; text-underline-offset: 3px; } + .pop { position: fixed; z-index: 1500; left: 14px; bottom: 34px; width: min(420px, calc(100vw - 28px)); padding: 14px 16px; border: 1px solid var(--line); + border-radius: 10px; background: var(--panel); box-shadow: var(--shadow); } + .pop dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 14px; } + .pop dt { color: var(--muted); } + .pop dd b { font-weight: 600; } + .pop dd small { display: block; color: var(--muted); font-size: 11.5px; } table { width: 100%; border-collapse: collapse; } th { text-align: left; padding: 3px 0 6px; } td { padding: 6px 0; border-top: 1px solid var(--line); } @@ -380,6 +412,7 @@ + +