Every outage took a route.
A small museum of famous outages, each told as the route it took: one small change, a few hops, then a very bad day. Live at failrouter.com, and in Simplified Chinese at failrouter.com/zh/.
Every exhibit is condensed from a public postmortem or investigation report, and each card names its source. The 3D hall hangs the same exhibits in a gallery you can walk through: a main corridor with a map, a short passage into each room, and next to every plaque a sculpture of its impact score. Each one also has its own page, for example the CrowdStrike outage, with an impact score on five dimensions. How impact is scored lists what every score means.
It's a static site served by a Cloudflare Worker with only static assets, so no Worker code runs per request. There's no framework. The one library is three.js for the 3D hall: a pinned subset is committed under public/vendor/ and only downloaded when a visitor presses Enter. A small Node script renders every page to plain HTML ahead of time, and the output is committed, so the site reads fine with JavaScript off and deploys without a build step.
| File | What it holds |
|---|---|
public/exhibits.js |
The collection: rooms and exhibits, each with its Chinese text in zh |
public/i18n.js |
Interface strings in English and Chinese, plus locale helpers |
public/museum.js |
Card markup, room filters, sorting, impact score and keyboard walk, as pure functions |
public/app.js |
DOM wiring only |
public/hall.js |
3D hall floor plan, walls, paths, map, camera, movement, render tiers and plaque text layout, as pure functions |
public/hall-mode.js |
What the hall page decides before Enter: whether to offer 3D, what Enter downloads |
public/hall-app.js |
Wires the hall page; on Enter loads three.js and hall-scene.js, which builds the 3D world |
scripts/site.js |
Page, sitemap and structured-data generator for both languages, pure |
scripts/build.js |
Writes the generated files into public/ |
scripts/viewport.js |
Rules for the phone-width check; viewport-run.js runs it in headless Chrome |
test/ |
node:test suites, no extra test dependencies |
You need Node 22 or newer and pnpm.
pnpm install
pnpm dev # http://localhost:8787
pnpm build # regenerate pages after editing exhibits
pnpm build --og # re-render share images and icons (needs rsvg-convert)
pnpm build --posters # re-render the 3D hall poster (needs Chrome)
pnpm build --vendor # re-bundle three.js after bumping it
pnpm coverage # tests + coverage gate (90% lines, branches, functions)
pnpm viewport # every page at 320 / 375 / 430 px wide in headless Chromepnpm viewport needs Google Chrome installed (or CHROME_PATH pointing at a Chrome or Chromium binary). It fails if a page scrolls sideways or a button is shorter than 44 px, and checks the 3D hall a second time after entering it (WebGL runs in software, so no GPU is needed), and pnpm viewport --shots <dir> also saves screenshots so you can look for yourself.
Append an object to EXHIBITS in public/exhibits.js, run pnpm build, and commit both. The tests check the shape: a url-safe unique id, a subject written the way people search for the event, the added date, a known room, an ISO date, at least three hops where the last hop is the failure, a one-line lesson and a source. They also fail if you forget the build.
Score it with impact: whole numbers 0 to 10 for reach, duration, loss, recovery and cascade, each matched against the levels in RUBRIC (public/i18n.js) using only facts from the source. impactNotes gives the one-line fact behind each score.
Every exhibit also needs a zh block with the Chinese subject, title, duration, hops, lesson and impactNotes: same facts, same number of hops. The tests fail without it. If you don't write Chinese, open the PR anyway and say so in the description; the maintainer can add it before merging.
Keep to what the public report says. Describe systems and decisions, not individual people. Not sure an incident fits? Open an issue first.
Merging to main runs the workflow in .github/workflows/ci.yml: tests and the coverage gate plus the phone-width check, then wrangler deploy and a smoke test against the deployed Worker. Pull requests run the same checks with a deploy dry run. Cloudflare credentials live in repository secrets only.
If Failrouter is useful to you, a ⭐ helps other people find it.