The storage story, the recovery path, and the numbers
A production audit found three shipped sentences that disagreed about where saved work lives, a password reset that was really a support email, a tool FAQ that asked the same generic question on every page, and a monitoring blind spot: nothing in the product could say that a browser was failing. This release answers all four from one place — one contract, one page, one beacon.
- **One data-boundary contract.** `shared/data-boundaries.ts` is now the only place that states where saved work lives: tool inputs and results stay on the device with sync on or off, favorites, recents, and run counts follow the account only while the switch is on, and playground secret values never leave the browser. The tool-page footnote, the account page, the dashboard, the privacy policy, and the homepage read it instead of restating it, and a test fails when a rendered sentence contradicts a row.
- **Self-service account recovery.** Password reset and email verification are real: one-use, hash-only tokens (30 minutes for a reset, 24 hours for a verification), a completed reset revoking every session, and a session list where any entry — including the others, behind the current password — can be ended. With no mail provider configured, both endpoints answer 503 with the honest reason rather than pretending a message was sent. Account export and deletion sit on the same page: the export holds the account record and the synced saved work, never a password hash or a token; deletion removes them.
- **The privacy policy and terms now describe accounts.** Both were rewritten for a paid, account-backed product and read from the boundary contract; the review date moved forward only because the text moved. The audit's own condition is recorded rather than quietly satisfied: final wording still needs qualified counsel review before commercial launch, and `docs/LAUNCH-OPS-CHECKLIST.md` lists that as a blocking step alongside DNS, Search Console, and mail authentication.
- **FAQs that answer their own tool's question.** The generic professional-advice question now appears only on higher-stakes workbenches; every other tool's FAQ is derived from its own record — whether an input leaves the device, which fields have hard limits, what the tool does not do. The FAQPage rich-result block was removed from the tool page and the shared graph, because the rich result was retired and the questions stay on the page regardless.
- **Client error monitoring and Core Web Vitals.** Two endpoints that always answer 204 record what a browser can safely report: a route shape, an error class name, and a scope for failures; a value and a rating for LCP, CLS, INP, TTFB, and FCP. No message, stack, query string, or identity is sent or stored, repeats collapse into a count per minute per route shape, and the limiter keys on an in-memory salted hash. A test proves against real PostgreSQL that nothing typed can appear in the table.
- **A catalogue whose filters are links.** `/tools` reads `?q=`, `?family=`, `?page=`, and `?saved=1` from the address, so a narrowed view can be sent to someone; filtered and searched views are noindex,follow while the unfiltered catalogue stays indexable, and the saved-tools share link no longer carries a list of slugs.
- **Long result tables on a phone.** A schedule wider than the viewport now has a card view (one labelled row at a time), a sticky first column, a scroll affordance, and the CSV export that already existed — because horizontal scrolling was the one gap the accessibility statement named.
- **Smaller and duller, which the audit asked for.** `robots.txt` no longer blocks `/_next/` — Google needs the CSS and JavaScript it serves — the tool-page ordinal and the tool registry vocabulary are gone from public copy, sitemap `lastmod` reads the shared publishing record so every published tool carries the date that exists and none is invented, the installed app is named UtilityForge and paints the same colour as the app token, and the CSP no longer allows Google Fonts or a blanket https connect-src outside the one browser-direct route that needs it.
- **Workflow power, in the free workbenches.** A result can be handed to another local workbench without going through the clipboard or the address bar — the value travels inside the tab, is read once, and is deleted. A workbench can be run down a pasted column (200 rows, refused rather than truncated past that, CSV out, one row per value with failures isolated to their own row). The command palette gained two local actions beside the existing ones, and the request builder now imports a curl command or an OpenAPI document, keeps named drafts in this browser, and diffs a response against the previous send. None of it is gated behind Pro: the audit's own constraint was that no local tool is ever paywalled, and these are the local tools' conveniences.
- **A recovery path that actually resolves.** `/reset-password`, `/verify-email`, `/account/billing`, and share links were client routes with no server-side route entry, so the catch-all answered 404 to anyone who followed an emailed link — the one place where a wrong sentence and a wrong status code are the same size of problem. `seo-contract.test.ts` now reads the app's own route table and fails if a literal route resolves to not-found, which is the check that would have caught it.
- **Embeddable workbenches, and a workspace you can carry.** `/embed/<slug>` frames a local workbench with its processing declaration and one link back: LOCAL processing only, never a higher-stakes health or finance workbench, noindex, canonical to the full page, and no handoff control — a framed page must not route itself into a page that refuses to be framed. The route is now rendered through `react-dom/server` in a test, because the first version of it answered 500 to every request: the shared workbench reads the router's location, and outside a router that read falls back to a browser global the server does not have. The embed supplies a context that is honest in both places and opens links in a new tab. The dashboard can export and import the whole local workspace as JSON — it merges rather than replaces, and a secret value is never in the file.