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>
|
||||
|
||||
<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="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>
|
||||
</aside>
|
||||
</section>
|
||||
@@ -273,11 +275,11 @@
|
||||
</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))">
|
||||
<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">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">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">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 RID hourly slots recorded</div></article>
|
||||
</section>
|
||||
</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) {
|
||||
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);
|
||||
|
||||
+52
-2
@@ -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,
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user