Projects Dashboard

My build fails if the page it just produced contains an email address.

The private app I run my week from, written entirely by agents. Its build audits its own HTML, and yesterday’s cached page can’t vouch for itself.

The Reading queue with saved articles and one saved screenshot of a bundle budget slide, its text transcribed on the card.
Real screen, sample data.

After every build, my dashboard reads the page it just produced as if a stranger wrote it.

This is the private app I run my week from. It opens on Today and reaches Briefing, Radio, Reading and Cooking, plus screens I’m keeping to myself. You can’t log in, so any screen on this page is the real app running on sample data I had made for it.

Agents write all of the code. With no customers, nobody will ever file a bug. If I want something to stay true, I need a check that can fail.

The Briefing as it appears in the dashboard, with a Top Story and numbered Takes on a white page against a dark background.
Real screen, sample data.

The Build Audits Its Own HTML

The first thing the app serves is one static document with nobody’s identity in it, which lets it come off a CDN. Personal data arrives only after sign-in. An agent can write a page that looks right and still serialize an identity into it.

So a script reads the exact HTML that came out, since that’s what gets cached. An email address or a user ID fails the build. So does a fingerprint derived from an allowed user’s ID. So does an address spelled @ or \x40. A plain search for the @ sign walks right past those, so the test suite feeds them in on purpose.

The same script then writes the page’s content security policy from SHA-256 hashes of its exact inline scripts, and refuses to emit one whose script-src allows unsafe-inline. Then it re-reads what it just wrote and fails if any hash didn’t land. It’s a build script that doesn’t trust itself, which I like in a build script.

Three spellings of one example address, and whether a plain search for the at sign finds each one
In the built page Search for @
allowed@example.com Finds it
allowed@example.com Walks past it
allowed\x40example.com Walks past it

The audit fails the build on all three.

A reconstruction with example.com values, not the script’s real output.

The Bytes Changed After the Build

That same page ships integrity hashes on its local scripts. An integrity hash tells the browser what a file’s bytes should be, and the build computed them correctly.

Then Vercel appended deployment-specific toolbar bytes to the webpack runtime, after the build had hashed it. The page stopped hydrating.

The easy fix is to delete the hashes. Instead, the postbuild step removes integrity from that one runtime chunk and adds hashes to any stable chunk that lacks one. Then it fails the build unless there is exactly one such runtime, nothing still refers to it with a hash, and every other local script has a SHA-256 hash.

This bug sits between the framework, the host and the browser’s security rules. No component an agent generates will ever show it to you. Frontend platform work lives in that gap.

The build hashes its chunks.

Each hash describes the bytes the build wrote.

The host appends bytes to one runtime.

Its hash no longer describes it, and the browser stops hydrating the page.

The fix removes that one hash.

The build fails if there’s more than one such runtime, or if anything else lacks a hash.

Schematic: bar lengths aren’t file sizes. The line under a bar is its hash, blue where it matches and gray where it doesn’t.

The Cache Is a Suspect Witness

The first time I open the app on a new day, it shows me yesterday. On purpose.

The page has to paint immediately, so the last good copy lives in the browser. What I care about is what a stale screen is allowed to do.

Every read of that copy gets sorted into one of seven states: current, previous-day, expired, malformed, old-version, wrong-user or missing. Only the first two can paint. The rest delete the entry or return nothing.

Yesterday’s page paints and goes straight on probation. One day-scoped write stays locked until today’s data arrives, so today’s entry can’t land on yesterday’s page.

The lock covers that one write. Other saves on the page still go through.

What the screen does with each kind of cached copy
The cached copy is What the screen does
Current Paints.
From the previous day Paints instantly. One day-scoped write stays locked until today’s data arrives.
Expired, malformed, an old version or another user’s Deleted or ignored.
Missing Waits for live data.

Yesterday’s sneakiest way back in is through the server. Browsers have a polite way of asking whether something has changed, and a 304 answer means no, what you have is fine. If what you have is yesterday’s page, that “fine” just vouched for it. So the app sends its cached ETag only for a snapshot classified as current.

A request can also straddle midnight. When the date rolls over the app retries, and if the server still answers with the old date, the app counts that as an error, even when the answer is a tidy “not modified”. The screen says: “Today is still showing the previous day.”

The Laptop Went to Sleep

I kept getting asked to sign in, and then signing in failed with “You’re already signed in.” Getting locked out of my own app by an app insisting I was already in it is a real achievement.

On the web, one cause was a cached session token that could expire while the laptop slept, before the auth library’s background refresh ran. One 401 looked like a signed-out user. That takes days of real use on a real laptop to appear.

The client now answers a 401 by minting a fresh token and trying once more, and only then believes it. A stream body can’t be sent twice, so it’s exempt. The sign-in page also resumes a live session instead of showing the form, at most once every 30 seconds so a stale server can’t put it in a loop.

The other direction stays strict. If the API really rejects a user, or the identity changes inside an open tab, the app clears private browser storage and leaves the page.

A Failed Classifier Waits for a Person

There’s a box I can type anything into. Unless a simple rule claims the text first, it goes to a model that has to pick from a fixed list of destinations. Most of that list is generated into the prompt from the same source the schema enforces, so for those destinations the prompt and the validator can’t disagree.

An empty response, invalid JSON, a schema failure, a timeout, a rate limit or a missing model each put the item in a review queue with a named reason. Below 0.55 confidence the item keeps its suggested destination and waits for me. From 0.55 up to 0.60 it gets delivered and flagged.

A step that didn’t throw may still not have delivered anything, so a delivered item stores a receipt naming the kind of record it became and that record’s ID, and the queue shows that trail on the item.

Nothing pushes a missing receipt at me, though. A row with no trail just sits in the queue until I look.

When I move an item by hand, one database transaction redelivers it and stamps it as rerouted. Counting those is the closest thing I have to a measure of how often the model got it wrong.

One Contract, Two Clients

The API and the web client share one package of contracts: the types and the runtime checks. The iOS app has hand-written Swift models, and the server can ship ahead of whatever build is on my phone. So selected enums decode an unrecognized value to .unknown. One throw would fail the entire list and leave me looking at an error message.

On the web, if the combined home-page endpoint returns a 404, 405 or 5xx, fails at the network, or sends a body that fails its contract check, the client assembles the page from the older separate reads. The new endpoint is allowed to break. Today still gets built.