feat: date-range picker on the station history panel; drop PostgreSQL naming
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 14s
Documentation / Validate Documentation (push) Failing after 8s
Documentation / Generate API Documentation (push) Successful in 10s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 24s
Documentation / Build Sphinx Documentation (push) Successful in 19s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s
Documentation / Documentation Summary (push) Successful in 2s

/measurements/history/{code} accepts optional start/end date params that
override the hours window (end date inclusive). The dashboard history
card gains from/to date inputs beside the quick-range dropdown —
explicit dates win, changing the dropdown clears them. All user-facing
'PostgreSQL history' labels renamed to 'Station history'.
This commit is contained in:
2026-08-11 15:52:40 +07:00
parent d29d49eac7
commit 9516857d44
3 changed files with 43 additions and 14 deletions
+22 -6
View File
@@ -234,7 +234,7 @@
</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 PostgreSQL history</p></div> <div class="side-head"><h2>Current station flow</h2><p>Select a station to locate it and load its history</p></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 ThaiWater sensors</h2><p id="thaiwater-count">Loading Ping basin sensors…</p></div>
<div class="station-list" id="thaiwater-sensors" aria-live="polite"></div> <div class="station-list" id="thaiwater-sensors" aria-live="polite"></div>
@@ -261,8 +261,13 @@
<section class="map-card" id="history-card" style="margin-top:14px;padding:20px"> <section class="map-card" id="history-card" style="margin-top:14px;padding:20px">
<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">PostgreSQL history</h2><p id="history-status" class="subtitle">Select a RID flow 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">
<select id="history-range" style="padding:9px 12px;border:1px solid var(--border);border-radius:10px;background:white"><option value="24">24 hours</option><option value="168" selected>7 days</option><option value="720">30 days</option><option value="2160">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">24 hours</option><option value="168" selected>7 days</option><option value="720">30 days</option><option value="2160">90 days</option><option value="876000">All time</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">
<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">
</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>
</section> </section>
@@ -524,10 +529,15 @@
state.selectedStation = stationCode; state.selectedStation = stationCode;
state.historyRequestId++; state.historyRequestId++;
const reqId = state.historyRequestId; const reqId = state.historyRequestId;
$('history-title').textContent = `${stationCode} · PostgreSQL history`; $('history-title').textContent = `${stationCode} · station history`;
$('history-status').textContent = 'Loading historical measurements…'; $('history-status').textContent = 'Loading historical measurements…';
try { try {
const response = await fetch(`/measurements/history/${encodeURIComponent(stationCode)}?hours=${$('history-range').value}`); // Explicit dates win over the quick-range dropdown
const startDate = $('history-start').value, endDate = $('history-end').value;
const query = startDate || endDate
? [startDate && `start=${startDate}`, endDate && `end=${endDate}`].filter(Boolean).join('&')
: `hours=${$('history-range').value}`;
const response = await fetch(`/measurements/history/${encodeURIComponent(stationCode)}?${query}`);
if (!response.ok) throw new Error((await response.json()).detail || `HTTP ${response.status}`); if (!response.ok) throw new Error((await response.json()).detail || `HTTP ${response.status}`);
const rows = await response.json(); const rows = await response.json();
if (reqId !== state.historyRequestId) return; if (reqId !== state.historyRequestId) return;
@@ -605,7 +615,7 @@
} }
}] }]
}); });
$('history-status').textContent = rows.length ? `${rows.length} measurements (${sampled.length} daily) from PostgreSQL` : 'No PostgreSQL measurements in this period'; $('history-status').textContent = rows.length ? `${rows.length} measurements${sampled.length !== rows.length ? ` (${sampled.length} daily averages)` : ''}` : 'No measurements in this period';
$('history-card').scrollIntoView({ behavior: 'smooth', block: 'nearest' }); $('history-card').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (error) { } catch (error) {
$('history-status').textContent = `History unavailable: ${error.message}`; $('history-status').textContent = `History unavailable: ${error.message}`;
@@ -1149,7 +1159,13 @@
else state.map.removeLayer(state.rainLayer); else state.map.removeLayer(state.rainLayer);
}); });
$('replay-2024').addEventListener('click', replayFlood2024); $('replay-2024').addEventListener('click', replayFlood2024);
$('history-range').addEventListener('change', () => { if (state.selectedStation) loadHistory(state.selectedStation); }); $('history-range').addEventListener('change', () => {
$('history-start').value = ''; $('history-end').value = '';
if (state.selectedStation) loadHistory(state.selectedStation);
});
['history-start', 'history-end'].forEach((id) => $(id).addEventListener('change', () => {
if (state.selectedStation) loadHistory(state.selectedStation);
}));
loadDashboard(); loadDashboard();
window.setInterval(loadDashboard, 5 * 60 * 1000); window.setInterval(loadDashboard, 5 * 60 * 1000);
})(); })();
+14 -5
View File
@@ -8,7 +8,7 @@ import os
import secrets import secrets
import time import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import datetime, timedelta from datetime import date, datetime, timedelta
from decimal import Decimal from decimal import Decimal
from threading import Lock from threading import Lock
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
@@ -609,9 +609,11 @@ async def get_postgres_history(
station_code: str, station_code: str,
hours: int = Query(168, ge=1), hours: int = Query(168, ge=1),
limit: int = Query(50000, ge=1, le=100000), limit: int = Query(50000, ge=1, le=100000),
start: Optional[date] = Query(None, description="First day (overrides hours)"),
end: Optional[date] = Query(None, description="Last day, inclusive"),
): ):
"""Get historical measurements for a station from the configured database.""" """Get historical measurements for a station from the configured database."""
cache_key = f"{station_code}:{hours}:{limit}" cache_key = f"{station_code}:{hours}:{limit}:{start}:{end}"
now = time.monotonic() now = time.monotonic()
with HISTORY_CACHE_LOCK: with HISTORY_CACHE_LOCK:
cached = HISTORY_CACHE.get(cache_key) cached = HISTORY_CACHE.get(cache_key)
@@ -619,13 +621,20 @@ async def get_postgres_history(
return cached[1] return cached[1]
try: try:
db_config = Config.get_database_config() db_config = Config.get_database_config()
end_time = datetime.now() end_time = (
datetime.combine(end, datetime.max.time()) if end else datetime.now()
)
start_time = (
datetime.combine(start, datetime.min.time())
if start
else end_time - timedelta(hours=hours)
)
if db_config["type"] == "postgresql": if db_config["type"] == "postgresql":
history = PostgresHistory(db_config["connection_string"]) history = PostgresHistory(db_config["connection_string"])
data = await asyncio.to_thread( data = await asyncio.to_thread(
history.station_history, history.station_history,
station_code, station_code,
end_time - timedelta(hours=hours), start_time,
end_time, end_time,
limit, limit,
) )
@@ -635,7 +644,7 @@ async def get_postgres_history(
raise RuntimeError("Database not available") raise RuntimeError("Database not available")
rows = await asyncio.to_thread( rows = await asyncio.to_thread(
scraper.db_adapter.get_measurements_by_timerange, scraper.db_adapter.get_measurements_by_timerange,
end_time - timedelta(hours=hours), start_time,
end_time, end_time,
[station_code], [station_code],
) )
+6 -2
View File
@@ -51,9 +51,13 @@ def test_dashboard_shows_hii_rainfall_layer():
assert "Extreme &gt; 150" in html assert "Extreme &gt; 150" in html
def test_dashboard_loads_postgresql_history_chart(): def test_dashboard_loads_station_history_chart():
html = DASHBOARD_PATH.read_text(encoding="utf-8") html = DASHBOARD_PATH.read_text(encoding="utf-8")
assert "PostgreSQL history" in html assert "Station history" in html
assert "PostgreSQL" not in html
assert "/measurements/history/" in html assert "/measurements/history/" in html
assert "history-chart" in html assert "history-chart" in html
# Date-range picker alongside the quick-range dropdown
assert 'id="history-start"' in html
assert 'id="history-end"' in html