import type { Alarm } from "./alerts"; import { prettyJson } from "./email"; import { REGIONS, REGION_NAMES, type Check, type Region, type RegionSnapshot } from "./monitor"; const ESCAPES: Record = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; export function esc(value: unknown): string { return String(value).replace(/[&<>"']/g, (c) => ESCAPES[c]); } /** A region's result is stale when its newest check is older than this. */ export function staleAfterMs(env: Env): number { return 2 * Number(env.CHECK_INTERVAL_SECONDS) * 1000 + Number(env.TIMEOUT_MS); } function shortUrl(url: string): string { return url.replace(/^https?:\/\//, ""); } function historyHref(endpoint: string, params: Record = {}): string { return "/endpoint?" + new URLSearchParams({ url: endpoint, ...params }).toString(); } function statusLabel(c: Check): string { return c.status === 0 ? "ERR" : String(c.status); } function time(ts: number): string { const iso = new Date(ts).toISOString(); return ``; } function body(c: Check): string { const parts: string[] = []; if (c.error) parts.push(`

${esc(c.error)}

`); const pretty = prettyJson(c.body); if (pretty !== null) parts.push(`
${esc(pretty)}
`); else if (!c.error) parts.push(`

Empty response body

`); return parts.join(""); } // --- Homepage --------------------------------------------------------------- type Cell = { region: Region; check: Check | null; state: "ok" | "fail" | "stale" | "unknown" }; export function homePage( env: Env, endpoints: string[], snapshots: (RegionSnapshot | null)[], alarms: Alarm[] | null, now: number, ): string { const staleMs = staleAfterMs(env); const rows = endpoints.map((endpoint) => { const cells: Cell[] = REGIONS.map((region, i) => { const snap = snapshots[i]; const check = snap?.checks.find((c) => c.endpoint === endpoint) ?? null; const state = !check ? "unknown" : now - check.ts > staleMs ? "stale" : check.status === 200 ? "ok" : "fail"; return { region, check, state }; }); return { endpoint, cells, failing: cells.some((c) => c.state === "fail") }; }); // Failing endpoints first, otherwise keep configured order. rows.sort((a, b) => Number(b.failing) - Number(a.failing)); const unreachable = REGIONS.filter((_, i) => snapshots[i] === null); const banner = unreachable.length ? `` : ""; return layout( "Monitor", `

Monitor

Refresh
${banner} ${alarmsSection(alarms, new Set(endpoints))}

Endpoints

${rows.map(endpointRow).join("") || `

No endpoints configured.

`}

Status and latency show the worst result across regions. Expand a row for per-region details.

`, ); } function alarmsSection(alarms: Alarm[] | null, configured: Set): string { if (alarms === null) { return `

Active alarms

`; } const items = alarms.map((a) => { const silenced = a.silencedAt !== null; const isConfigured = configured.has(a.endpoint); const state = !isConfigured ? "Endpoint no longer configured" : silenced ? `Silenced ${time(a.silencedAt!)} · reminders resume if anything else fails; recovery is still sent` : a.lastAlertTs !== null ? `Last alert ${time(a.lastAlertTs)}` : "Alert not sent yet"; const name = isConfigured ? `${esc(shortUrl(a.endpoint))}` : `${esc(shortUrl(a.endpoint))}`; const control = isConfigured ? `` : ``; return `
${name} ${a.failures.map((f) => `${esc(f)}`).join("")} Failing since ${time(a.failingSince)}${a.failingRegions.length ? ` from ${a.failingRegions.map(esc).join(", ")}` : ""} ${state}
${control}
`; }); return `

Active alarms${alarms.length ? ` ${alarms.length}` : ""}

${items.join("") || `

No active alarms.

`}
`; } function endpointRow({ endpoint, cells, failing }: { endpoint: string; cells: Cell[]; failing: boolean }): string { const checks = cells.map((c) => c.check).filter((c): c is Check => c !== null); const worst = checks.find((c) => c.status !== 200) ?? checks[0]; const newest = checks.length ? Math.max(...checks.map((c) => c.ts)) : null; const maxLatency = checks.length ? Math.max(...checks.map((c) => c.duration_ms)) : null; const badges = cells .map((c) => `${c.region}`) .join(""); const regionRows = cells .map(({ region, check, state }) => check ? `
${region} ${statusLabel(check)} ${time(check.ts)} ${check.duration_ms} ms ${body(check)}
` : `
${region}no data
`, ) .join(""); return `
${esc(shortUrl(endpoint))} ${worst ? statusLabel(worst) : "—"} ${newest ? time(newest) : "—"} ${maxLatency !== null ? `${maxLatency} ms` : "—"} ${badges} History →
${regionRows}
`; } // --- History page ------------------------------------------------------------- export interface HistoryView { endpoint: string; region: Region | "all"; failuresOnly: boolean; before: number | null; checks: Check[]; nextBefore: number | null; unreachable: Region[]; } export function historyPage(v: HistoryView): string { const filters: Record = {}; if (v.region !== "all") filters.region = v.region; if (v.failuresOnly) filters.failures = "1"; const options = ["all", ...REGIONS] .map((r) => ``) .join(""); const rows = v.checks .map((c) => `
${time(c.ts)} ${c.region} ${statusLabel(c)} ${c.duration_ms} ms ${body(c)}
`) .join(""); const banner = v.unreachable.length ? `` : ""; const pager = [ v.before !== null ? `← Newest` : "", v.nextBefore !== null ? `Older →` : "", ].join(""); return layout( `${shortUrl(v.endpoint)} — Monitor`, `
← All endpoints

${esc(shortUrl(v.endpoint))}

${banner} ${rows || `

No checks match.

`} `, ); } // --- Shared ------------------------------------------------------------------- export function errorPage(status: number, message: string): Response { return html(layout("Monitor", `
← All endpoints

${esc(message)}

`), status); } export function html(content: string, status = 200): Response { return new Response(content, { status, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }, }); } /** Favicon: a white exclamation mark on a red rounded square, inlined as an SVG data URI. */ const FAVICON = "data:image/svg+xml," + encodeURIComponent( `` + `` + `` + `` + ``, ); function layout(title: string, content: string): string { return ` ${esc(title)}
${content}
`; } // Rewrites UTC timestamps into the viewer's local time. const SCRIPT = ` for (const t of document.querySelectorAll("time[datetime]")) { const d = new Date(t.dateTime); const sameDay = d.toDateString() === new Date().toDateString(); t.textContent = sameDay ? d.toLocaleTimeString() : d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }); t.title = d.toString(); }`; const CSS = ` :root { --fg:#1a1a1a; --muted:#666; --bg:#fff; --line:#e3e3e3; --hover:#f5f5f5; --pre:#f6f6f6; --ok:#17803d; --fail:#c62828; --stale:#b26a00; --unknown:#888; color-scheme: light dark; } @media (prefers-color-scheme: dark) { :root { --fg:#e6e6e6; --muted:#9a9a9a; --bg:#151515; --line:#2e2e2e; --hover:#1f1f1f; --pre:#1d1d1d; --ok:#4caf50; --fail:#ef5350; --stale:#ffb74d; --unknown:#777; } } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; } main { max-width: 1000px; margin: 0 auto; padding: 16px; } header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; } h1 { font-size: 1.3rem; margin: 0; } h2 { font-size: 1rem; margin: 20px 0 8px; } .count { display: inline-block; min-width: 1.5em; padding: 0 6px; border-radius: 10px; background: var(--fail); color: #fff; font-size: .8rem; text-align: center; } .alarm { display: flex; gap: 12px; align-items: center; justify-content: space-between; padding: 10px 12px; margin-bottom: 8px; border: 1px solid var(--line); border-left: 4px solid var(--fail); border-radius: 6px; } .alarm.silenced { border-left-color: var(--unknown); } .alarm.silenced .name { color: var(--muted); } .alarm form { flex-shrink: 0; } .alarm-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; } .alarm-main .muted { font-size: .85rem; } .chips { display: flex; gap: 4px; flex-wrap: wrap; } .chip { font-size: .75rem; padding: 1px 6px; border-radius: 10px; border: 1px solid var(--line); background: var(--hover); } .endpoint-title { margin: 0 0 12px; word-break: break-all; } a { color: inherit; } .button, button { display: inline-block; padding: 6px 12px; border: 1px solid var(--line); border-radius: 6px; text-decoration: none; background: var(--bg); color: var(--fg); font: inherit; cursor: pointer; } .muted { color: var(--muted); } .legend { font-size: .85rem; margin-top: 16px; } .banner { padding: 8px 12px; border-left: 4px solid var(--stale); background: var(--hover); } .error { color: var(--fail); margin: 8px 0; } pre { background: var(--pre); padding: 10px; border-radius: 6px; overflow-x: auto; font-size: .85rem; margin: 8px 0; } /* Rows: grid on wide screens, wrapped lines on phones. */ .row { display: grid; gap: 4px 12px; align-items: center; padding: 10px 8px; } details > summary { cursor: pointer; list-style: none; } details > summary::-webkit-details-marker { display: none; } details > summary:hover { background: var(--hover); } details[open] > summary { background: var(--hover); } .endpoint, .check { border-bottom: 1px solid var(--line); } .head { font-size: .8rem; color: var(--muted); text-transform: uppercase; letter-spacing: .03em; border-bottom: 1px solid var(--line); padding-top: 0; } .name { font-weight: 600; word-break: break-all; } .status { font-weight: 600; font-variant-numeric: tabular-nums; } .status.ok { color: var(--ok); } .status.fail { color: var(--fail); } .status.stale { color: var(--stale); } .status.unknown { color: var(--unknown); } .badges { display: flex; gap: 4px; flex-wrap: wrap; } .badge { font-size: .75rem; padding: 1px 6px; border-radius: 10px; color: #fff; background: var(--unknown); } .badge.ok { background: var(--ok); } .badge.fail { background: var(--fail); } .badge.stale { background: var(--stale); } .toggle::before { content: "▸"; display: inline-block; width: 1.1em; color: var(--muted); font-weight: normal; } details[open] > summary .toggle::before { content: "▾"; } time, .status, .cols-region span, .cols-check span { white-space: nowrap; } .regions { padding: 0 8px 12px 24px; } .region { border-top: 1px solid var(--line); } .region > pre, .region > p, .check > pre, .check > p { margin-left: 8px; margin-right: 8px; } /* Column templates */ .cols-endpoint { grid-template-columns: minmax(0,1fr) 3.5rem 7.5rem 5.5rem 15rem 5.5rem; } .cols-region { grid-template-columns: 5rem 3.5rem 7.5rem 5.5rem; } .cols-check { grid-template-columns: 12rem 4rem 3.5rem 5.5rem; } .filters { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 12px; } .filters select { padding: 6px; font: inherit; max-width: 100%; } .pager { display: flex; gap: 8px; justify-content: space-between; margin-top: 16px; } @media (max-width: 640px) { .head.cols-endpoint { display: none; } .cols-endpoint { grid-template-columns: auto auto 1fr auto; } .cols-endpoint .name { grid-column: 1 / -2; } .cols-endpoint a { grid-column: -2 / -1; grid-row: 1; justify-self: end; } .cols-endpoint .badges { grid-column: 1 / -1; } .cols-region { grid-template-columns: 4.5rem 3rem 1fr auto; } .cols-check { grid-template-columns: 1fr 3rem 3rem 4.5rem; } .cols-check .name { font-weight: normal; } .regions { padding-left: 8px; } } `;