Skip to content

refactor(phase0): gate-only pages become shells - #198

Closed
adamXbot wants to merge 2 commits into
mainfrom
feat/phase0-batch3
Closed

refactor(phase0): gate-only pages become shells#198
adamXbot wants to merge 2 commits into
mainfrom
feat/phase0-batch3

Conversation

@adamXbot

Copy link
Copy Markdown
Collaborator

What

Batch 3 of Rust-core Phase 018 of 35 pages converted. Stacked on #197 (base is main; merge #197 first). This batch is the clean boundary of "pages whose only server work was a gate" — no data reads at all:

  • Flag-gated content/dashboard/about/ai-disclosure and /help/export-app-list swap resolveFlagFromDb + notFound() for batch 2's RequireFlagGate.
  • /dashboard/settings/focus-matrix built its 220-row seed from HARD_DEFAULTS in the server component. That moves into FocusFlagMatrix, which already imported the same modulefeature-flag-rules is the client-safe pure-data half of lib/. The prop stays optional for injected rows.
  • /onboard/goals was a server redirect() alias for /welcome; a static export can't serve one, so it forwards client-side like the root page.
  • diagnostics / about / help/focus only carried force-dynamic. (help/focus turned out to have no force-dynamic at all — my first grep matched the string inside a comment.)

Two findings worth your attention

Both are recorded in the ledger's header comment rather than left to ambush the final batch:

  1. Dropping force-dynamic does not make a route static today. The root layout calls headers() (CSP nonce) and cookies() (locale), which forces the whole tree dynamic — I checked the build's route table and every route is ƒ, including /about. So these removals are currently preparatory: the flip happens all at once when the layout converts. The ledger test, not the build output, is the per-page proof until then.
  2. generateMetadata still calls getTranslations, and the locale is per-request from the NEXT_LOCALE cookie (i18n.ts). A statically prerendered page would bake the English title while the body renders translated. Page titles therefore move to the client with the layout batch — flagged now so it's a planned step, not a late surprise.

The ledger caught its own bug

The redirect() assertion matched the call text, so my doc comment explaining what a shell replaced tripped it — the same prose-matching trap the lib/ check already guarded against. It now matches the import specifier from next/navigation (redirect / permanentRedirect). Re-proved fail-able: injecting a real redirect import fires it; comment prose is clean.

Verification

tsc clean · biome clean · unit 469 pass / 0 fail · full Playwright 46 passed · visual 13/13 pixel-identical vs stash-captured pre-conversion baselines. Hand-checked where no e2e reaches: both gated pages render through the gate, and the focus matrix still lists 220 rows across 18 surface groups (matching the flag registry).

Next

Batch 4 is the pages needing new read endpoints (stats, privacy map, shortlist, review-recommendations, settings/layout, onboard/welcome family), then the big three (dashboard, app detail, apps grid), then the layout + CSP batch — which comes to you for explicit sign-off.

🤖 Generated with Claude Code

adamXbot and others added 2 commits August 16, 2026 02:11
…come shells

Batch 2 of the Rust-core Phase 0 page conversions (11/35 done). These
four pages shared two server concerns, and both now come from the API:

- Flag gates. `resolveFlagFromDb(key) !== "on" → notFound()` becomes
  RequireFlagGate, which reads the resolved value from
  GET /api/feature-flags (the `flags[].currentValue` field — the same
  resolver output). It fails CLOSED: a failed flag read renders the 404
  rather than the page, because these gates hide surfaces that
  shouldn't exist for the install. notFound() is thrown during render
  (not from the effect), which is the only way it works client-side.
- Initial data. Every one of these lists already had an exact API twin,
  so no new endpoints were needed:
    devices      → /api/devices (already includes appCount, and
                   DevicesView already fetched it in refresh())
    manual apps  → /api/manual-apps (already returned the exact
                   { apps, sources } pair the page assembled by hand)
    focus editor → /api/focus (already returned all six fields; the
                   new FocusEditLoader maps them and holds the form
                   back until they land, since the form stages edits
                   from its initial props)
    changelog    → /api/apps for the filter dropdown

The `initialX` props stay optional on DevicesView / ManualAppsView /
UniversalChangelogView so their Storybook stories keep seeding
fixtures; in Storybook the mount fetch simply fails and the seeded
state stands.

Verified by hand where no e2e covers it: both gated pages render with
their data through the gate, the manual-app form still defaults to
Safari web app (sources[0], unchanged — both create paths reset from
live sources), and flipping flag.page.manual_apps off renders the real
404 page.

Gate: tsc clean; biome clean; unit 462 pass / 0 fail; full Playwright
46 passed; visual net 13/13 pixel-identical against pre-conversion
baselines captured via stash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batch 3 of the Rust-core Phase 0 conversions (18/35 done). This batch is
the pages whose only server work was a gate — no data reads at all:

- Flag-gated content: /dashboard/about/ai-disclosure and
  /help/export-app-list swap resolveFlagFromDb + notFound() for
  RequireFlagGate (batch 2's component).
- /dashboard/settings/focus-matrix built its 220-row seed from
  HARD_DEFAULTS in the server component. That list moves into
  FocusFlagMatrix, which already imported the same module —
  feature-flag-rules is the client-safe pure-data half of lib/. The prop
  stays optional so callers can still inject rows.
- /onboard/goals was a server redirect() alias for /welcome. A static
  export can't serve one, so it forwards client-side like the root page.
- diagnostics / about / help/focus only carried force-dynamic.

Two findings recorded in the ledger's header comment rather than left to
surprise the final batch:

- Dropping force-dynamic does NOT make a route static today. The root
  layout calls headers() (CSP nonce) and cookies() (locale), which
  forces the whole tree dynamic — `next build` shows every route as ƒ,
  verified. The ledger, not the build output, is the per-page proof
  until the layout converts.
- generateMetadata still calls getTranslations, and the locale comes
  per-request from the NEXT_LOCALE cookie (i18n.ts). A statically
  prerendered page would bake the English title while the body renders
  translated, so page titles move to the client with the layout batch.

The ledger's redirect assertion was matching the call text, so a comment
explaining what a shell replaced tripped it. It now matches the import
specifier from next/navigation (redirect / permanentRedirect), the same
discipline the lib/ check already used. Re-proved fail-able by injecting
a real `redirect` import: fires; comment prose: clean.

Gate: tsc clean; biome clean; unit 469 pass / 0 fail; full Playwright
46 passed; visual 13/13 pixel-identical vs pre-conversion baselines.
Hand-checked (no e2e covers them): both gated pages render, and the
focus matrix still lists 220 rows across 18 surface groups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@adamXbot

Copy link
Copy Markdown
Collaborator Author

Superseded by #199, which consolidates Phase 0 batches 2–6 into a single PR (32 of 35 pages). This branch's commits are all contained in that one — closing to avoid a partial merge landing ahead of it.

@adamXbot adamXbot closed this Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant