From 9f3ea845facf4b11b72b04866dd096b1106582e0 Mon Sep 17 00:00:00 2001 From: Christopher Rodriguez Date: Tue, 27 Oct 2026 00:23:16 -0700 Subject: [PATCH] fix: send readers only the alerts they can act on alerts.json carried every announcement ever written, so the file every dashboard and overlay downloads grew with the archive while none of them shows a resolved incident. Past 50 entries it would have stopped working altogether: that is the cap the readers' schema enforces. The feed is now everything open or still to come plus anything resolved in the last 30 days, filtered on the end rather than the start so a scheduled incident still reaches readers before its window opens. The index page is untouched and keeps the whole history. Co-Authored-By: Claude Opus 5 --- README.md | 14 ++++++++++---- build.mjs | 38 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 31d703e..c0e8110 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,16 @@ Don't bother putting a CDN in front of this to speed it up; there is nothing to | Dashboard "Report a Bug" | Every active alert, before the bug tracker opens | | Overlay support panel | Every active alert, as health checks, and in the copied report | -`alerts.json` is `{ "schema": "gpv-status/1", "alerts": [...] }`. The readers validate it and treat -anything unexpected as no alerts at all, so a malformed or unreachable file degrades to silence -rather than breaking a page. The canonical schema lives in the `gpv` repository, at -`packages/data/lib/announcement.ts`; if you change the shape, change it in both places. +`alerts.json` is `{ "schema": "gpv-status/1", "alerts": [...] }`, and it carries only what a reader +could act on: every incident still open or still to come, plus anything resolved in the last 30 days. +The archive grows on the index page instead, which keeps every incident forever — no reason to make +every dashboard and overlay download years of history none of them shows. + +A reader checks the envelope strictly and then each alert on its own, keeping the ones it +understands. An unreachable file, an unknown `schema` version or a malformed entry all degrade to +silence rather than breaking a page, and one bad entry costs only itself. The canonical schema lives +in the `gpv` repository, at `packages/data/lib/announcement.ts`; if you change the shape, change it in +both places. ## Layout diff --git a/build.mjs b/build.mjs index b47cbc3..da00901 100644 --- a/build.mjs +++ b/build.mjs @@ -47,6 +47,17 @@ const SEVERITY_LABEL = { info: 'Info', warning: 'Warning', outage: 'Outage' }; const LIMITS = { title: 120, summary: 500, workaround: 200, id: 64, label: 60, links: 5 }; +/** + * How long a resolved incident stays in alerts.json. Nothing reading the feed ever shows a resolved + * alert — the dashboard and the overlay both check the window themselves — so this is slack for a + * reader whose clock is behind, and enough history to make curling the feed worth it. The index page + * is unaffected and keeps every incident forever. + */ +const FEED_RESOLVED_DAYS = 30; + +/** Mirrors MAX_ALERTS in the apps' packages/data/lib/announcement.ts, which ignores anything past it. */ +const MAX_FEED_ALERTS = 50; + const problems = []; /** Collects every problem rather than stopping at the first, so one run tells you all of them. */ @@ -178,6 +189,25 @@ const formatDate = (value) => const isActive = (alert, now) => Date.parse(alert.startsAt) <= now && (!alert.endsAt || Date.parse(alert.endsAt) > now); +/** + * The alerts worth sending to the dashboard and the overlay: everything still open or still to come, + * and anything resolved recently. Filtered on the end rather than the start, so an incident that is + * scheduled but has not begun is still sent and its reader can show it when the window opens. + */ +const feedAlerts = (announcements, now) => { + const cutoff = now - FEED_RESOLVED_DAYS * 24 * 60 * 60 * 1000; + const worthSending = announcements.filter(({ endsAt }) => !endsAt || Date.parse(endsAt) > cutoff); + + if (worthSending.length > MAX_FEED_ALERTS) { + console.warn( + `\n${worthSending.length} incidents are open or recent, over the ${MAX_FEED_ALERTS} a reader will take.` + + ` Sending the newest ${MAX_FEED_ALERTS}.\n` + ); + } + + return worthSending.slice(0, MAX_FEED_ALERTS); +}; + const linkList = (links) => links.length === 0 ? '' @@ -369,9 +399,11 @@ async function main() { await rm(DIST, { recursive: true, force: true }); await mkdir(DIST, { recursive: true }); + // The feed is only what a reader can act on; the pages below keep the whole archive + const feed = feedAlerts(announcements, now); await writeFile( join(DIST, 'alerts.json'), - `${JSON.stringify({ schema: 'gpv-status/1', alerts: announcements.map(toAlert) }, null, 2)}\n` + `${JSON.stringify({ schema: 'gpv-status/1', alerts: feed.map(toAlert) }, null, 2)}\n` ); await writeFile(join(DIST, 'index.html'), indexPage(announcements, now)); @@ -388,7 +420,9 @@ async function main() { } const active = announcements.filter((alert) => isActive(alert, now)).length; - console.log(`Built ${announcements.length} announcement(s), ${active} active, into dist/`); + console.log( + `Built ${announcements.length} announcement(s), ${active} active, ${feed.length} in the feed, into dist/` + ); } main().catch((error) => {