fix: restore station telemetry and make river flow visualization data-driven

Station selection showed no history since 21ca844: Chart.js v4 datasets
had parsing:false with plain number arrays, drawing empty axes. Remove
the flag so the chart parses values again.

Backend hardening for the same flow:
- /measurements/history/{code} no longer 503s on non-Postgres configs;
  it falls back to the configured adapter (reversed to ascending order)
- DB_TYPE defaults to postgresql when POSTGRES_CONNECTION_STRING is set
  and DB_TYPE is unset, so the .env psql wins over the sqlite default
- zero readings (0.0) are no longer coerced to None, which would fail
  MeasurementResponse validation and 500 /measurements/latest

Map visualization:
- river segments are now colored, widened and dash-speed-animated by
  the discharge at the nearest gauge (same scale as the marker legend)
- fix z-order bug that drew the animated flow line behind its casing
- legend entries for river lines, reduced-motion fallback

River geometry: rebuild ping-river-network.geojson from Overpass
(110 -> 202 features), restoring missing Ping mainstem reaches through
the Bhumibol reservoir and the Tak-Kamphaeng Phet braided section
(unnamed waterway=river ways in OSM), with short synthetic connectors
(connector: true) bridging remaining sub-8 km holes.
This commit is contained in:
grabowski committed 2026-08-10 10:30:05 +07:00
1 parent af1909db73
commit 49a3de0087
5 files changed
+99 -34

No files matched your search

