feat: station search in side panel; DB stats cover HII tables
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 38s
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 16s
Documentation / Validate Documentation (push) Failing after 9s
Documentation / Generate API Documentation (push) Successful in 10s
Documentation / Build Sphinx Documentation (push) Successful in 16s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s
Documentation / Documentation Summary (push) Successful in 2s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 38s
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 16s
Documentation / Validate Documentation (push) Failing after 9s
Documentation / Generate API Documentation (push) Successful in 10s
Documentation / Build Sphinx Documentation (push) Successful in 16s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s
Documentation / Documentation Summary (push) Successful in 2s
A search box above the station lists filters both the RID flow list and the basin-station list by code, name (Thai included), or river, and persists across refreshes. /api/stats now reports whole-DB totals — RID + hii_rainfall + hii_waterlevel counts, combined station count, and a date range spanning all sources — with per-source breakdown fields the stats strip shows as tile notes. Sensor section renamed to 'Additional basin stations · ThaiWater/HII'.
This commit is contained in:
@@ -234,9 +234,11 @@
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<aside class="side-card">
|
<aside class="side-card">
|
||||||
<div class="side-head"><h2>Current station flow</h2><p>Select a station to locate it and load its history</p></div>
|
<div class="side-head"><h2>Current station flow</h2><p>Select a station to locate it and load its history</p>
|
||||||
|
<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 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"><h2>Additional ThaiWater sensors</h2><p id="thaiwater-count">Loading Ping basin sensors…</p></div>
|
<div class="side-head"><h2>Additional basin stations</h2><p id="thaiwater-count">Loading ThaiWater/HII stations…</p></div>
|
||||||
<div class="station-list" id="thaiwater-sensors" aria-live="polite"></div>
|
<div class="station-list" id="thaiwater-sensors" aria-live="polite"></div>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
@@ -273,11 +275,11 @@
|
|||||||
</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))">
|
||||||
<article class="stat"><div class="stat-label">Total datapoints</div><div class="stat-value" id="db-total">—</div><div class="stat-note">measurements stored</div></article>
|
<article class="stat"><div class="stat-label">Total datapoints</div><div class="stat-value" id="db-total">—</div><div class="stat-note" id="db-total-note">measurements stored</div></article>
|
||||||
<article class="stat"><div class="stat-label">Date range</div><div class="stat-value" id="db-range" style="font-size:1.05rem;line-height:1.3;white-space:normal">—</div><div class="stat-note">first to last record</div></article>
|
<article class="stat"><div class="stat-label">Date range</div><div class="stat-value" id="db-range" style="font-size:1.05rem;line-height:1.3;white-space:normal">—</div><div class="stat-note">first to last record</div></article>
|
||||||
<article class="stat"><div class="stat-label">Days spanned</div><div class="stat-value" id="db-days">—</div><div class="stat-note">between first and last</div></article>
|
<article class="stat"><div class="stat-label">Days spanned</div><div class="stat-value" id="db-days">—</div><div class="stat-note">between first and last</div></article>
|
||||||
<article class="stat"><div class="stat-label">Stations</div><div class="stat-value" id="db-stations">—</div><div class="stat-note">distinct in database</div></article>
|
<article class="stat"><div class="stat-label">Stations</div><div class="stat-value" id="db-stations">—</div><div class="stat-note" id="db-stations-note">distinct in database</div></article>
|
||||||
<article class="stat"><div class="stat-label">Coverage</div><div class="stat-value" id="db-coverage">—</div><div class="stat-note">of hourly slots recorded</div></article>
|
<article class="stat"><div class="stat-label">Coverage</div><div class="stat-value" id="db-coverage">—</div><div class="stat-note">of RID hourly slots recorded</div></article>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -623,6 +625,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filters both station lists by any text on the row (code, names, river —
|
||||||
|
// Thai included, since it matches against the rendered text).
|
||||||
|
function applyStationSearch() {
|
||||||
|
const query = $('station-search').value.trim().toLowerCase();
|
||||||
|
['river-flow', 'thaiwater-sensors'].forEach((id) => {
|
||||||
|
[...$(id).children].forEach((row) => {
|
||||||
|
row.style.display = !query || row.textContent.toLowerCase().includes(query) ? '' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderList(stations, readings) {
|
function renderList(stations, readings) {
|
||||||
const container = $('river-flow');
|
const container = $('river-flow');
|
||||||
container.replaceChildren();
|
container.replaceChildren();
|
||||||
@@ -654,7 +667,7 @@
|
|||||||
const additional = sensors
|
const additional = sensors
|
||||||
.filter((sensor) => !existingCodes.has(sensor.station_code))
|
.filter((sensor) => !existingCodes.has(sensor.station_code))
|
||||||
.sort((a, b) => (Number(b.bank_percent) || -1) - (Number(a.bank_percent) || -1));
|
.sort((a, b) => (Number(b.bank_percent) || -1) - (Number(a.bank_percent) || -1));
|
||||||
$('thaiwater-count').textContent = `${additional.length} additional Ping basin stations · water level`;
|
$('thaiwater-count').textContent = `${additional.length} Ping basin stations · ThaiWater/HII water level`;
|
||||||
additional.forEach((sensor) => {
|
additional.forEach((sensor) => {
|
||||||
const percent = sensor.bank_percent == null ? null : Number(sensor.bank_percent);
|
const percent = sensor.bank_percent == null ? null : Number(sensor.bank_percent);
|
||||||
// ThaiWater situation bands by % of bank capacity: over-bank red,
|
// ThaiWater situation bands by % of bank capacity: over-bank red,
|
||||||
@@ -751,6 +764,7 @@
|
|||||||
renderRainLayer(rainRecords);
|
renderRainLayer(rainRecords);
|
||||||
renderList(stations, readings);
|
renderList(stations, readings);
|
||||||
renderThaiWaterSensors(sensors, new Set(stations.map((station) => station.station_code)));
|
renderThaiWaterSensors(sensors, new Set(stations.map((station) => station.station_code)));
|
||||||
|
applyStationSearch(); // keep an active search applied across refreshes
|
||||||
loadCustomMarkers();
|
loadCustomMarkers();
|
||||||
renderSummary(stations, readings);
|
renderSummary(stations, readings);
|
||||||
$('loading').style.display = 'none';
|
$('loading').style.display = 'none';
|
||||||
@@ -1134,6 +1148,11 @@
|
|||||||
const stats = await response.json();
|
const stats = await response.json();
|
||||||
const fmtDate = (value) => new Date(value).toLocaleDateString([], { day: 'numeric', month: 'short', year: 'numeric' });
|
const fmtDate = (value) => new Date(value).toLocaleDateString([], { day: 'numeric', month: 'short', year: 'numeric' });
|
||||||
$('db-total').textContent = Number(stats.total_measurements).toLocaleString();
|
$('db-total').textContent = Number(stats.total_measurements).toLocaleString();
|
||||||
|
if (stats.rid_measurements != null) {
|
||||||
|
const fmtNum = (value) => Number(value || 0).toLocaleString();
|
||||||
|
$('db-total-note').textContent = `${fmtNum(stats.rid_measurements)} RID · ${fmtNum(stats.hii_waterlevel_measurements)} HII level · ${fmtNum(stats.hii_rainfall_measurements)} rain`;
|
||||||
|
$('db-stations-note').textContent = `${stats.rid_station_count} RID · ${stats.hii_station_count} ThaiWater/HII`;
|
||||||
|
}
|
||||||
$('db-range').textContent = `${fmtDate(stats.first_timestamp)} – ${fmtDate(stats.last_timestamp)}`;
|
$('db-range').textContent = `${fmtDate(stats.first_timestamp)} – ${fmtDate(stats.last_timestamp)}`;
|
||||||
$('db-days').textContent = Number(stats.days_spanned).toLocaleString();
|
$('db-days').textContent = Number(stats.days_spanned).toLocaleString();
|
||||||
$('db-stations').textContent = String(stats.station_count);
|
$('db-stations').textContent = String(stats.station_count);
|
||||||
@@ -1154,6 +1173,7 @@
|
|||||||
? 'Hide station forecasts ▴'
|
? 'Hide station forecasts ▴'
|
||||||
: `Show all ${count} station forecasts ▾`;
|
: `Show all ${count} station forecasts ▾`;
|
||||||
});
|
});
|
||||||
|
$('station-search').addEventListener('input', applyStationSearch);
|
||||||
$('rain-toggle').addEventListener('change', (event) => {
|
$('rain-toggle').addEventListener('change', (event) => {
|
||||||
if (!state.rainLayer) return;
|
if (!state.rainLayer) return;
|
||||||
if (event.target.checked) state.rainLayer.addTo(state.map);
|
if (event.target.checked) state.rainLayer.addTo(state.map);
|
||||||
|
|||||||
+52
-2
@@ -762,11 +762,61 @@ async def get_database_stats():
|
|||||||
if stats is None:
|
if stats is None:
|
||||||
raise HTTPException(status_code=503, detail="Database statistics unavailable")
|
raise HTTPException(status_code=503, detail="Database statistics unavailable")
|
||||||
|
|
||||||
|
def hii_totals():
|
||||||
|
engine = _hii_engine()
|
||||||
|
if engine is None:
|
||||||
|
return None
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
with engine.connect() as conn:
|
||||||
|
return conn.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT (SELECT COUNT(*) FROM hii_rainfall) AS rain_n,
|
||||||
|
(SELECT COUNT(*) FROM hii_waterlevel) AS wl_n,
|
||||||
|
(SELECT COUNT(*) FROM hii_rain_stations) AS rain_s,
|
||||||
|
(SELECT COUNT(*) FROM hii_wl_stations) AS wl_s,
|
||||||
|
(SELECT MIN(timestamp) FROM hii_rainfall) AS rain_lo,
|
||||||
|
(SELECT MAX(timestamp) FROM hii_rainfall) AS rain_hi,
|
||||||
|
(SELECT MIN(timestamp) FROM hii_waterlevel) AS wl_lo,
|
||||||
|
(SELECT MAX(timestamp) FROM hii_waterlevel) AS wl_hi
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
).one()
|
||||||
|
|
||||||
|
hii = None
|
||||||
|
try:
|
||||||
|
hii = await asyncio.to_thread(hii_totals)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"HII stats unavailable: {e}")
|
||||||
|
|
||||||
|
def as_dt(value):
|
||||||
|
if isinstance(value, str):
|
||||||
|
return datetime.fromisoformat(value)
|
||||||
|
return value
|
||||||
|
|
||||||
first_ts = stats["first_timestamp"]
|
first_ts = stats["first_timestamp"]
|
||||||
last_ts = stats["last_timestamp"]
|
last_ts = stats["last_timestamp"]
|
||||||
|
rain_n = wl_n = hii_stations = 0
|
||||||
|
if hii is not None:
|
||||||
|
rain_n, wl_n = hii.rain_n or 0, hii.wl_n or 0
|
||||||
|
hii_stations = (hii.rain_s or 0) + (hii.wl_s or 0)
|
||||||
|
for lo in (as_dt(hii.rain_lo), as_dt(hii.wl_lo)):
|
||||||
|
if lo is not None and lo < first_ts:
|
||||||
|
first_ts = lo
|
||||||
|
for hi in (as_dt(hii.rain_hi), as_dt(hii.wl_hi)):
|
||||||
|
if hi is not None and hi > last_ts:
|
||||||
|
last_ts = hi
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"total_measurements": stats["total_measurements"],
|
# Whole-DB totals (RID + HII feeds); breakdown fields alongside
|
||||||
"station_count": stats["station_count"],
|
"total_measurements": stats["total_measurements"] + rain_n + wl_n,
|
||||||
|
"rid_measurements": stats["total_measurements"],
|
||||||
|
"hii_rainfall_measurements": rain_n,
|
||||||
|
"hii_waterlevel_measurements": wl_n,
|
||||||
|
"station_count": stats["station_count"] + hii_stations,
|
||||||
|
"rid_station_count": stats["station_count"],
|
||||||
|
"hii_station_count": hii_stations,
|
||||||
"first_timestamp": first_ts.isoformat(),
|
"first_timestamp": first_ts.isoformat(),
|
||||||
"last_timestamp": last_ts.isoformat(),
|
"last_timestamp": last_ts.isoformat(),
|
||||||
"days_spanned": (last_ts.date() - first_ts.date()).days + 1,
|
"days_spanned": (last_ts.date() - first_ts.date()).days + 1,
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ def test_dashboard_loads_additional_thaiwater_sensors():
|
|||||||
html = DASHBOARD_PATH.read_text(encoding="utf-8")
|
html = DASHBOARD_PATH.read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "fetch('/sensors/thaiwater')" in html
|
assert "fetch('/sensors/thaiwater')" in html
|
||||||
assert "Additional ThaiWater sensor" in html
|
assert "Additional basin stations" in html
|
||||||
|
assert 'id="station-search"' in html
|
||||||
|
assert "applyStationSearch" in html
|
||||||
|
|
||||||
|
|
||||||
def test_dashboard_shows_hii_rainfall_layer():
|
def test_dashboard_shows_hii_rainfall_layer():
|
||||||
|
|||||||
Reference in New Issue
Block a user