Log official NWS observations for a reference ZIP and overlay them on the dashboard
Publish Container / publish (push) Successful in 2m33s
Publish Container / publish (push) Successful in 2m33s
- Resolves ZIP (default 24550) to the nearest NWS observation station; logs every observation (normalised imperial values + raw METAR), back-fills ~7 days, snapshots the hourly forecast every 3h, and logs alerts for that point - Chart overlay (temp, humidity/dew point, wind, pressure) with an NWS toggle in the History bar - 'Official NWS observation' card on the Now tab comparing against your station - Settings: change the reference ZIP and see logging status - Exact-unit conversions for NWS values (verified against known conversions) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5ed673bacc
commit
acdcd6a2c5
5 files changed
+364
-20
No files matched your search
@@ -249,6 +249,7 @@ function renderNowExtras() {
|
||||
updateCompass(LATEST);
|
||||
renderSunCard();
|
||||
renderHealthCard();
|
||||
renderNwsCard();
|
||||
renderHighlights();
|
||||
}
|
||||
|
||||
@@ -569,14 +570,52 @@ function renderInsights() {
|
||||
} catch {}
|
||||
})();
|
||||
|
||||
|
||||
// ── Official NWS observation vs your station ─────────────────────────────────
|
||||
let NWS = null;
|
||||
function renderNwsCard() {
|
||||
const host = $('nwsCard');
|
||||
if (!host) return;
|
||||
const n = NWS, l = n?.latest, r = n?.resolved;
|
||||
if (!n) { host.innerHTML = ''; return; }
|
||||
if (!l) {
|
||||
host.innerHTML = `<div class="ui-card"><h3>Official NWS observation</h3><p class="note">${n.lastError ? 'Could not reach the National Weather Service yet: ' + n.lastError : 'Waiting for the first observation from the National Weather Service…'}${r ? ` Station <b>${r.stationId}</b> for ZIP ${r.zip}.` : ''}</p></div>`;
|
||||
return;
|
||||
}
|
||||
const f = (v, d = 0, u = '') => (num(v) == null ? '—' : v.toFixed(d) + u);
|
||||
const ageMin = Math.round((Date.now() - l.ts) / 60000);
|
||||
const mine = LATEST;
|
||||
const diffs = [];
|
||||
if (mine && num(mine.tempf) != null && num(l.tempf) != null && Math.abs(mine.dateutc - l.ts) < 90 * 60000) {
|
||||
const d = mine.tempf - l.tempf;
|
||||
diffs.push(Math.abs(d) < 0.5 ? 'Your station matches NWS on temperature' : `Your station reads <b>${Math.abs(d).toFixed(1)}°F ${d > 0 ? 'warmer' : 'cooler'}</b>`);
|
||||
if (num(mine.humidity) != null && num(l.humidity) != null && Math.abs(mine.humidity - l.humidity) >= 3) diffs.push(`${Math.abs(mine.humidity - l.humidity).toFixed(0)}% ${mine.humidity > l.humidity ? 'more humid' : 'drier'}`);
|
||||
}
|
||||
const wind = num(l.windspeedmph) != null ? `${l.winddir != null ? cardinal(l.winddir) + ' ' : ''}${l.windspeedmph.toFixed(0)} mph${num(l.windgustmph) != null ? ' (g ' + l.windgustmph.toFixed(0) + ')' : ''}` : '—';
|
||||
host.innerHTML = `<div class="ui-card nws-card"><h3>Official NWS observation <span class="nws-dot"></span></h3>
|
||||
<p class="note" style="margin:-0.4rem 0 0.8rem"><b>${esc2(r?.stationId || l.station)}</b> ${esc2(r?.stationName || '')} · ${r?.distanceMi != null ? r.distanceMi + ' mi from ' : 'for '}ZIP ${esc2(r?.zip || '')}${r?.place ? ' (' + esc2(r.place) + ')' : ''}</p>
|
||||
<div class="kv">
|
||||
<div><b>${f(l.tempf, 1, '°F')}</b><span>Temperature</span></div>
|
||||
<div><b>${f(l.dewPoint, 0, '°F')}</b><span>Dew point</span></div>
|
||||
<div><b>${f(l.humidity, 0, '%')}</b><span>Humidity</span></div>
|
||||
<div><b>${wind}</b><span>Wind</span></div>
|
||||
<div><b>${f(l.baromrelin, 2, ' inHg')}</b><span>Pressure</span></div>
|
||||
<div><b>${f(l.visibilityMi, 0, ' mi')}</b><span>Visibility</span></div>
|
||||
<div><b>${esc2(l.text || '—')}</b><span>Conditions</span></div>
|
||||
</div>
|
||||
<p class="note" style="margin-top:0.8rem">Observed ${new Date(l.ts).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })} (${ageMin} min ago)${diffs.length ? ' · ' + diffs.join(' · ') : ''} · ${n.observations.toLocaleString()} observations logged${n.firstObservation ? ' since ' + prettyDay(dayStr(n.firstObservation)) : ''}${l.raw ? `<br><code class="nws-raw">${esc2(l.raw)}</code>` : ''}</p></div>`;
|
||||
}
|
||||
const esc2 = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
|
||||
// ── Data refresh ─────────────────────────────────────────────────────────────
|
||||
async function refreshData() {
|
||||
try {
|
||||
const [daily, recent] = await Promise.all([
|
||||
const [daily, recent, nws] = await Promise.all([
|
||||
getJSON('/api/station/daily').catch(() => []),
|
||||
getJSON('/api/station/history?range=2d').catch(() => []),
|
||||
getJSON('/api/nws/latest').catch(() => null),
|
||||
]);
|
||||
DAILY = daily; RECENT = recent;
|
||||
DAILY = daily; RECENT = recent; NWS = nws;
|
||||
LATEST = RECENT[RECENT.length - 1] || null;
|
||||
const allBtn = document.querySelector('.range-btn[data-range="all"]');
|
||||
if (allBtn && DAILY[0]) {
|
||||
|
||||
Reference in new issue
Block a user