fix: UI/UX review round 1 — mobile reachability, legend toggle, replay correctness
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 25s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 13s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s

From the multi-lens review (27 confirmed findings; this lands the quick
high-impact set):
- mobile: side-card cap 400px->70vh so the basin-stations expander is
  reachable; live-pill kept as a dot (title carries the label); replay
  button no longer wraps; legend returns behind a Legend toggle button
  (was display:none, which also removed the only rain toggle)
- replay: auto-refresh skips while replaying (was clobbering state);
  Chiang Mai flow tile shows P.1 replay discharge instead of a
  basin-wide sum mislabelled as P.1
- history: placeholder text in the empty chart, dropdown flips to
  'Custom range' when dates are hand-edited, controls hint when no
  station is selected
- a11y/copy: aria-expanded/aria-controls on all three disclosure
  controls, sensor rows say 'of bank height' vs tile 'channel capacity',
  P.1 tile note deduped
This commit is contained in:
2026-08-11 16:39:53 +07:00
parent 09c1c84153
commit 5848621c77
+42 -14
View File
@@ -177,21 +177,33 @@
.popup-metric span { display: block; color: var(--muted); font-size: .62rem; } .popup-metric span { display: block; color: var(--muted); font-size: .62rem; }
.popup-metric strong { display: block; margin-top: 2px; font-size: .86rem; } .popup-metric strong { display: block; margin-top: 2px; font-size: .86rem; }
.popup-time { margin-top: 9px; color: var(--muted); font-size: .64rem; } .popup-time { margin-top: 9px; color: var(--muted); font-size: .64rem; }
#legend-toggle {
display: none; pointer-events: auto; background: rgba(255,255,255,.93); border: 1px solid rgba(207,224,218,.9);
border-radius: 11px; padding: 8px 12px; font-size: .72rem; font-weight: 800; color: var(--ink);
box-shadow: 0 7px 20px rgba(22,58,68,.12);
}
@media (max-width: 900px) { @media (max-width: 900px) {
.stats { grid-template-columns: repeat(2, 1fr); } .stats { grid-template-columns: repeat(2, 1fr); }
.workspace { grid-template-columns: 1fr; } .workspace { grid-template-columns: 1fr; }
.side-card { max-height: 400px; } /* No fixed cap: the sensors expander must stay reachable below the RID list */
.side-card { max-height: 70vh; }
#river-flow { min-height: 160px; }
} }
@media (max-width: 560px) { @media (max-width: 560px) {
.shell { padding: 14px; } .shell { padding: 14px; }
header { align-items: flex-start; } header { align-items: flex-start; }
.subtitle, .live-pill { display: none; } .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; } .stats { gap: 8px; }
.stat { min-height: 96px; padding: 14px; } .stat { min-height: 96px; padding: 14px; }
.stat-value { font-size: 1.25rem; } .stat-value { font-size: 1.25rem; }
#station-map { height: 540px; } #station-map { height: 540px; }
.map-overlay { left: 46px; } .map-overlay { left: 46px; }
#legend-toggle { display: inline-flex; }
.legend { display: none; } .legend { display: none; }
.legend.open { display: block; max-height: 42vh; overflow-y: auto; }
} }
</style> </style>
</head> </head>
@@ -224,7 +236,9 @@
<div id="station-map" role="application" aria-label="Interactive map of Ping River monitoring stations"></div> <div id="station-map" role="application" aria-label="Interactive map of Ping River monitoring stations"></div>
<div class="map-overlay"> <div class="map-overlay">
<div class="map-heading"><strong>Station flow map</strong><span>River width, colour &amp; dash speed follow live discharge</span></div> <div class="map-heading"><strong>Station flow map</strong><span>River width, colour &amp; dash speed follow live discharge</span></div>
<div class="legend"> <div style="display:flex;flex-direction:column;align-items:flex-end;gap:8px;min-width:0">
<button type="button" id="legend-toggle" aria-expanded="false" aria-controls="map-legend">☰ Legend</button>
<div class="legend" id="map-legend">
<div class="legend-title">Flow status</div> <div class="legend-title">Flow status</div>
<div class="legend-row"><i class="swatch" style="background:#1e8b60"></i> Low &lt; 25 m³/s</div> <div class="legend-row"><i class="swatch" style="background:#1e8b60"></i> Low &lt; 25 m³/s</div>
<div class="legend-row"><i class="swatch" style="background:#087da5"></i> Moderate 25100</div> <div class="legend-row"><i class="swatch" style="background:#087da5"></i> Moderate 25100</div>
@@ -241,6 +255,7 @@
<div class="legend-row"><i class="swatch" style="background:#d99018;border-radius:50%"></i> Very heavy 90150</div> <div class="legend-row"><i class="swatch" style="background:#d99018;border-radius:50%"></i> Very heavy 90150</div>
<div class="legend-row"><i class="swatch" style="background:#cc4b37;border-radius:50%"></i> Extreme &gt; 150</div> <div class="legend-row"><i class="swatch" style="background:#cc4b37;border-radius:50%"></i> Extreme &gt; 150</div>
</div> </div>
</div>
</div> </div>
<div class="loading-panel" id="loading"><div class="loading-card">Loading river conditions…</div></div> <div class="loading-panel" id="loading"><div class="loading-card">Loading river conditions…</div></div>
<div class="error-panel" id="error"><div><strong>Map data could not be loaded.</strong><br><span id="error-message"></span></div></div> <div class="error-panel" id="error"><div><strong>Map data could not be loaded.</strong><br><span id="error-message"></span></div></div>
@@ -252,7 +267,7 @@
<input type="search" id="station-search" placeholder="Search stations · code, name, river…" aria-label="Search stations" style="margin-top:10px;width:100%;box-sizing:border-box;padding:9px 12px;border:1px solid var(--border);border-radius:10px;background:white;font-size:.8rem"> <input type="search" id="station-search" placeholder="Search stations · code, name, river…" aria-label="Search stations" style="margin-top:10px;width:100%;box-sizing:border-box;padding:9px 12px;border:1px solid var(--border);border-radius:10px;background:white;font-size:.8rem">
</div> </div>
<div class="station-list" id="river-flow" aria-live="polite"></div> <div class="station-list" id="river-flow" aria-live="polite"></div>
<div class="side-head" id="sensors-head" style="cursor:pointer" role="button" tabindex="0" title="Show / hide the ThaiWater/HII station list"><h2>Additional basin stations <span id="sensors-arrow" style="color:var(--muted);font-size:.8rem"></span></h2><p id="thaiwater-count">Loading ThaiWater/HII stations…</p></div> <div class="side-head" id="sensors-head" style="cursor:pointer" role="button" tabindex="0" aria-expanded="false" aria-controls="thaiwater-sensors" title="Show / hide the ThaiWater/HII station list"><h2>Additional basin stations <span id="sensors-arrow" style="color:var(--muted);font-size:.8rem"></span></h2><p id="thaiwater-count">Loading ThaiWater/HII stations…</p></div>
<div class="station-list" id="thaiwater-sensors" aria-live="polite" style="display:none"></div> <div class="station-list" id="thaiwater-sensors" aria-live="polite" style="display:none"></div>
</aside> </aside>
</section> </section>
@@ -279,13 +294,13 @@
<div style="display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap"> <div style="display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap">
<div><h2 id="history-title" style="margin:0;font-size:1rem">Station history</h2><p id="history-status" class="subtitle">Select a station to load the last 7 days</p></div> <div><h2 id="history-title" style="margin:0;font-size:1rem">Station history</h2><p id="history-status" class="subtitle">Select a station to load the last 7 days</p></div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"> <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<select id="history-range" style="padding:9px 12px;border:1px solid var(--border);border-radius:10px;background:white"><option value="24">Last 24 hours</option><option value="168" selected>Last 7 days</option><option value="720">Last 30 days</option><option value="2160">Last 90 days</option><option value="876000">All time</option></select> <select id="history-range" style="padding:9px 12px;border:1px solid var(--border);border-radius:10px;background:white"><option value="24">Last 24 hours</option><option value="168" selected>Last 7 days</option><option value="720">Last 30 days</option><option value="2160">Last 90 days</option><option value="876000">All time</option><option value="custom" hidden>Custom range</option></select>
<input type="date" id="history-start" title="From date" style="padding:8px 10px;border:1px solid var(--border);border-radius:10px;background:white"> <input type="date" id="history-start" title="From date" style="padding:8px 10px;border:1px solid var(--border);border-radius:10px;background:white">
<span style="color:var(--muted)"></span> <span style="color:var(--muted)"></span>
<input type="date" id="history-end" title="To date" style="padding:8px 10px;border:1px solid var(--border);border-radius:10px;background:white"> <input type="date" id="history-end" title="To date" style="padding:8px 10px;border:1px solid var(--border);border-radius:10px;background:white">
</div> </div>
</div> </div>
<div style="height:260px;margin-top:14px;overflow:hidden;position:relative"><canvas id="history-chart" aria-label="Historical water level and discharge chart" style="display:block"></canvas></div> <div style="height:260px;margin-top:14px;overflow:hidden;position:relative"><canvas id="history-chart" aria-label="Historical water level and discharge chart" style="display:block"></canvas><div id="history-placeholder" style="position:absolute;inset:0;display:grid;place-items:center;color:var(--muted);font-size:.85rem;text-align:center;padding:20px">Click any station on the map or in the list to see its history</div></div>
</section> </section>
<section class="stats" id="db-stats" aria-label="Database statistics" style="margin-top:14px;display:none;grid-template-columns:repeat(auto-fit,minmax(170px,1fr))"> <section class="stats" id="db-stats" aria-label="Database statistics" style="margin-top:14px;display:none;grid-template-columns:repeat(auto-fit,minmax(170px,1fr))">
@@ -517,6 +532,7 @@
function setLiveIndicator(mode, label) { function setLiveIndicator(mode, label) {
const pill = $('live-pill'); const pill = $('live-pill');
$('live-pill-text').textContent = label || 'LIVE DATA'; $('live-pill-text').textContent = label || 'LIVE DATA';
pill.title = label || 'LIVE DATA'; // mobile shows the dot only
pill.classList.toggle('demo', mode !== 'live'); pill.classList.toggle('demo', mode !== 'live');
} }
@@ -556,6 +572,7 @@
const reqId = state.historyRequestId; const reqId = state.historyRequestId;
$('history-title').textContent = `${stationCode} · station history`; $('history-title').textContent = `${stationCode} · station history`;
$('history-status').textContent = 'Loading historical measurements…'; $('history-status').textContent = 'Loading historical measurements…';
$('history-placeholder').style.display = 'none';
try { try {
// Hand-edited dates win; dropdown-driven loads use the precise // Hand-edited dates win; dropdown-driven loads use the precise
// trailing-hours window (the pickers just mirror it as dates). // trailing-hours window (the pickers just mirror it as dates).
@@ -651,6 +668,7 @@
state.sensorsOpen = open; state.sensorsOpen = open;
$('thaiwater-sensors').style.display = open ? '' : 'none'; $('thaiwater-sensors').style.display = open ? '' : 'none';
$('sensors-arrow').textContent = open ? '▾' : '▸'; $('sensors-arrow').textContent = open ? '▾' : '▸';
$('sensors-head').setAttribute('aria-expanded', String(open));
} }
// Filters both station lists by any text on the row (code, names, river — // Filters both station lists by any text on the row (code, names, river —
@@ -726,7 +744,7 @@
const row = document.createElement('button'); const row = document.createElement('button');
row.type = 'button'; row.className = 'station-row'; row.type = 'button'; row.className = 'station-row';
const codeClass = sensor.station_code.length > 5 ? 'station-code long' : 'station-code'; const codeClass = sensor.station_code.length > 5 ? 'station-code long' : 'station-code';
row.innerHTML = `<span class="${codeClass}" style="background:${color}">${escapeHtml(sensor.station_code)}</span><span class="station-name"><strong>${escapeHtml(sensor.station_name)}</strong><span>${escapeHtml(sensor.river_name || 'Ping basin')} · ThaiWater</span></span><span class="flow-value">${percent == null ? '—' : percent.toFixed(0) + '%'}<span>bank level</span></span>`; row.innerHTML = `<span class="${codeClass}" style="background:${color}">${escapeHtml(sensor.station_code)}</span><span class="station-name"><strong>${escapeHtml(sensor.station_name)}</strong><span>${escapeHtml(sensor.river_name || 'Ping basin')} · ThaiWater</span></span><span class="flow-value">${percent == null ? '—' : percent.toFixed(0) + '%'}<span>of bank height</span></span>`;
row.addEventListener('click', () => { state.map.flyTo(marker.getLatLng(), Math.max(state.map.getZoom(), 11), { duration: .8 }); marker.openPopup(); }); row.addEventListener('click', () => { state.map.flyTo(marker.getLatLng(), Math.max(state.map.getZoom(), 11), { duration: .8 }); marker.openPopup(); });
container.appendChild(row); container.appendChild(row);
}); });
@@ -745,9 +763,7 @@
const p1 = readings.get('P.1'); const p1 = readings.get('P.1');
const p1Flow = p1?.discharge == null ? null : Number(p1.discharge); const p1Flow = p1?.discharge == null ? null : Number(p1.discharge);
$('total-flow').textContent = p1Flow == null ? '—' : p1Flow.toLocaleString(undefined, { maximumFractionDigits: 1 }); $('total-flow').textContent = p1Flow == null ? '—' : p1Flow.toLocaleString(undefined, { maximumFractionDigits: 1 });
$('cm-flow-note').textContent = p1?.discharge_percent != null $('cm-flow-note').textContent = 'P.1 Nawarat Bridge · m³/s'; // capacity % lives in the next tile
? `P.1 Nawarat Bridge · ${Number(p1.discharge_percent).toFixed(0)}% of channel capacity · m³/s`
: 'P.1 Nawarat Bridge · m³/s';
const stressed = current const stressed = current
.filter((m) => m.discharge_percent != null) .filter((m) => m.discharge_percent != null)
.map((m) => ({ code: m.station_code, percent: Number(m.discharge_percent) })); .map((m) => ({ code: m.station_code, percent: Number(m.discharge_percent) }));
@@ -990,9 +1006,11 @@
} }
}); });
// stat tiles follow the replay (restored by loadDashboard on finish) // stat tiles follow the replay (restored by loadDashboard on finish)
$('total-flow').textContent = totalFlow.toLocaleString(undefined, { maximumFractionDigits: 1 }); const p1ReplayQ = data.stations['P.1'] ? data.stations['P.1'].discharge[frame] : null;
$('peak-flow').textContent = peakFlow ? peakFlow.value.toFixed(1) : '—'; $('total-flow').textContent = p1ReplayQ == null ? '—' : Number(p1ReplayQ).toLocaleString(undefined, { maximumFractionDigits: 1 });
$('peak-station').textContent = peakFlow ? `${peakFlow.code} · m³/s · 2024 replay` : 'No discharge reported'; $('cm-flow-note').textContent = 'P.1 Nawarat Bridge · m³/s · 2024 replay';
$('peak-flow').textContent = peakFlow ? peakFlow.value.toFixed(0) : '—';
$('peak-station').textContent = peakFlow ? `${peakFlow.code} · strongest flow · m³/s · 2024 replay` : 'No discharge reported';
restyleRiver(readings); restyleRiver(readings);
const p1Level = data.stations['P.1'] ? data.stations['P.1'].level[frame] : null; const p1Level = data.stations['P.1'] ? data.stations['P.1'].level[frame] : null;
if (p1Level != null) state.p1Now = Number(p1Level); if (p1Level != null) state.p1Now = Number(p1Level);
@@ -1209,6 +1227,7 @@
$('forecast-expand').addEventListener('click', () => { $('forecast-expand').addEventListener('click', () => {
state.forecastExpanded = !state.forecastExpanded; state.forecastExpanded = !state.forecastExpanded;
$('forecast-grid').style.display = state.forecastExpanded ? 'grid' : 'none'; $('forecast-grid').style.display = state.forecastExpanded ? 'grid' : 'none';
$('forecast-expand').setAttribute('aria-expanded', String(state.forecastExpanded));
const count = $('forecast-grid').children.length; const count = $('forecast-grid').children.length;
$('forecast-expand').textContent = state.forecastExpanded $('forecast-expand').textContent = state.forecastExpanded
? 'Hide station forecasts ▴' ? 'Hide station forecasts ▴'
@@ -1226,6 +1245,7 @@
}); });
$('replay-2024').addEventListener('click', replayFlood2024); $('replay-2024').addEventListener('click', replayFlood2024);
$('history-range').addEventListener('change', () => { $('history-range').addEventListener('change', () => {
if ($('history-range').value === 'custom') return;
state.useDates = false; state.useDates = false;
const hours = Number($('history-range').value); const hours = Number($('history-range').value);
if (hours >= 876000) { if (hours >= 876000) {
@@ -1238,13 +1258,21 @@
$('history-start').value = isoLocal(new Date(now.getTime() - hours * 3600 * 1000)); $('history-start').value = isoLocal(new Date(now.getTime() - hours * 3600 * 1000));
} }
if (state.selectedStation) loadHistory(state.selectedStation); if (state.selectedStation) loadHistory(state.selectedStation);
else $('history-status').textContent = 'Select a station first — click one on the map or in the list';
}); });
['history-start', 'history-end'].forEach((id) => $(id).addEventListener('change', () => { ['history-start', 'history-end'].forEach((id) => $(id).addEventListener('change', () => {
state.useDates = true; state.useDates = true;
$('history-range').value = 'custom'; // dropdown no longer describes the window
if (state.selectedStation) loadHistory(state.selectedStation); if (state.selectedStation) loadHistory(state.selectedStation);
else $('history-status').textContent = 'Select a station first — click one on the map or in the list';
})); }));
$('legend-toggle').addEventListener('click', () => {
const open = $('map-legend').classList.toggle('open');
$('legend-toggle').setAttribute('aria-expanded', String(open));
});
loadDashboard(); loadDashboard();
window.setInterval(loadDashboard, 5 * 60 * 1000); // Skip auto-refresh while the 2024 replay is running (it would clobber replay state)
window.setInterval(() => { if (!state.replayTimer) loadDashboard(); }, 5 * 60 * 1000);
})(); })();
</script> </script>
</body> </body>