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
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:
@@ -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>
|
||||||
<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 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>
|
||||||
<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
@@ -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],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,9 +51,13 @@ def test_dashboard_shows_hii_rainfall_layer():
|
|||||||
assert "Extreme > 150" in html
|
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")
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user