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
+23 -7
View File
@@ -234,7 +234,7 @@
</article>
<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="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>
@@ -261,8 +261,13 @@
<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><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>
<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>
<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>
<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 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>
@@ -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);
})();
+14 -5
View File
@@ -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],
)