feat: physically meaningful stat tiles + footer links
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 24s
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 13s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s

'Combined discharge' summed sequential mainstem gauges, counting the
same water several times — replaced with Chiang Mai river flow (P.1
discharge + % of channel capacity). 'Strongest flow' becomes 'Most
stressed gauge' (max discharge_percent basin-wide). Reporting-stations
note shows the ThaiWater/HII station count. Footer links to the source
repo and /docs API reference.
This commit is contained in:
2026-08-11 16:25:41 +07:00
parent df31de092b
commit d0018d6529
+29 -9
View File
@@ -202,9 +202,9 @@
</header>
<section class="stats" aria-label="River summary">
<article class="stat"><div class="stat-label">Reporting stations</div><div class="stat-value" id="station-count"></div><div class="stat-note">with current readings</div></article>
<article class="stat"><div class="stat-label">Combined discharge</div><div class="stat-value" id="total-flow"></div><div class="stat-note">sum of reported flows · m³/s</div></article>
<article class="stat"><div class="stat-label">Strongest flow</div><div class="stat-value" id="peak-flow"></div><div class="stat-note" id="peak-station">Awaiting station data</div></article>
<article class="stat"><div class="stat-label">Reporting stations</div><div class="stat-value" id="station-count"></div><div class="stat-note" id="station-count-note">RID gauges with current readings</div></article>
<article class="stat"><div class="stat-label">Chiang Mai river flow</div><div class="stat-value" id="total-flow"></div><div class="stat-note" id="cm-flow-note">P.1 Nawarat Bridge · m³/s</div></article>
<article class="stat"><div class="stat-label">Most stressed gauge</div><div class="stat-value" id="peak-flow"></div><div class="stat-note" id="peak-station">Awaiting station data</div></article>
<article class="stat"><div class="stat-label">Last updated</div><div class="stat-value" id="last-updated"></div><div class="stat-note" id="data-age">Loading latest readings</div></article>
</section>
@@ -284,6 +284,14 @@
<article class="stat"><div class="stat-label">Stations</div><div class="stat-value" id="db-stations"></div><div class="stat-note" id="db-stations-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 RID hourly slots recorded</div></article>
</section>
<footer style="margin:22px 0 10px;display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;color:var(--muted);font-size:.75rem">
<span>Open-source river monitoring for Northern Thailand · data from RID &amp; ThaiWater/HII</span>
<nav style="display:flex;gap:16px">
<a href="https://git.b4l.co.th/B4L/Northern-Thailand-Ping-River-Monitor" target="_blank" rel="noopener" style="color:var(--ink);font-weight:700;text-decoration:none">Source code ↗</a>
<a href="/docs" target="_blank" rel="noopener" style="color:var(--ink);font-weight:700;text-decoration:none">API docs ↗</a>
</nav>
</footer>
</main>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha384-cxOPjt7s7Iz04uaHJceBmS+qpjv2JkIHNVcuOrM+YHwZOmJGBXI00mdUXEq65HTH" crossorigin="anonymous"></script>
@@ -676,6 +684,7 @@
const additional = sensors
.filter((sensor) => !existingCodes.has(sensor.station_code))
.sort((a, b) => (Number(b.bank_percent) || -1) - (Number(a.bank_percent) || -1));
state.hiiReportingCount = additional.length;
$('thaiwater-count').textContent = `${additional.length} Ping basin stations · ThaiWater/HII water level`;
additional.forEach((sensor) => {
const percent = sensor.bank_percent == null ? null : Number(sensor.bank_percent);
@@ -713,15 +722,26 @@
function renderSummary(stations, readings) {
const current = stations.map((s) => readings.get(s.station_code)).filter(Boolean);
const flows = current.filter((m) => m.discharge != null).map((m) => ({ code: m.station_code, value: Number(m.discharge) }));
const total = flows.reduce((sum, item) => sum + item.value, 0);
const peak = flows.length ? flows.reduce((max, item) => item.value > max.value ? item : max) : null;
const timestamps = current.map((m) => new Date(m.timestamp)).filter((date) => !Number.isNaN(date.getTime()));
const latest = timestamps.length ? new Date(Math.max(...timestamps.map((date) => date.getTime()))) : null;
$('station-count').textContent = `${current.length} / ${stations.length}`;
$('total-flow').textContent = flows.length ? total.toLocaleString(undefined, { maximumFractionDigits: 1 }) : '—';
$('peak-flow').textContent = peak ? peak.value.toFixed(1) : '—';
$('peak-station').textContent = peak ? `${peak.code} · m³/s` : 'No discharge reported';
if (state.hiiReportingCount != null) {
$('station-count-note').textContent = `RID gauges · +${state.hiiReportingCount} ThaiWater/HII reporting`;
}
// Flow at the basin anchor (summing sequential mainstem gauges would
// double-count the same water, so no basin-wide total is shown).
const p1 = readings.get('P.1');
const p1Flow = p1?.discharge == null ? null : Number(p1.discharge);
$('total-flow').textContent = p1Flow == null ? '—' : p1Flow.toLocaleString(undefined, { maximumFractionDigits: 1 });
$('cm-flow-note').textContent = p1?.discharge_percent != null
? `P.1 Nawarat Bridge · ${Number(p1.discharge_percent).toFixed(0)}% of channel capacity · m³/s`
: 'P.1 Nawarat Bridge · m³/s';
const stressed = current
.filter((m) => m.discharge_percent != null)
.map((m) => ({ code: m.station_code, percent: Number(m.discharge_percent) }));
const worst = stressed.length ? stressed.reduce((max, item) => item.percent > max.percent ? item : max) : null;
$('peak-flow').textContent = worst ? `${worst.percent.toFixed(0)}%` : '—';
$('peak-station').textContent = worst ? `${worst.code} · of channel capacity` : 'No capacity data';
$('last-updated').textContent = latest ? latest.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '—';
if (latest) {
const minutes = Math.max(0, Math.round((Date.now() - latest.getTime()) / 60000));