+5 -1
View File
@@ -45,7 +45,11 @@ class Config:
) )
# Database configuration # Database configuration
DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower() # When DB_TYPE is not set explicitly, a configured Postgres connection wins over the sqlite default
DB_TYPE = os.getenv(
"DB_TYPE",
"postgresql" if os.getenv("POSTGRES_CONNECTION_STRING") else "sqlite",
).lower()
# VictoriaMetrics settings # VictoriaMetrics settings
# Default to localhost; set VM_HOST in the environment for real deployments # Default to localhost; set VM_HOST in the environment for real deployments
+9 -9
View File
@@ -519,9 +519,9 @@ class SQLAdapter(DatabaseAdapter):
"station_code": row[1], "station_code": row[1],
"station_name_en": row[2], "station_name_en": row[2],
"station_name_th": row[3], "station_name_th": row[3],
"water_level": float(row[4]) if row[4] else None, "water_level": float(row[4]) if row[4] is not None else None,
"discharge": float(row[5]) if row[5] else None, "discharge": float(row[5]) if row[5] is not None else None,
"discharge_percent": float(row[6]) if row[6] else None, "discharge_percent": float(row[6]) if row[6] is not None else None,
"status": row[7], "status": row[7],
} }
) )
@@ -573,9 +573,9 @@ class SQLAdapter(DatabaseAdapter):
"station_code": row[1], "station_code": row[1],
"station_name_en": row[2], "station_name_en": row[2],
"station_name_th": row[3], "station_name_th": row[3],
"water_level": float(row[4]) if row[4] else None, "water_level": float(row[4]) if row[4] is not None else None,
"discharge": float(row[5]) if row[5] else None, "discharge": float(row[5]) if row[5] is not None else None,
"discharge_percent": float(row[6]) if row[6] else None, "discharge_percent": float(row[6]) if row[6] is not None else None,
"status": row[7], "status": row[7],
} }
) )
@@ -618,9 +618,9 @@ class SQLAdapter(DatabaseAdapter):
"station_id": row[1], "station_id": row[1],
"station_code": row[2] or f"Station_{row[1]}", "station_code": row[2] or f"Station_{row[1]}",
"station_name_th": row[3] or f"Station {row[1]}", "station_name_th": row[3] or f"Station {row[1]}",
"water_level": float(row[4]) if row[4] else None, "water_level": float(row[4]) if row[4] is not None else None,
"discharge": float(row[5]) if row[5] else None, "discharge": float(row[5]) if row[5] is not None else None,
"discharge_percent": float(row[6]) if row[6] else None, "discharge_percent": float(row[6]) if row[6] is not None else None,
"status": row[7], "status": row[7],
} }
) )
+60 -10
View File
@@ -114,8 +114,15 @@
opacity: .36; animation: pulse 2.2s ease-out infinite; opacity: .36; animation: pulse 2.2s ease-out infinite;
} }
@keyframes pulse { 0% { transform: scale(.72); opacity: .55; } 75%,100% { transform: scale(1.35); opacity: 0; } } @keyframes pulse { 0% { transform: scale(.72); opacity: .55; } 75%,100% { transform: scale(1.35); opacity: 0; } }
.flow-line { animation: riverMove 1.8s linear infinite; } .flow-line { animation: riverMove 3s linear infinite; }
@keyframes riverMove { to { stroke-dashoffset: -30; } } .flow-idle { animation-duration: 5.5s; }
.flow-slow { animation-duration: 3s; }
.flow-med { animation-duration: 1.9s; }
.flow-fast { animation-duration: 1.15s; }
.flow-surge { animation-duration: .7s; }
@keyframes riverMove { to { stroke-dashoffset: -40; } }
@media (prefers-reduced-motion: reduce) { .flow-line, .flow-marker::before { animation: none; } }
.line-swatch { width: 24px; height: 4px; border-radius: 2px; flex: none; }
.leaflet-popup-content-wrapper { border-radius: 14px; box-shadow: 0 12px 35px rgba(14,45,54,.2); } .leaflet-popup-content-wrapper { border-radius: 14px; box-shadow: 0 12px 35px rgba(14,45,54,.2); }
.popup { min-width: 190px; } .popup { min-width: 190px; }
.popup-code { font-size: .7rem; color: var(--river); font-weight: 850; text-transform: uppercase; letter-spacing: .08em; } .popup-code { font-size: .7rem; color: var(--river); font-weight: 850; text-transform: uppercase; letter-spacing: .08em; }
@@ -171,13 +178,15 @@
<article class="map-card"> <article class="map-card">
<div id="station-map" role="application" aria-label="Interactive map of Ping River monitoring stations"></div> <div id="station-map" role="application" aria-label="Interactive map of Ping River monitoring stations"></div>
<div class="map-overlay"> <div class="map-overlay">
<div class="map-heading"><strong>Station flow map</strong><span>Marker size follows current discharge</span></div> <div class="map-heading"><strong>Station flow map</strong><span>River width, colour &amp; dash speed follow live discharge</span></div>
<div class="legend"> <div class="legend">
<div class="legend-title">Flow status</div> <div class="legend-title">Flow status</div>
<div class="legend-row"><i class="swatch" style="background:#1e8b60"></i> Low &lt; 25 m³/s</div> <div class="legend-row"><i class="swatch" style="background:#1e8b60"></i> Low &lt; 25 m³/s</div>
<div class="legend-row"><i class="swatch" style="background:#087da5"></i> Moderate 25–100</div> <div class="legend-row"><i class="swatch" style="background:#087da5"></i> Moderate 25–100</div>
<div class="legend-row"><i class="swatch" style="background:#d99018"></i> High 100–250</div> <div class="legend-row"><i class="swatch" style="background:#d99018"></i> High 100–250</div>
<div class="legend-row"><i class="swatch" style="background:#cc4b37"></i> Very high &gt; 250</div> <div class="legend-row"><i class="swatch" style="background:#cc4b37"></i> Very high &gt; 250</div>
<div class="legend-row"><i class="line-swatch" style="background:#69b7d0"></i> River · no nearby gauge</div>
<div class="legend-row"><i class="line-swatch" style="background:linear-gradient(90deg,#1e8b60,#087da5,#d99018,#cc4b37)"></i> River · gauge colour, dashes = flow</div>
</div> </div>
</div> </div>
<div class="loading-panel" id="loading"><div class="loading-card">Loading river conditions…</div></div> <div class="loading-panel" id="loading"><div class="loading-card">Loading river conditions…</div></div>
@@ -273,22 +282,63 @@
</div>`; </div>`;
} }
function renderRiverNetwork(riverNetwork) { function nearestGaugeFlow(feature, gauges) {
const coords = feature.geometry && feature.geometry.coordinates;
if (!gauges.length || !coords || !coords.length) return null;
let best = Infinity, q = null;
const step = Math.max(1, Math.floor(coords.length / 5));
for (let i = 0; i < coords.length; i += step) {
const lon = coords[i][0], lat = coords[i][1];
gauges.forEach((g) => {
const d = (g.lat - lat) * (g.lat - lat) + (g.lon - lon) * (g.lon - lon);
if (d < best) { best = d; q = g.q; }
});
}
return best < 0.16 ? q : null; // only grade segments within ~0.4° (~45 km) of a gauge
}
function riverWeight(q) {
return q == null ? 2.5 : Math.max(3, Math.min(9, 2.5 + Math.sqrt(Math.max(0, q)) * .38));
}
function riverSpeedClass(q) {
if (q == null) return 'flow-idle';
if (q < 25) return 'flow-slow';
if (q < 100) return 'flow-med';
if (q < 250) return 'flow-fast';
return 'flow-surge';
}
function renderRiverNetwork(riverNetwork, stations, readings) {
if (!riverNetwork) return; if (!riverNetwork) return;
const gauges = stations
.filter((s) => Number.isFinite(s.latitude) && Number.isFinite(s.longitude))
.map((s) => ({ lat: s.latitude, lon: s.longitude, q: readings.get(s.station_code)?.discharge }))
.filter((g) => g.q != null)
.map((g) => ({ lat: g.lat, lon: g.lon, q: Number(g.q) }));
const flowBySegment = new Map();
(riverNetwork.features || []).forEach((f) => flowBySegment.set(f, nearestGaugeFlow(f, gauges)));
const casing = L.geoJSON(riverNetwork, { const casing = L.geoJSON(riverNetwork, {
style: { color: '#d7f3f5', weight: 6, opacity: .72, lineCap: 'round' } style: (f) => ({ color: '#e3f4f8', weight: riverWeight(flowBySegment.get(f)) + 4.5, opacity: .8, lineCap: 'round' })
}).addTo(state.map); }).addTo(state.map);
const flow = L.geoJSON(riverNetwork, { const flow = L.geoJSON(riverNetwork, {
style: { color: '#087da5', weight: 2.5, opacity: .88, dashArray: '5 12', className: 'flow-line' } style: (f) => {
const q = flowBySegment.get(f);
return {
color: q == null ? '#69b7d0' : flowColor(q),
weight: riverWeight(q), opacity: .92, lineCap: 'round',
dashArray: '6 14', className: `flow-line ${riverSpeedClass(q)}`
};
}
}).addTo(state.map); }).addTo(state.map);
casing.bringToBack();
flow.bringToBack(); flow.bringToBack();
casing.bringToBack();
state.layers.push(casing, flow); state.layers.push(casing, flow);
} }
function renderMap(stations, readings, riverNetwork) { function renderMap(stations, readings, riverNetwork) {
clearLayers(); clearLayers();
renderRiverNetwork(riverNetwork); renderRiverNetwork(riverNetwork, stations, readings);
const mapped = stations.filter((station) => Number.isFinite(station.latitude) && Number.isFinite(station.longitude)); const mapped = stations.filter((station) => Number.isFinite(station.latitude) && Number.isFinite(station.longitude));
const bounds = []; const bounds = [];
@@ -358,8 +408,8 @@
data: { data: {
labels: sampled.map((row) => new Date(row.timestamp).toLocaleString('en-TH', { timeZone: 'Asia/Bangkok', month: 'short', day: 'numeric', year: '2-digit', hour: '2-digit' })), labels: sampled.map((row) => new Date(row.timestamp).toLocaleString('en-TH', { timeZone: 'Asia/Bangkok', month: 'short', day: 'numeric', year: '2-digit', hour: '2-digit' })),
datasets: [ datasets: [
{ label: 'Discharge (m³/s)', data: sampled.map((row) => row.discharge), borderColor: '#087da5', backgroundColor: 'rgba(8,125,165,.12)', yAxisID: 'flow', pointRadius: 0, tension: .25, parsing: false }, { label: 'Discharge (m³/s)', data: sampled.map((row) => row.discharge), borderColor: '#087da5', backgroundColor: 'rgba(8,125,165,.12)', yAxisID: 'flow', pointRadius: 0, tension: .25 },
{ label: 'Water level (m)', data: sampled.map((row) => row.water_level), borderColor: '#d99018', yAxisID: 'level', pointRadius: 0, tension: .25, parsing: false } { label: 'Water level (m)', data: sampled.map((row) => row.water_level), borderColor: '#d99018', yAxisID: 'level', pointRadius: 0, tension: .25 }
] ]
}, },
options: { options: {
+1 -1
View File
@@ -1 +1 @@
{"source":"© OpenStreetMap contributors, extracted via Overpass API","type":"FeatureCollection","features":[{"type":"Feature","properties":{"osm_way_id":27892685,"name":"แม่น้ำกวง"},"geometry":{"type":"LineString","coordinates":[[99.058856,18.7463891],[99.0592224,18.7462794],[99.0595327,18.746237],[99.0599007,18.7462357],[99.0603232,18.746174],[99.0605782,18.7457661],[99.0605863,18.7453452],[99.0607284,18.7448035],[99.0610004,18.7445606],[99.0613466,18.7445519],[99.0615034,18.7445365],[99.0617451,18.744583],[99.061853,18.7445525],[99.0621018,18.7443802],[99.0622599,18.7442339],[99.0623643,18.7440253],[99.0623788,18.7437408],[99.0623505,18.7434416],[99.0623995,18.742592],[99.0623872,18.7422416],[99.0623346,18.7420611],[99.0623547,18.7418469],[99.0624193,18.7415371],[99.0625664,18.7412417],[99.0626902,18.7411198],[99.0628174,18.7409945],[99.0630894,18.7407091],[99.0633877,18.7403559],[99.0636527,18.7398887],[99.0638132,18.7396067],[99.0639918,18.739364],[99.064167,18.7391451],[99.0645228,18.7388548],[99.064912,18.7385523],[99.0652821,18.7382767],[99.0655498,18.7379856],[99.0656802,18.7376996],[99.0658444,18.7375187],[99.0661258,18.7373028],[99.0664054,18.7371264],[99.0667023,18.7369249],[99.0669773,18.7366454],[99.0670994,18.7363908],[99.0671231,18.73622],[99.0671653,18.7360193],[99.067352,18.735891],[99.0676194,18.7357968],[99.068016,18.7357296],[99.0683016,18.7356515],[99.0684483,18.7355734],[99.0686935,18.7353291],[99.0688598,18.7352275],[99.069179,18.7351132],[99.0693849,18.7350217],[99.0694761,18.7349931],[99.0695364,18.7349658],[99.0695746,18.7349322],[99.0696082,18.7348934],[99.0696532,18.7347879],[99.0696407,18.7346281],[99.0695941,18.7343594],[99.0695686,18.7342527],[99.0695639,18.7342051],[99.0695693,18.7341358],[99.0695773,18.734092],[99.0695994,18.7340412],[99.0696645,18.7339701],[99.0697101,18.733918],[99.0698764,18.7338025],[99.0701339,18.7336475],[99.0707166,18.7333535],[99.0710358,18.7331541],[99.0712463,18.7329884],[99.0713875,18.732843],[99.0714572,18.7327943],[99.0714823,18.7327686],[99.0714914,18.7327427]]}},{"type":"Feature","properties":{"osm_way_id":37704725,"name":"แม่น้ำปิง"},"geometry":{"type":"LineString","coordinates":[[99.0359977,19.1642427],[99.0357903,19.1640038],[99.0353649,19.1628347],[99.0352293,19.1618359]]}},{"type":"Feature","properties":{"osm_way_id":40138033,"name":"แม่น้ำปิง"},"geometry":{"type":"LineString","coordinates":[[99.5354989,16.3921684],[99.5399935,16.389432],[99.5462645,16.3874843],[99.5502225,16.3870135],[99.5530083,16.3863461],[99.5590443,16.3818857],[99.5643527,16.3787697],[99.5650653,16.3783514],[99.5679379,16.3771753],[99.5705741,16.3762348],[99.5747861,16.3745831],[99.5766036,16.3734069],[99.5849391,16.3703591],[99.5877482,16.3666686],[99.5893928,16.3631314],[99.590806,16.3618265],[99.5908699,16.3618221],[99.5987686,16.361273],[99.6017158,16.3611279],[99.605748,16.3609048],[99.6157311,16.3621903],[99.6193721,16.3612336],[99.6222042,16.3599634],[99.6251774,16.3600578],[99.6296533,16.3613798],[99.632515,16.3617973],[99.6363114,16.3627646],[99.6441243,16.3628027],[99.6463098,16.3615849],[99.6502859,16.3578202],[99.6539777,16.3533513],[99.6530556,16.3493944],[99.6564095,16.344855],[99.6575896,16.3416862],[99.6599291,16.3396002],[99.661826,16.3353654],[99.6686258,16.3284697],[99.6691773,16.3271019],[99.6691256,16.3219636],[99.6685847,16.3177322],[99.6726339,16.3128941],[99.6736094,16.3112529],[99.6737528,16.3110117],[99.6762178,16.3093334],[99.6788061,16.3057461],[99.681069,16.3031829],[99.6859211,16.2976868],[99.6883994,16.2961686],[99.6887632,16.2957905],[99.6895131,16.295011],[99.6904491,16.2932891],[99.691254,16.2909791],[99.6927381,16.2881953],[99.6911636,16.2827265],[99.6910928,16.2824807],[99.6919803,16.2789988],[99.6912793,16.2770848],[99.6915816,16.2744262],[99.6913649,16.2711617],[99.6913171,16.2696569],[99.692397,16.2673747],[99.694034,16.2637748],[99.6936588,16.2624955],[99.6937203,16.261564],[99.6949202,16.2603987],[99.6999526,16.2601019],[99.703579,16.2591545],[99.705817,16.2574406],[99.7079071,16.2548276],[99.7081885,16.2538278],[99.7080669,16.2527861],[99.7072764,16.2507401],[99.7054029,16.2473714],[99.7045966,16.2448588],[99.7046907,16.2434842],[99.7051,16.2417274],[99.7078834,16.2398319],[99.7099579,16.2390744],[99.7116387,16.2389099],[99.7140755,16.2397266],[99.7159696,16.2405002],[99.7164631,16.2407371],[99.717375,16.2409019],[99.7173954,16.2408351],[99.71789,16.240871],[99.7185981,16.240665],[99.7195315,16.2403354],[99.720068,16.2398821],[99.7207332,16.2393774],[99.7216022,16.238883],[99.7228038,16.23844],[99.7236514,16.2380486],[99.7248638,16.2373378],[99.7258186,16.2366889],[99.7270203,16.2355352],[99.7282755,16.2342888],[99.72893,16.2334132],[99.7293484,16.2324655],[99.7296059,16.2317547],[99.7295415,16.2306113],[99.7294342,16.2299932],[99.7292948,16.229509],[99.7288473,16.2288783],[99.7276376,16.2266564],[99.7273289,16.2249035],[99.7223047,16.2197046],[99.7215284,16.2149804],[99Line truncated {"type": "FeatureCollection", "source": "© OpenStreetMap contributors, extracted via Overpass API", "features": [{"type": "Feature", "properties": {"osm_way_id": 23719502, "name": "ลำน้ำแม่แจ่ม"}, "geometry": {"type": "LineString", "coordinates": [[98.3555662, 18.6112755], [98.3552759, 18.6104848], [98.3553011, 18.6098122], [98.3561304, 18.608622], [98.3569689, 18.6077354], [98.3571529, 18.6072392], [98.3571287, 18.606834], [98.3565488, 18.6064867], [98.3559008, 18.6067628], [98.3541828, 18.6068178], [98.3529348, 18.6059799], [98.3514628, 18.605261], [98.3516206, 18.6042381], [98.3545581, 18.6005642], [98.3550389, 18.5994685], [98.3552576, 18.5986871], [98.3548843, 18.597715], [98.3543156, 18.5969605], [98.35307, 18.596746], [98.3524027, 18.5962233], [98.3517568, 18.5954586], [98.3516495, 18.5948231], [98.3517311, 18.593978], [98.352186, 18.5930588], [98.3532867, 18.5920297], [98.3535485, 18.591449], [98.353467, 18.5898027], [98.3536397, 18.5889536], [98.3545002, 18.5872187], [98.3536633, 18.5860218], [98.3538801, 18.584296], [98.3523834, 18.5822469], [98.3512976, 18.5797736], [98.3513985, 18.5778333], [98.353718, 18.5770176], [98.3543944, 18.5764523], [98.3546966, 18.5743038], [98.3518864, 18.5711017], [98.3522399, 18.5699452], [98.3530066, 18.5690643], [98.3533856, 18.5685511], [98.3532291, 18.568009], [98.3528753, 18.5677942], [98.3517334, 18.5671012], [98.3510701, 18.5663569], [98.3514931, 18.565337], [98.3574165, 18.5633029], [98.3591391, 18.5615267], [98.3607079, 18.5609153], [98.3619752, 18.5609559], [98.362366, 18.5605687], [98.3622054, 18.5598666], [98.3616652, 18.5592483], [98.3616644, 18.5575022], [98.3609353, 18.5568383], [98.3596782, 18.5562514], [98.3586412, 18.5562755], [98.357434, 18.5565944], [98.3568141, 18.5564034], [98.3564319, 18.5560297], [98.3563525, 18.5553472], [98.3568189, 18.553094], [98.356475, 18.5519903], [98.3553614, 18.5502168], [98.3549524, 18.5485156], [98.3545782, 18.5474855], [98.3541214, 18.5463273], [98.3535899, 18.5455804], [98.3534997, 18.5453222], [98.3534446, 18.5448484], [98.3531642, 18.5436279], [98.3527501, 18.542146], [98.3525261, 18.5411586], [98.3522729, 18.5396056], [98.3519199, 18.5384196], [98.352264, 18.5373156], [98.3525061, 18.5364215], [98.3525818, 18.5359129], [98.3525324, 18.5354646], [98.3520795, 18.5347088], [98.3520748, 18.5343076], [98.3521033, 18.5339959], [98.3524461, 18.5335557], [98.3529705, 18.5315115], [98.3530488, 18.5306235], [98.3527874, 18.5298745], [98.3527123, 18.5296201], [98.3527111, 18.5293773], [98.3528841, 18.528958], [98.3532913, 18.5283985], [98.3538428, 18.5278438], [98.3555218, 18.5267116], [98.3557012, 18.5262424], [98.3555511, 18.5254178], [98.354365, 18.5242174], [98.3542698, 18.5238163], [98.3543913, 18.5231335], [98.3550542, 18.5224033], [98.3553468, 18.5219272], [98.3554075, 18.5216016], [98.3553748, 18.5212606], [98.3543992, 18.5191156], [98.354365, 18.518723], [98.3543918, 18.51786], [98.3544386, 18.5173388], [98.3545423, 18.5168485], [98.3548651, 18.5163456], [98.3556594, 18.5155188], [98.3558013, 18.5152318], [98.3558782, 18.514937], [98.3559005, 18.5133778], [98.3559737, 18.5130721], [98.3561527, 18.5128126], [98.3570491, 18.5120505], [98.3572101, 18.5113719], [98.3572411, 18.5105314], [98.357012, 18.5095622], [98.3565103, 18.508784], [98.3558379, 18.5077238], [98.3557269, 18.5076185], [98.3542813, 18.5062469], [98.3542293, 18.5058227], [98.3543226, 18.5048973], [98.3545212, 18.5040981], [98.3552632, 18.5031322], [98.3561262, 18.5023584], [98.3569974, 18.5018251], [98.3574627, 18.5015764], [98.3577904, 18.5012908], [98.3583273, 18.5006186], [98.3586843, 18.5003156], [98.3589735, 18.5001823], [98.3604648, 18.4997811], [98.3615734, 18.4988634], [98.3620069, 18.4983062], [98.3621498, 18.4977408], [98.3620476, 18.497137], [98.3611633, 18.4954223], [98.3610295, 18.4950939], [98.3611477, 18.4945872], [98.3614793, 18.4943066], [98.3619194, 18.4937673], [98.3622668, 18.493354], [98.3625739, 18.4932416], [98.3634937, 18.4929341], [98.3646837, 18.4921895], [98.3652295, 18.4920899], [98.3656446, 18.4920552], [98.3659841, 18.4921147], [98.3665159, 18.4925158], [98.3684703, 18.4947842], [98.3686883, 18.4947936], [98.3697053, 18.4941509], [98.370411, 18.493647], [98.3712154, 18.4935023], [98.3726099, 18.4931029], [98.373197, 18.4925674], [98.3734254, 18.4919313], [98.3733394, 18.4911234], [98.3729784, 18.4896853], [98.3727503, 18.4892024], [98.3724886, 18.4890157], [98.3722926, 18.4888758], [98.3710768, 18.4885274], [98.3705526, 18.487926], [98.3705534, 18.4873871], [98.3714588, 18.4864993], [98.3726988, 18.4859625], [98.3734889, 18.4854163], [98.373641, 18.4852057], [98.3737071, 18.4848921], [98.3734164, 18.4846083], [98.3723289, 18.4841743], [98.371699, 18.4836671], [98.3713734, 18.482987], [98.3710596, 18.4817848], [98.3710329, 18.4804998], [98.3710283, 18.4797777], [98.3706495, 18.4790623], [98.3704121, 18.4781317], [98.37047, 18.4775372], [98.3703624, 18.4769995], [98.3698864, 18.4766533], [98.36927Line truncated
+24 -13
View File
@@ -454,7 +454,7 @@ async def get_postgres_history(
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),
): ):
"""Get historical measurements for a station from PostgreSQL.""" """Get historical measurements for a station from the configured database."""
cache_key = f"{station_code}:{hours}:{limit}" cache_key = f"{station_code}:{hours}:{limit}"
now = time.monotonic() now = time.monotonic()
with HISTORY_CACHE_LOCK: with HISTORY_CACHE_LOCK:
@@ -463,25 +463,36 @@ 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()
if db_config["type"] != "postgresql":
raise RuntimeError("PostgreSQL is not configured")
history = PostgresHistory(db_config["connection_string"])
end_time = datetime.now() end_time = datetime.now()
data = await asyncio.to_thread( if db_config["type"] == "postgresql":
history.station_history, history = PostgresHistory(db_config["connection_string"])
station_code, data = await asyncio.to_thread(
end_time - timedelta(hours=hours), history.station_history,
end_time, station_code,
limit, end_time - timedelta(hours=hours),
) end_time,
limit,
)
else:
scraper = app_state["scraper"]
if not scraper or not scraper.db_adapter:
raise RuntimeError("Database not available")
rows = await asyncio.to_thread(
scraper.db_adapter.get_measurements_by_timerange,
end_time - timedelta(hours=hours),
end_time,
[station_code],
)
# adapter returns newest-first; keep the newest `limit` rows, chart wants ascending
data = list(reversed(rows[:limit]))
with HISTORY_CACHE_LOCK: with HISTORY_CACHE_LOCK:
HISTORY_CACHE[cache_key] = (now, data) HISTORY_CACHE[cache_key] = (now, data)
return data return data
except RuntimeError as error: except RuntimeError as error:
raise HTTPException(status_code=503, detail=str(error)) raise HTTPException(status_code=503, detail=str(error))
except Exception as error: except Exception as error:
logger.error(f"Error fetching PostgreSQL history: {error}") logger.error(f"Error fetching measurement history: {error}")
raise HTTPException(status_code=502, detail="PostgreSQL history unavailable") raise HTTPException(status_code=502, detail="Measurement history unavailable")
@app.get("/measurements/latest", response_model=List[MeasurementResponse]) @app.get("/measurements/latest", response_model=List[MeasurementResponse])