feat: Oct-2024 flood replay, simulation hooks, and static replay data
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 30s
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 17s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s

- header button 'Replay Oct 2024 flood': animates the real Sep 15 -
  Oct 12 2024 P.1 hourly series (~45 s) through the flood-zone display;
  zones flood blue as the river climbs to the 5.30 m record and recede
  as it falls; click again to stop; restores live state when done
- replay reads a pre-extracted 15 kB static snapshot
  (static/flood-2024-p1.json, 335 frames) instead of pulling the 4.6 MB
  all-time history on every click; falls back to the history API if the
  snapshot is missing
- demo hooks for testing/presentations: ?demo_level=4.4 pins a simulated
  P.1 level, ?demo_rise=1 animates rising water to 5.30 m; both label
  the outlook line as SIMULATION with the real level alongside
This commit is contained in:
2026-08-10 17:44:36 +07:00
parent c62a03e778
commit 0672ea5ffa
2 changed files with 76 additions and 1 deletions
+75 -1
View File
@@ -182,6 +182,7 @@
</div>
<div class="header-actions">
<div class="live-pill"><span class="live-dot"></span> LIVE DATA</div>
<button id="replay-2024" type="button">▶ Replay Oct 2024 flood</button>
<button id="refresh-button" type="button">↻ Refresh</button>
</div>
</header>
@@ -732,6 +733,61 @@
return `<div class="popup"><div class="popup-code">Marker</div><h3>${escapeHtml(poi.name)}</h3>${poi.note ? `<div class="popup-th">${escapeHtml(poi.note)}</div>` : ''}${riskHtml}</div>`;
}
async function replayFlood2024() {
const button = $('replay-2024');
if (state.replayTimer) { // stop a running replay
window.clearInterval(state.replayTimer);
state.replayTimer = null;
state.p1Now = state.p1RealNow;
restyleFloodZones();
button.textContent = '▶ Replay Oct 2024 flood';
$('p1-peak').textContent = state.p1PeakText || '';
return;
}
button.textContent = 'Loading 2024 data…';
try {
// pre-extracted static snapshot (15 kB); fall back to the live history API
let frames;
const staticResponse = await fetch('/static/flood-2024-p1.json');
if (staticResponse.ok) {
frames = await staticResponse.json();
} else {
const response = await fetch('/measurements/history/P.1?hours=876000&limit=100000');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const rows = await response.json();
frames = rows.filter((row) => {
const t = row.timestamp;
return t >= '2024-09-15' && t <= '2024-10-12' && row.water_level != null;
});
}
if (frames.length < 10) throw new Error('no 2024 data in history');
state.p1RealNow = state.p1Now;
state.p1PeakText = $('p1-peak').textContent;
state.zonesUserHidden = false;
await showFloodZones(false);
button.textContent = '⏹ Stop replay';
let i = 0;
state.replayTimer = window.setInterval(() => {
const frame = frames[Math.min(i, frames.length - 1)];
state.p1Now = Number(frame.water_level);
const when = new Date(frame.timestamp).toLocaleString([], { day: 'numeric', month: 'short', hour: '2-digit' });
$('p1-peak').textContent = `⏪ 2024 flood replay · ${when} · P.1 at ${state.p1Now.toFixed(2)} m`;
restyleFloodZones();
i += 1; // 1 h per frame (~45 s total)
if (i >= frames.length + 25) { // hold the last frame briefly
window.clearInterval(state.replayTimer);
state.replayTimer = null;
state.p1Now = state.p1RealNow;
restyleFloodZones();
button.textContent = '▶ Replay Oct 2024 flood';
$('p1-peak').textContent = state.p1PeakText || '';
}
}, 120);
} catch (error) {
button.textContent = `Replay unavailable: ${error.message}`;
}
}
async function loadCustomMarkers() {
try {
const response = await fetch('/static/custom-markers.json');
@@ -773,8 +829,25 @@
const row = p1rows.reduce((best, r) => r.horizon_hours > best.horizon_hours ? r : best);
state.p1Stages = row.stages;
state.p1Now = row.current_level == null ? null : Number(row.current_level);
// Demo hooks: ?demo_level=4.4 pins a simulated P.1 level; ?demo_rise=1 animates
// the water from the real level up to the 2024 record (5.30 m).
const params = new URLSearchParams(location.search);
const demoLevel = Number(params.get('demo_level'));
const simulated = Number.isFinite(demoLevel) && demoLevel > 0;
if (simulated) state.p1Now = demoLevel;
if (params.has('demo_rise') && !state.demoRiseTimer) {
let level = state.p1Now ?? 2.2;
state.demoRiseTimer = window.setInterval(() => {
level = Math.min(level + 0.05, 5.30);
state.p1Now = level;
$('p1-peak').textContent = `⚠ SIMULATION: rising water · P.1 at ${level.toFixed(2)} m (real: ${Number(row.current_level).toFixed(2)} m)`;
restyleFloodZones();
if (level >= 5.30) window.clearInterval(state.demoRiseTimer);
}, 700);
}
restyleFloodZones();
$('p1-peak').textContent = `Now ${Number(row.current_level).toFixed(2)} m · predicted peak next ${row.horizon_hours} h: ${Number(row.predicted_max_level).toFixed(2)} m`;
$('p1-peak').textContent = (simulated ? `⚠ SIMULATION: P.1 at ${demoLevel.toFixed(2)} m (real: ${Number(row.current_level).toFixed(2)} m)` :
`Now ${Number(row.current_level).toFixed(2)} m`) + ` · predicted peak next ${row.horizon_hours} h: ${Number(row.predicted_max_level).toFixed(2)} m`;
const strip = $('p1-stages');
strip.replaceChildren();
row.stages.forEach((s) => {
@@ -835,6 +908,7 @@
$('refresh-button').addEventListener('click', loadDashboard);
$('zones-toggle').addEventListener('click', toggleFloodZones);
$('replay-2024').addEventListener('click', replayFlood2024);
$('history-range').addEventListener('change', () => { if (state.selectedStation) loadHistory(state.selectedStation); });
loadDashboard();
window.setInterval(loadDashboard, 5 * 60 * 1000);