feat: database stats on the dashboard via GET /api/stats
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 23s
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 9s
Documentation / Build Sphinx Documentation (push) Successful in 17s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 1s
Documentation / Documentation Summary (push) Successful in 3s

New SQLAdapter.get_database_stats() aggregates totals, station count,
date range, and hourly-slot coverage in one query per dialect. The
endpoint follows the existing 503-guard/to_thread/TTL-cache pattern;
the dashboard gains a five-tile stats strip on the existing refresh
cadence. Coverage denominator is hour-truncated so off-hour endpoints
cannot push it past 100%; MySQL slot expression avoids % characters
that would break under pyformat bind interpolation.
This commit is contained in:
2026-08-10 23:27:36 +07:00
parent 5e62ea529d
commit 7befc82ff5
3 changed files with 140 additions and 0 deletions
+27
View File
@@ -255,6 +255,14 @@
</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 class="stats" id="db-stats" aria-label="Database statistics" style="margin-top:14px;display:none;grid-template-columns:repeat(auto-fit,minmax(170px,1fr))">
<article class="stat"><div class="stat-label">Total datapoints</div><div class="stat-value" id="db-total"></div><div class="stat-note">measurements stored</div></article>
<article class="stat"><div class="stat-label">Date range</div><div class="stat-value" id="db-range" style="font-size:1.05rem;line-height:1.3;white-space:normal"></div><div class="stat-note">first to last record</div></article>
<article class="stat"><div class="stat-label">Days spanned</div><div class="stat-value" id="db-days"></div><div class="stat-note">between first and last</div></article>
<article class="stat"><div class="stat-label">Stations</div><div class="stat-value" id="db-stations"></div><div class="stat-note">distinct in database</div></article>
<article class="stat"><div class="stat-label">Coverage</div><div class="stat-value" id="db-coverage"></div><div class="stat-note">of hourly slots recorded</div></article>
</section>
</main>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha384-cxOPjt7s7Iz04uaHJceBmS+qpjv2JkIHNVcuOrM+YHwZOmJGBXI00mdUXEq65HTH" crossorigin="anonymous"></script>
@@ -656,6 +664,7 @@
renderSummary(stations, readings);
$('loading').style.display = 'none';
loadForecasts(); // non-blocking; the card stays hidden until models are deployed
loadDbStats(); // non-blocking; the strip stays hidden if /api/stats is unavailable
} catch (error) {
$('loading').style.display = 'none';
$('error').style.display = 'grid';
@@ -1017,6 +1026,24 @@
}
}
async function loadDbStats() {
const strip = $('db-stats');
try {
const response = await fetch('/api/stats');
if (!response.ok) { strip.style.display = 'none'; return; }
const stats = await response.json();
const fmtDate = (value) => new Date(value).toLocaleDateString([], { day: 'numeric', month: 'short', year: 'numeric' });
$('db-total').textContent = Number(stats.total_measurements).toLocaleString();
$('db-range').textContent = `${fmtDate(stats.first_timestamp)} ${fmtDate(stats.last_timestamp)}`;
$('db-days').textContent = Number(stats.days_spanned).toLocaleString();
$('db-stations').textContent = String(stats.station_count);
$('db-coverage').textContent = stats.coverage_percent == null ? '—' : `${Number(stats.coverage_percent).toFixed(1)}%`;
strip.style.display = 'grid';
} catch (error) {
strip.style.display = 'none';
}
}
$('refresh-button').addEventListener('click', loadDashboard);
$('zones-toggle').addEventListener('click', toggleFloodZones);
$('replay-2024').addEventListener('click', replayFlood2024);