diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 72befea..f547ce4 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -234,9 +234,11 @@ @@ -273,11 +275,11 @@ - Total datapoints—measurements stored + Total datapoints—measurements stored Date range—first to last record Days spanned—between first and last - Stations—distinct in database - Coverage—of hourly slots recorded + Stations—distinct in database + Coverage—of RID hourly slots recorded @@ -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) { const container = $('river-flow'); container.replaceChildren(); @@ -654,7 +667,7 @@ const additional = sensors .filter((sensor) => !existingCodes.has(sensor.station_code)) .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) => { const percent = sensor.bank_percent == null ? null : Number(sensor.bank_percent); // ThaiWater situation bands by % of bank capacity: over-bank red, @@ -751,6 +764,7 @@ renderRainLayer(rainRecords); renderList(stations, readings); renderThaiWaterSensors(sensors, new Set(stations.map((station) => station.station_code))); + applyStationSearch(); // keep an active search applied across refreshes loadCustomMarkers(); renderSummary(stations, readings); $('loading').style.display = 'none'; @@ -1134,6 +1148,11 @@ const stats = await response.json(); const fmtDate = (value) => new Date(value).toLocaleDateString([], { day: 'numeric', month: 'short', year: 'numeric' }); $('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-days').textContent = Number(stats.days_spanned).toLocaleString(); $('db-stations').textContent = String(stats.station_count); @@ -1154,6 +1173,7 @@ ? 'Hide station forecasts ▴' : `Show all ${count} station forecasts ▾`; }); + $('station-search').addEventListener('input', applyStationSearch); $('rain-toggle').addEventListener('change', (event) => { if (!state.rainLayer) return; if (event.target.checked) state.rainLayer.addTo(state.map); diff --git a/src/web_api.py b/src/web_api.py index 153b215..0d32a0e 100644 --- a/src/web_api.py +++ b/src/web_api.py @@ -762,11 +762,61 @@ async def get_database_stats(): if stats is None: 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"] 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 = { - "total_measurements": stats["total_measurements"], - "station_count": stats["station_count"], + # Whole-DB totals (RID + HII feeds); breakdown fields alongside + "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(), "last_timestamp": last_ts.isoformat(), "days_spanned": (last_ts.date() - first_ts.date()).days + 1, diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 6d9dc74..7896729 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -35,7 +35,9 @@ def test_dashboard_loads_additional_thaiwater_sensors(): html = DASHBOARD_PATH.read_text(encoding="utf-8") 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():