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
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:
+42
-14
@@ -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 & dash speed follow live discharge</span></div>
|
<div class="map-heading"><strong>Station flow map</strong><span>River width, colour & 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 < 25 m³/s</div>
|
<div class="legend-row"><i class="swatch" style="background:#1e8b60"></i> Low < 25 m³/s</div>
|
||||||
<div class="legend-row"><i class="swatch" style="background:#087da5"></i> Moderate 25–100</div>
|
<div class="legend-row"><i class="swatch" style="background:#087da5"></i> Moderate 25–100</div>
|
||||||
@@ -241,6 +255,7 @@
|
|||||||
<div class="legend-row"><i class="swatch" style="background:#d99018;border-radius:50%"></i> Very heavy 90–150</div>
|
<div class="legend-row"><i class="swatch" style="background:#d99018;border-radius:50%"></i> Very heavy 90–150</div>
|
||||||
<div class="legend-row"><i class="swatch" style="background:#cc4b37;border-radius:50%"></i> Extreme > 150</div>
|
<div class="legend-row"><i class="swatch" style="background:#cc4b37;border-radius:50%"></i> Extreme > 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user