asos-tools / static /embed.html
consgicody's picture
Sync from GitHub a470e01
68d54e8 verified
Raw
History Blame Contribute Delete
5.84 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>O.W.L. embed</title>
<style>
:root {
--bg: #0b1020;
--bg-elev: #121830;
--fg: #e6e9f2;
--fg-dim: #9aa3b8;
--accent: #7dd3fc;
--border: #1f2845;
--flag: #facc15;
}
html, body { margin: 0; padding: 0; height: 100%; background: var(--bg); color: var(--fg);
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
font-size: 14px; line-height: 1.4; }
.card { padding: 12px 14px; height: 100%; display: flex; flex-direction: column; gap: 10px; }
.head { display: flex; justify-content: space-between; align-items: baseline; }
.id { font-size: 22px; font-weight: 800; letter-spacing: .02em;
color: var(--accent); }
.name { font-size: 12px; color: var(--fg); opacity: .85; }
.kpis { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.kpi { background: var(--bg-elev); border: 1px solid var(--border); border-radius: 6px;
padding: 6px 8px; }
.kpi .label { font-size: 10px; color: var(--fg-dim); text-transform: uppercase; letter-spacing: .06em; }
.kpi .value { font-size: 16px; font-variant-numeric: tabular-nums; margin-top: 2px; }
.metar { background: var(--bg-elev); border: 1px solid var(--border); border-radius: 6px;
padding: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px; white-space: pre-wrap; word-break: break-word; }
.foot { display: flex; justify-content: space-between; align-items: center;
font-size: 10px; color: var(--fg-dim); margin-top: auto; }
.foot a { color: var(--accent); text-decoration: none; }
.badge { display: inline-block; padding: 1px 6px; border-radius: 999px; background: var(--flag);
color: #1b1500; font-size: 10px; font-weight: 700; }
</style>
</head>
<body>
<div class="card">
<div class="head">
<div>
<div class="id" id="id"></div>
<div class="name" id="name">loading…</div>
</div>
<div id="flag"></div>
</div>
<div class="kpis">
<div class="kpi"><div class="label">temp °F</div><div class="value" id="tmpf"></div></div>
<div class="kpi"><div class="label">dew °F</div><div class="value" id="dwpf"></div></div>
<div class="kpi"><div class="label">wind kt</div><div class="value" id="wind"></div></div>
<div class="kpi"><div class="label">peak gust kt</div><div class="value" id="gust"></div></div>
<div class="kpi"><div class="label">pres inHg</div><div class="value" id="pres"></div></div>
<div class="kpi"><div class="label">precip in/h</div><div class="value" id="precip"></div></div>
</div>
<div class="metar" id="metar"></div>
<div class="foot">
<span id="ts"></span>
<a id="link" href="#" target="_blank">powered by O.W.L. ↗</a>
</div>
</div>
<script src="/sdk.js"></script>
<script>
(async function () {
// /embed/{STATION}; fall back to ?station=XXXX
const path = window.location.pathname.split('/').filter(Boolean);
const fromPath = path[0] === 'embed' && path[1] ? path[1] : null;
const fromQuery = new URLSearchParams(window.location.search).get('station');
const sid = (fromPath || fromQuery || 'KJFK').toUpperCase();
document.getElementById('id').textContent = sid;
document.getElementById('link').href = '/explore?station=' + encodeURIComponent(sid);
const owl = OWL.client();
let rec = null;
try {
rec = await owl.station(sid);
document.getElementById('name').textContent =
(rec.name || '') + (rec.state ? ', ' + rec.state : '');
} catch (e) {
document.getElementById('name').textContent = 'station not found';
return;
}
async function refresh() {
try {
// IEM 1-min archive ingests with a ~2-4h lag; walk back through
// expanding windows until we hit data (or give up after 7 days).
const vars = ['tmpf','dwpf','sknt','gust_sknt','pres1','precip'];
const windows = [[1,0],[6,2],[24,2],[168,24]];
let obs = [];
for (const [spanH, lagH] of windows) {
const end = new Date(Date.now() - lagH * 3600 * 1000);
const start = new Date(end.getTime() - spanH * 3600 * 1000);
obs = await owl.obs1min(sid, isoZ(start), isoZ(end), { vars });
if (obs && obs.length) break;
}
const metar = await owl.metarLatest(sid).catch(() => null);
if (obs.length) {
const latest = obs[obs.length - 1];
const peakGust = obs.reduce((a, r) => Math.max(a, r.gust_sknt || 0), 0);
const hourPrecip = obs.reduce((a, r) => a + (r.precip || 0), 0);
set('tmpf', num(latest.tmpf, 1));
set('dwpf', num(latest.dwpf, 1));
set('wind', num(latest.sknt, 0));
set('gust', num(peakGust, 0));
set('pres', num(latest.pres1, 2));
set('precip', num(hourPrecip, 2));
document.getElementById('ts').textContent = 'updated ' + escape(latest.valid);
}
if (metar && metar.metar) {
const raw = metar.metar.rawOb || metar.metar.raw_text || '';
document.getElementById('metar').textContent = raw;
const flagged = raw.trimEnd().replace(/=$/, '').endsWith('$');
document.getElementById('flag').innerHTML = flagged
? '<span class="badge">$ MAINT</span>' : '';
}
} catch (e) {
console.warn('embed refresh failed', e);
}
}
refresh();
setInterval(refresh, 60000);
function num(v, p) { return (v == null || isNaN(v)) ? '—' : Number(v).toFixed(p); }
function set(id, v) { document.getElementById(id).textContent = v; }
function escape(s) { return s == null ? '' : String(s); }
function isoZ(d) { return d.toISOString().replace(/\.\d{3}Z$/, 'Z'); }
})();
</script>
</body>
</html>