Files
Sandbar/internal/sandbar/web/status.html
T
VeillaxandClaude Sonnet 5.5 0c5a57556a Initial commit: notifications, monitoring, on-call and incidents
One binary and one SQLite file. Topics with ntfy-style publish and
subscribe, HTTP/TCP/DNS/TLS/heartbeat monitors with a public status page,
schedules and escalation policies, and incidents with a timeline and
post-mortem. The UI follows the Lightwell design language.

Builds against an unreleased Reef (secretbox) through a local replace line.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 23:21:56 -06:00

105 lines
6.5 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Status</title>
<style>
/* tokens:begin Design tokens, the Lightwell design language (docs/design.md). This is the only place raw colors and sizes are defined. */
:root {
--accent:#1f5c5c; --accent-contrast:#ffffff;
--bg:#f6f5f3; --surface:#ffffff; --surface-2:#efede9; --border:#e0ddd7; --border-strong:#c4c0b8;
--text:#1d1c1a; --text-muted:#5f5b54; --text-faint:#7a766e;
--accent-text:var(--accent);
--accent-soft:color-mix(in srgb, var(--accent) 9%, var(--surface));
--focus:color-mix(in srgb, var(--accent) 70%, #ffffff);
--success:#1c6b3a; --success-bg:#e3f1e8; --warning:#85560a; --warning-bg:#f9eed6; --danger:#a3271e; --danger-bg:#f9e6e3; --neutral-bg:#eceae6;
--s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px; --s7:48px; --s8:64px;
--radius:6px; --radius-lg:10px; --border-w:1px;
--font:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
--text-xs:12px; --text-sm:13px; --text-base:15px; --text-lg:18px; --text-xl:22px; --text-2xl:28px; --leading:1.5;
--content-max:720px;
color-scheme:light;
}
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) {
--bg:#131312; --surface:#1b1b19; --surface-2:#252523; --border:#33322e; --border-strong:#4a4943;
--text:#ecebe7; --text-muted:#adaaa2; --text-faint:#8f8c84;
--accent-text:color-mix(in srgb, var(--accent) 45%, #ffffff);
--accent-soft:color-mix(in srgb, var(--accent) 22%, var(--surface));
--success:#86d3a0; --success-bg:#17301f; --warning:#e8bd6b; --warning-bg:#33280f; --danger:#f19a92; --danger-bg:#3a1b18; --neutral-bg:#2b2a27;
color-scheme:dark;
} }
/* tokens:end */
*, *::before, *::after { box-sizing:border-box; }
body { margin:0; background:var(--bg); color:var(--text); font:var(--text-base)/var(--leading) var(--font); -webkit-font-smoothing:antialiased; }
h1, h2 { margin:0; line-height:1.25; font-weight:600; letter-spacing:-.01em; }
h1 { font-size:var(--text-2xl); } h2 { font-size:var(--text-lg); margin:var(--s6) 0 var(--s3); }
main { max-width:var(--content-max); margin:0 auto; padding:var(--s6) var(--s4) var(--s8); }
.dim { color:var(--text-muted); } .small { font-size:var(--text-sm); } .num { font-variant-numeric:tabular-nums; }
.banner { margin-top:var(--s4); padding:var(--s4) var(--s5); border-radius:var(--radius-lg); border:var(--border-w) solid var(--border); font-weight:600; background:var(--surface-2); }
.banner.operational { background:var(--success-bg); color:var(--success); border-color:var(--success); }
.banner.degraded { background:var(--warning-bg); color:var(--warning); border-color:var(--warning); }
.banner.outage { background:var(--danger-bg); color:var(--danger); border-color:var(--danger); }
.card { background:var(--surface); border:var(--border-w) solid var(--border); border-radius:var(--radius-lg); overflow:hidden; }
.row { display:flex; align-items:center; justify-content:space-between; gap:var(--s3); flex-wrap:wrap; padding:var(--s3) var(--s4); border-bottom:var(--border-w) solid var(--border); }
.row:last-child { border-bottom:0; }
.badge { display:inline-flex; align-items:center; padding:2px var(--s2); border-radius:999px; font-size:var(--text-xs); font-weight:600; background:var(--neutral-bg); color:var(--text-muted); white-space:nowrap; }
.badge-success { background:var(--success-bg); color:var(--success); }
.badge-warning { background:var(--warning-bg); color:var(--warning); }
.badge-danger { background:var(--danger-bg); color:var(--danger); }
.inc { padding:var(--s4); border-bottom:var(--border-w) solid var(--border); }
.inc:last-child { border-bottom:0; }
.inc ul { margin:var(--s2) 0 0; padding-left:var(--s5); }
:focus-visible { outline:2px solid var(--focus); outline-offset:2px; border-radius:var(--radius); }
</style>
</head>
<body>
<main>
<h1>Status</h1>
<div id="banner" class="banner" role="status">Loading</div>
<h2>Services</h2>
<div class="card" id="services"></div>
<h2>Incidents</h2>
<div class="card" id="incidents"></div>
</main>
<script>
const $ = id => document.getElementById(id);
const el = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
const when = t => new Date(t * 1000).toLocaleString(undefined, {month:'short', day:'numeric', year:'numeric', hour:'numeric', minute:'2-digit'});
const label = { operational: 'All systems are operational', degraded: 'Some systems are affected', outage: 'There is a service outage' };
const statusBadge = s => s === 'up' ? el('span', 'badge badge-success', 'Operational') : s === 'down' ? el('span', 'badge badge-danger', 'Down') : el('span', 'badge', 'Waiting for first check');
const incStatus = { investigating: 'Investigating', identified: 'Identified', monitoring: 'Monitoring', resolved: 'Resolved' };
async function load() {
let d;
try { d = await (await fetch('/status.json')).json(); } catch (e) { $('banner').textContent = 'The status could not be loaded. Try again in a moment.'; return; }
$('banner').className = 'banner ' + d.overall;
$('banner').textContent = label[d.overall] || d.overall;
const sv = $('services'); sv.replaceChildren();
if (!d.monitors.length) sv.append(el('div', 'row dim', 'No services are published yet.'));
for (const m of d.monitors) {
const r = el('div', 'row');
r.append(el('span', '', m.name));
const right = el('span', 'small dim num');
right.append(statusBadge(m.status), document.createTextNode(m.uptime >= 0 ? ' ' + m.uptime.toFixed(2) + '% uptime' : ''));
r.append(right); sv.append(r);
}
const ic = $('incidents'); ic.replaceChildren();
if (!d.incidents.length) ic.append(el('div', 'row dim', 'No incidents in the last 14 days.'));
for (const i of d.incidents) {
const b = el('div', 'inc');
const head = el('div'); head.append(el('strong', '', i.title + ' '), el('span', 'badge ' + (i.status === 'resolved' ? 'badge-success' : 'badge-warning'), incStatus[i.status] || i.status));
b.append(head, el('div', 'small dim', 'Started ' + when(i.created) + (i.resolved ? ', resolved ' + when(i.resolved) : '')));
if (i.updates.length) { const ul = el('ul', 'small'); for (const u of i.updates) ul.append(el('li', '', when(u.time) + ': ' + u.text)); b.append(ul); }
ic.append(b);
}
}
load(); setInterval(load, 30000);
</script>
</body>
</html>