Skip to main content
Settings
Color Mode
Theme Skin
Background

Appearance preferences are saved in this browser only.

Environment
Current Environment Production

Built with JEKYLL_ENV=production. Changes require deployment.

Theme & Build
Jekyll v3.10.0
Last Build Jul 10, 11:02
Page Location
Page Info
Layout default
Collection docs
Path _docs/the-dashboard-with-a-stale-twin.md
URL /docs/the-dashboard-with-a-stale-twin/
Date 2026-07-10

The Dashboard With a Stale Twin in the Repo

gen_dashboard.rb renders the same triage data twice — a live page for visitors and a committed SITE_HEALTH.md for the repo. Nothing keeps the copy in sync.

The Dashboard With a Stale Twin in the Repo

lifehacker.dev has a health dashboard. You can visit it at /docs/health/: last triage timestamp, the queue size, a breakdown by severity and route, and the top twenty items the robots think are wrong with the site right now. It is generated, not typed, and it says so.

It also has that same dashboard a second time — as a plain Markdown file, SITE_HEALTH.md, committed to the root of the repo so the numbers show up in the GitHub file view and in any PR that touches them. Two renderings of one set of facts. The live one is Liquid over _data/health/; the committed one is written by scripts/triage/gen_dashboard.rb.

This page is about that second file, and about the small honest problem baked into having the truth in two places: only one of the two copies refreshes itself.

What it does, in one breath

gen_dashboard.rb reads the two files the triage step already produced — _data/health/summary.yml (the rollup) and _data/health/queue.json (the ranked list) — and renders a committed Markdown snapshot. That’s it. It computes nothing; it’s a formatter. The header of the file it writes says as much:

<!-- generated by scripts/triage/gen_dashboard.rb — do not edit by hand -->

Run it and it prints one line and exits:

$ ruby scripts/triage/gen_dashboard.rb
[gen_dashboard] wrote SITE_HEALTH.md (75 queue items)

The instruction “don’t edit by hand” is doing real work. A human who fixes a number in SITE_HEALTH.md directly is writing into a file the next gen_dashboard run will silently overwrite. The file is output, not source — the same rule that governs a build artifact, printed at the top of the artifact so you can’t miss it.

Two renderers, two clocks

Here is the design worth staring at. Both dashboards read the same data, but they read it at different moments:

  • The live page (/docs/health/) renders from _data/health/ at build time, on every deploy. Whatever the data files hold when Jekyll runs is what a visitor sees. It is always current with the data because it is recomputed from the data.
  • The committed file (SITE_HEALTH.md) is a snapshot, frozen at the last moment someone ran gen_dashboard.rb. It is current with the data only if that run happened after the data last changed.

So the two agree exactly when the snapshot was taken from the current data, and they disagree the instant the data moves without a re-run. Same source of truth, two clocks — and one of the clocks is a person remembering to wind it.

Reproducing the drift (real, then reverted)

I wanted to see the twins actually disagree, so I made the data move the way a fresh triage run would — bump the queue size and the timestamp in the live data — without re-running the generator:

$ # patch the LIVE data (_data/health/summary.yml) that /docs/health/ reads
$ ruby -e 'require "yaml"; s=YAML.load_file(f="_data/health/summary.yml");
           s["queue_size"]=91; s["generated_at"]="2026-07-10T12:00:00Z";
           File.write(f, s.to_yaml)'

$ # what the LIVE page now reads:
$ grep -E 'queue_size|generated_at' _data/health/summary.yml
generated_at: '2026-07-10T12:00:00Z'
queue_size: 91

$ # what the COMMITTED SITE_HEALTH.md still says (snapshot, not regenerated):
$ grep -E 'Last triage|item.s. in the queue' SITE_HEALTH.md
_Last triage: 2026-07-06T09:11:15Z_  ·  **75** item(s) in the queue.

There it is: the live page would tell a visitor “91 items, triaged July 10,” while the file committed to the repo still says “75 items, triaged July 6.” Nobody lied. The data moved and the snapshot didn’t. The fix is exactly what you’d guess — run the generator again and the snapshot catches up:

$ ruby scripts/triage/gen_dashboard.rb
[gen_dashboard] wrote SITE_HEALTH.md (91 queue items)
$ grep -E 'Last triage' SITE_HEALTH.md
_Last triage: 2026-07-10T12:00:00Z_  ·  **91** item(s) in the queue.

Then I put both files back the way I found them (git checkout -- _data/health/summary.yml SITE_HEALTH.md) — the drift above was a demonstration on scratch data, not a change I’m shipping.

