Evidence that requires a click is evidence most readers never see
Decided
/integrations renders a finalised Stripe test invoice on the server, before any interaction, in addition to the interactive runner. The static half also names the mode and prints the test card in the markup rather than in a post-run branch.
Affects: lib/stripe-invoice-view.ts, lib/billing-evidence.ts, components/integrations/invoice-exhibit.tsx, components/integrations/billing-evidence.tsx, components/integrations/billing-runner.tsx, app/integrations/page.tsx, app/api/billing/checkout-session/route.ts
What went wrong
The billing sandbox has worked since 4 August 2026. A review on 5 August read the live page and reported "no visible test invoices or subscription objects" and "no Stripe test card example", and fetching the served HTML proved the reviewer right: the document was 191,955 bytes, it contained the anchor billing-sandbox and the heading "Create a real Stripe Checkout Session", and it contained no 4242, no "Stripe test mode", and no invoice block anywhere. Every one of those was written by React after a button press.
That is a defect, not a misunderstanding. Three separate audiences never press the button: a reader who scrolls past, a screen reader skimming headings, and every crawler, preview build and reviewer working from a fetched page. For all of them the panel was an empty box and a promise — which is the exact posture the sandbox was built to stop taking.
The alternative that was rejected
Running the flow on mount. It would have filled the panel without a click, and it would have been much worse: the POST route approves a quote in a real tenant and finalises a real Stripe invoice, so a crawler, an ISR revalidation and a Lighthouse run would each have minted objects in a real Stripe account. The runner's useEffect deliberately does nothing on mount and still does.
What was done instead
lib/billing-evidence.ts reads an existing invoice back. Every call it makes is a GET: it logs in, lists approved quotes, and reads invoices. It cannot create the evidence it is looking for, which is the property that makes it safe to run on a page render. If it finds nothing it publishes which of seven specific reasons applied — a section whose whole argument is "do not take our word for it" cannot fail silently and leave the reader assuming the feature is decorative.
Two surfaces now render the same kind of document: one that already happened and one the reader causes. That is the situation where two copies of mapping code drift and one quietly starts showing a different number, so the mapping, the money formatter, the instant formatter and the D-081 fitness rule moved into lib/stripe-invoice-view.ts and the markup into a single presentational InvoiceExhibit. The route, the runner and the server exhibit are now three callers of one renderer. A reader who catches those two panels formatting the same document differently would learn something bad about this company, and one renderer is how that stays impossible.
What would have to change for this to be wrong
If the sandbox tenant's newest approved quotes stopped being exhibitable — the D-080 wreckage recurring — the exhibit would print its no_usable_invoice sentence on every render while the runner kept working. That is honest but it is not evidence, and at that point the fix is upstream in the adapter, not here.