Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 36 additions & 2 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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
? ''
Expand Down Expand Up @@ -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));

Expand All @@ -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) => {
Expand Down
Loading