-
PostgreSQL history
Select a RID flow station to load the last 7 days
-
+
Station history
Select a station to load the last 7 days
+
+
+
+ –
+
+
@@ -524,10 +529,15 @@
state.selectedStation = stationCode;
state.historyRequestId++;
const reqId = state.historyRequestId;
- $('history-title').textContent = `${stationCode} · PostgreSQL history`;
+ $('history-title').textContent = `${stationCode} · station history`;
$('history-status').textContent = 'Loading historical measurements…';
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}`);
const rows = await response.json();
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' });
} catch (error) {
$('history-status').textContent = `History unavailable: ${error.message}`;
@@ -1149,7 +1159,13 @@
else state.map.removeLayer(state.rainLayer);
});
$('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();
window.setInterval(loadDashboard, 5 * 60 * 1000);
})();
diff --git a/src/web_api.py b/src/web_api.py
index fc989ce..153b215 100644
--- a/src/web_api.py
+++ b/src/web_api.py
@@ -8,7 +8,7 @@ import os
import secrets
import time
from contextlib import asynccontextmanager
-from datetime import datetime, timedelta
+from datetime import date, datetime, timedelta
from decimal import Decimal
from threading import Lock
from typing import Any, Dict, List, Optional
@@ -609,9 +609,11 @@ async def get_postgres_history(
station_code: str,
hours: int = Query(168, ge=1),
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."""
- cache_key = f"{station_code}:{hours}:{limit}"
+ cache_key = f"{station_code}:{hours}:{limit}:{start}:{end}"
now = time.monotonic()
with HISTORY_CACHE_LOCK:
cached = HISTORY_CACHE.get(cache_key)
@@ -619,13 +621,20 @@ async def get_postgres_history(
return cached[1]
try:
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":
history = PostgresHistory(db_config["connection_string"])
data = await asyncio.to_thread(
history.station_history,
station_code,
- end_time - timedelta(hours=hours),
+ start_time,
end_time,
limit,
)
@@ -635,7 +644,7 @@ async def get_postgres_history(
raise RuntimeError("Database not available")
rows = await asyncio.to_thread(
scraper.db_adapter.get_measurements_by_timerange,
- end_time - timedelta(hours=hours),
+ start_time,
end_time,
[station_code],
)
diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py
index 99de918..9b5f1c4 100644
--- a/tests/test_dashboard.py
+++ b/tests/test_dashboard.py
@@ -51,9 +51,13 @@ def test_dashboard_shows_hii_rainfall_layer():
assert "Extreme > 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")
- assert "PostgreSQL history" in html
+ assert "Station history" in html
+ assert "PostgreSQL" not in html
assert "/measurements/history/" 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