From b02e815d721653a04d529880c916aaf4ba570d76 Mon Sep 17 00:00:00 2001 From: grabowski Date: Thu, 13 Aug 2026 23:10:34 +0700 Subject: [PATCH] feat: Thai localisation and portrait-phone layout for the dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thai is the default unless the browser prefers English, chosen by first match in navigator.languages order and remembered in localStorage. A STRINGS table carries both languages (interpolated strings as functions), applyTranslations() drives static markup via data-i18n attributes, and setLang() rebuilds everything the JS renders — including map layers, so popups render in the current language and sensor markers are replaced rather than stacked. Thai dates use the Buddhist era, matching the replay label; station names lead with the reader's language. Portrait phones: the header overflowed a 412 px Android viewport by 64 px, so the page scrolled sideways and the Refresh button sat off screen. The header now wraps into two deliberate rows (DOM order matches visual order, so focus order is unaffected), the map description box is hidden on phones, map height is capped by viewport — including a height-gated rule for landscape phones, whose 850-960 px widths never matched the width breakpoints — and the forecast grid goes single column. Verified in a real browser at 412x915, 360x800, 915x412 and 1440x900: zero horizontal overflow, no desktop change. Review-swarm fixes: a language switch no longer relabels a pinned SIMULATION or the 2024 replay as LIVE DATA (it kept the mode from state); Thai wording corrected where it asserted a rising trend the code never checks, labelled every gauge 'critical', or used a malformed compound; aria-labels, the Leaflet load failure and the flood-stage chips are translated; the language toggle states its action instead of an aria-pressed value that contradicted its label; and Thai font families sit after the Latin stack so they cannot restyle English text. --- src/static/dashboard.html | 896 ++++++++++++++++++++++++++++++-------- tests/test_dashboard.py | 114 ++++- 2 files changed, 823 insertions(+), 187 deletions(-) diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 45551c3..7e21cac 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -36,19 +36,38 @@ } * { box-sizing: border-box; } html, body { margin: 0; min-height: 100%; } + /* Nothing may push the page sideways on a phone: wide content scrolls + inside its own container instead (the map and chart already do). */ + html { overflow-x: hidden; } body { color: var(--ink); + overflow-x: hidden; background: radial-gradient(circle at 8% 0%, rgba(56, 180, 213, .12), transparent 25rem), var(--paper); - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + /* Thai families first so Thai text uses a proper Thai face where one + is installed (Android/iOS/Windows all ship one); Latin falls + through to Inter. No webfont download — this page is served over a + bandwidth-constrained uplink. */ + /* Thai faces sit AFTER the Latin stack: a Thai family listed ahead + of system-ui would also claim the Latin glyphs and restyle the + whole English page. Latin resolves first, Thai falls through. */ + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", "Noto Sans Thai", "Sarabun", "Leelawadee UI", "Thonburi", sans-serif; } + /* Thai has no inter-word spaces: let the browser segment, and allow a + break anywhere rather than letting a long phrase force a scrollbar. */ + html[lang="th"] body { line-break: normal; overflow-wrap: anywhere; } + html[lang="th"] .stat-value, html[lang="th"] .flow-value { overflow-wrap: normal; } + .lang-toggle { padding: 9px 12px; font-size: .78rem; font-weight: 800; white-space: nowrap; } + .lang-toggle[data-active-lang="th"] { background: var(--mint); border-color: #b6dfc9; color: #146644; } .shell { max-width: 1500px; margin: 0 auto; padding: 24px; } header { display: flex; align-items: center; justify-content: space-between; gap: 20px; - margin-bottom: 20px; + margin-bottom: 20px; flex-wrap: wrap; } - .brand { display: flex; align-items: center; gap: 14px; } + .brand { display: flex; align-items: center; gap: 14px; min-width: 0; } + .brand > div { min-width: 0; } .brand-mark { width: 48px; height: 48px; border-radius: 15px; display: grid; place-items: center; color: white; font-size: 25px; background: linear-gradient(145deg, #0a91b9, #076787); @@ -56,7 +75,7 @@ } h1 { margin: 0; font-size: clamp(1.4rem, 2.5vw, 2rem); letter-spacing: -.035em; } .subtitle { margin: 4px 0 0; color: var(--muted); font-size: .92rem; } - .header-actions { display: flex; align-items: center; gap: 12px; } + .header-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } .live-pill { display: flex; gap: 8px; align-items: center; padding: 9px 13px; border-radius: 999px; background: var(--mint); color: #146644; font-weight: 750; font-size: .8rem; @@ -193,22 +212,52 @@ /* No fixed cap: the sensors expander must stay reachable below the RID list */ .side-card { max-height: 70vh; } #river-flow { min-height: 160px; } + /* Landscape phones are ~400 px tall: a fixed 640 px map filled more + than the screen and buried everything under it. */ + #station-map { height: min(640px, 78vh); min-height: 320px; } } @media (max-width: 560px) { .shell { padding: 14px; } - header { align-items: flex-start; } + /* Portrait phones: brand on its own row, controls spread on the next. + Side by side the nowrap buttons overflowed the viewport (~64 px at + 412 px wide) and squeezed the title into three lines. */ + header { align-items: stretch; flex-direction: column; gap: 12px; margin-bottom: 16px; } + .brand-mark { width: 40px; height: 40px; border-radius: 13px; font-size: 21px; } + /* Two deliberate rows instead of a ragged wrap: the compact controls + share the first, the long replay label owns the second. The DOM + is already in this order, so focus order matches what is seen. */ + .header-actions { width: 100%; justify-content: flex-start; gap: 8px; } + #refresh-button { flex: 1 1 auto; text-align: center; } + #replay-2024 { flex: 1 1 100%; text-align: center; } .subtitle { display: none; } .live-pill { padding: 8px; } #live-pill-text { display: none; } /* dot stays: live vs replay/simulation signal */ #replay-2024 { white-space: nowrap; padding: 9px 11px; font-size: .72rem; } .stats { gap: 8px; } .stat { min-height: 96px; padding: 14px; } - .stat-value { font-size: 1.25rem; } - #station-map { height: 540px; } + .stat-value { font-size: 1.25rem; white-space: normal; } + #station-map { height: 62vh; min-height: 320px; max-height: 560px; } + } + /* Landscape phones are 380-430 px TALL but 850-960 px wide, so the width + breakpoints above never catch them and a 640 px map filled the screen. + Gate this one on height. */ + @media (max-height: 520px) and (orientation: landscape) { + #station-map { height: 80vh; min-height: 0; max-height: none; } + .workspace { min-height: 0; } + .side-card { max-height: 80vh; } .map-overlay { left: 46px; } + /* The description box covered a quarter of the map on a phone */ + .map-heading { display: none; } #legend-toggle { display: inline-flex; } .legend { display: none; } .legend.open { display: block; max-height: 42vh; overflow-y: auto; } + /* Range controls: dropdown on one row, the two dates sharing the next */ + .history-controls { width: 100%; } + .history-controls select { flex: 1 1 100%; } + .history-controls input[type="date"] { flex: 1 1 0; min-width: 0; } + .forecast-grid { grid-template-columns: 1fr; } + .stage-chip { flex: 1 1 auto; min-width: 66px; } + footer { flex-direction: column; align-items: flex-start; gap: 10px; } } @@ -218,14 +267,15 @@
-