One aside I noticed while doing it: in the drifted run the rendered header said 91 (from summary.yml’s queue_size) but the console log said “75 queue items” (from the length of queue.json, which I hadn’t touched). Two counts, two sources, inside the one generator. In normal operation you never see them diverge, because the triage step writes summary.yml and queue.json together in the same breath — but the moment I hand-edited only one, the seam showed. A number that comes from two places agrees only as long as something upstream keeps them equal.

The confession: on this checkout, they agree — which is exactly why it’s easy to miss

Before I patched anything, I ran the generator against the unmodified data and diffed its output against the committed file:

$ ruby scripts/triage/gen_dashboard.rb
[gen_dashboard] wrote SITE_HEALTH.md (75 queue items)
$ diff SITE_HEALTH.committed.md SITE_HEALTH.md
$ echo $?
0

No diff. The committed twin is faithful right now, because it was generated from the same triage that wrote the data files it sits next to. That is the trap in miniature: a snapshot that happens to be correct looks identical to one that’s guaranteed correct. Nothing in the repo enforces that SITE_HEALTH.md was regenerated after the last change to _data/health/. It’s a convention — “run gen_dashboard.rb after build_queue.rb,” says the comment at the top of the script — and conventions that nothing enforces drift, and you don’t find out until someone reads the stale number and believes it.

The honest read: the live page is the one to trust, because it can’t go stale — it’s recomputed from the data every build. SITE_HEALTH.md is a convenience twin for people looking at the repo instead of the site, and its freshness is only ever as good as the last time the generator ran. I’m not adding a CI check to enforce the sync here — that’s a change to scripts/triage/, and this is a content branch. I flagged it; I didn’t patch around it.

The recursive part, because of course

Look at what actually sits at the top of the committed queue:

| score | sev  | type              | where                                          |
|-------|------|-------------------|------------------------------------------------|
| 0.7   | sev4 | type/brand-lint   | /docs/the-box-with-no-internet/                |
| 0.7   | sev4 | type/brand-lint   | /docs/the-word-police-that-cant-make-an-arrest/|
| 0.7   | sev4 | type/brand-lint   | /docs/the-check-that-wont-take-done-for-an-answer/ |

The dashboard’s own top complaints are brand-lint findings against the Meta docs — the pages that explain the brand linter. The doc about the word police trips the word police, because to describe the banned words you have to print them. The dashboard reporting this is itself a Meta doc; give it a day and it’ll be listing this page too. Everything here is sev4 — the lowest tier, nothing that blocks a merge — so it’s a to-do list, not an alarm. And note the line the summary carries at the top of every render:

_Analytics not yet connected — ranking by severity only._

That’s the analytics_stale flag, honest in both twins: the reach half of the ranking was never wired up, so the router ranks by severity and rounds reach up to a constant. The dashboard doesn’t hide that its ranking is running on one leg. It prints the disclaimer, in both copies, every time.

What it structurally can’t do

The usual list of nevers:

  • It can’t compute health. It’s a formatter over summary.yml and queue.json; if those are wrong, it renders wrong numbers faithfully.
  • It can’t keep itself fresh. It writes a snapshot when you run it. It has no idea whether the data changed since; that’s the drift this whole page is about.
  • It can’t be the source of truth. It says “do not edit by hand” because it’s output. The truth lives in _data/health/; the live page reads it directly.
  • It can’t rank or route. That happened upstream in build_queue.rb; the dashboard only shows the first twenty rows of a decision already made.

I can render the same facts twice, once for the site and once for the repo, and I can make them match the moment I run. The one thing I can’t do is promise they still match tomorrow — that’s a person, or a check nobody’s written yet, running the generator after the data moves.

But wait — there’s more! Introducing the “revolutionary,” “best-in-class” HealthSync Engine™ that “seamlessly” keeps your two dashboards in “effortless” “10x” harmony with “zero” human oversight! — a product that, in reality, is a 30-line Ruby formatter you have to remember to run, next to a live page that was already correct. It writes a snapshot. A human still has to notice when the snapshot went stale. Operators (one operator, human, re-running a script) are standing by.


Run it yourself: ruby scripts/triage/gen_dashboard.rb (after build_queue.rb) rewrites SITE_HEALTH.md from _data/health/. The queue it renders is built by the bug tracker that can’t close a ticket; the findings underneath it come from the harness that grades its own homework; the live twin of this dashboard is at /docs/health/.