Ping River Live Monitor

-

Current water level and discharge across Northern Thailand

+

Ping River Live Monitor

+

Current water level and discharge across Northern Thailand

-
LIVE DATA
- - +
LIVE DATA
+ + +
@@ -234,76 +284,76 @@
-
Experimental model — not an official warning · official alerts: ThaiWater ↗
+
Experimental model — not an official warning · official alerts: ThaiWater ↗
-
-
Reporting stations
RID gauges with current readings
-
Chiang Mai river flow
P.1 Nawarat Bridge · m³/s
-
Most stressed gauge
Awaiting station data
-
Last updated
Loading latest readings
+
+
Reporting stations
RID gauges with current readings
+
Chiang Mai river flow
P.1 Nawarat Bridge · m³/s
+
Most stressed gauge
Awaiting station data
+
Last updated
Loading latest readings
-
+
-
Station flow mapRiver width, colour & dash speed follow live discharge
+
Station flow mapRiver width, colour & dash speed follow live discharge
- +
-
Flow status
-
Low < 25 m³/s
-
Moderate 25–100
-
High 100–250
-
Very high > 250
-
River · no nearby gauge
-
River · gauge colour, dashes = flow
-
Rainfall · 24 h - +
Flow status
+
Low < 25 m³/s
+
Moderate 25–100
+
High 100–250
+
Very high > 250
+
River · no nearby gauge
+
River · gauge colour, dashes = flow
+
Rainfall · 24 h +
-
Light 0.1–10 mm
-
Moderate 10–35
-
Heavy 35–90
-
Very heavy 90–150
-
Extreme > 150
-
Other markers
-
Gauge · no recent data
-
+ Water-level sensor · colour = % of bank height
+
Light 0.1–10 mm
+
Moderate 10–35
+
Heavy 35–90
+
Very heavy 90–150
+
Extreme > 150
+
Other markers
+
Gauge · no recent data
+
+ Water-level sensor · colour = % of bank height
-
Loading river conditions…
-
Map data could not be loaded.
+
Loading river conditions…
+
Map data could not be loaded.