feat(demo): a Cloudflare snapshot, so people who are not in the room can open it - #880
Conversation
…can open it The LAN demo covers the people beside you. This covers everyone else: a fully static export of the same app, served from Cloudflare Pages with no API, no PostgreSQL and no Redis behind it. The guided tour is entirely client-side, so it comes along unchanged. Cloudflare was the right instinct for the wrong half of the stack. The NestJS API cannot run on Workers -- sharp is a native binary, bullmq/ioredis need long-lived connections, and there is no managed Postgres -- but 47 of 54 built routes were already prerendered, and the seven dynamic ones are parameterised over a small, known set of synthetic ids. So the presentation half ships to the estate that already exists. Reads are answered from fixtures captured by DRIVING the real demo per persona and recording what the app actually requests. Hand-written fixtures drift silently and render a plausible, wrong screen, which is the worst failure a demo has. Writes are refused in plain language rather than faked, and a path with no fixture says so instead of returning empty data that looks like a real but empty product. Static export required generateStaticParams on the two dynamic segments. Both pages are client components, which cannot carry that export, so it lives on segment layouts -- and the realm chrome moved to RealmChrome.tsx to let its layout be a server component. Outside a snapshot build the helpers return [], leaving those routes rendering on demand exactly as before. Three defects this surfaced, each fixed at its base: - trailingSlash made usePathname report "/tour/" while every registry key is "/tour", so the guided tour vanished on the hosted build. Normalised, with a test. - The capture read response bodies without awaiting them before the next navigation; Playwright discards bodies on navigate, so it silently recorded nothing at all. - A machine-global gitignore rule for `public/` would have dropped every fixture, shipping a snapshot with no data. Overridden in the repo, where the intent belongs. Verified against the built export with the local stack irrelevant: real per-route HTML rather than an SPA fallback (`serve -s` masked exactly that, and made a text assertion pass against the tour panel's own words), the dashboard rendering its synthetic contracts, streak and truth log, the tour panel present on every route, and zero requests to ports 4310/4311. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmP1SLhXcZBmA7g4NM6PTY
|
Warning Review limit reached
Next review available in: 69 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (19)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Reviewer's GuideImplements a Cloudflare-ready static snapshot of the demo by introducing a snapshot mode that serves captured API fixtures, wiring Next.js static export for specific dynamic routes, fixing guided tour trailing-slash routing, and adding scripts plus guards to capture, build, serve, and deploy the snapshot reliably without altering normal demo behavior. Sequence diagram for API requests in Cloudflare snapshot modesequenceDiagram
actor User
participant BrowserApp
participant api_client_request as request
participant snapshot as snapshotRespond
participant BackendAPI
User->>BrowserApp: navigate /dashboard
BrowserApp->>api_client_request: request("/dashboard/summary")
api_client_request->>api_client_request: isSnapshotMode()
alt snapshot mode
api_client_request->>snapshot: snapshotRespond("/dashboard/summary", "GET")
alt fixture found
snapshot-->>api_client_request: { ok: true, data }
api_client_request-->>BrowserApp: data
BrowserApp-->>User: render snapshot data
else missing fixture
snapshot-->>api_client_request: { ok: false, status: 404, message }
api_client_request-->>BrowserApp: ApiError(message, 404)
BrowserApp-->>User: show read-only / missing screen
end
else normal demo
api_client_request->>BackendAPI: HTTP GET /api/dashboard/summary
BackendAPI-->>api_client_request: 200 JSON
api_client_request-->>BrowserApp: data
BrowserApp-->>User: render live data
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
…traps Records what the snapshot deliberately cannot do (writes are refused, not faked) and the two traps that cost real time building it: a snapshot build overwrites .next so the local demo afterwards answers from fixtures instead of calling /api, and previewing with `serve -s` returns the landing page for every route with a 200 while the URL bar still looks correct. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmP1SLhXcZBmA7g4NM6PTY
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The snapshot capture script and snapshot.ts both hard-code the persona identifiers; consider centralising this mapping so adding or renaming a persona can’t diverge between capture and runtime.
- readRegistryRoutes in capture-snapshot.mjs parses the guided tour registry source with regex, which is brittle to formatting/ordering changes; exposing a structured route/persona export from the registry would make the capture more robust.
- setPersona currently accepts any string and persists it, even if there is no corresponding fixture file; you might want to constrain it to SNAPSHOT_PERSONAS to avoid confusing 404s when switching personas.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The snapshot capture script and snapshot.ts both hard-code the persona identifiers; consider centralising this mapping so adding or renaming a persona can’t diverge between capture and runtime.
- readRegistryRoutes in capture-snapshot.mjs parses the guided tour registry source with regex, which is brittle to formatting/ordering changes; exposing a structured route/persona export from the registry would make the capture more robust.
- setPersona currently accepts any string and persists it, even if there is no corresponding fixture file; you might want to constrain it to SNAPSHOT_PERSONAS to avoid confusing 404s when switching personas.
## Individual Comments
### Comment 1
<location path="src/web/services/snapshot.ts" line_range="89-90" />
<code_context>
+ const verb = method.toUpperCase();
+
+ // Sign-in is a persona switch, not an authentication.
+ if (verb === 'POST' && path === '/auth/login') {
+ // A fixed placeholder, not a credential: the snapshot has no auth to grant.
+ return { ok: true, data: { token: 'snapshot-session', userId: getPersona() } as T }; // allow-secret: placeholder session marker
+ }
</code_context>
<issue_to_address>
**issue (bug_risk):** Login handling doesn’t actually switch personas based on the supplied email.
In snapshot mode, `POST /auth/login` ignores the request body and always returns `{ token: 'snapshot-session', userId: getPersona() }`, so logging in with different synthetic emails never changes the active persona unless something else calls `setPersona`. Since `SNAPSHOT_PERSONAS` maps emails to personas, `snapshotRespond` should read the posted email, look up the persona, call `setPersona`, and return a matching `userId`. As-is, the snapshot appears to support multiple accounts but will always show River’s data.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if (verb === 'POST' && path === '/auth/login') { | ||
| // A fixed placeholder, not a credential: the snapshot has no auth to grant. |
There was a problem hiding this comment.
issue (bug_risk): Login handling doesn’t actually switch personas based on the supplied email.
In snapshot mode, POST /auth/login ignores the request body and always returns { token: 'snapshot-session', userId: getPersona() }, so logging in with different synthetic emails never changes the active persona unless something else calls setPersona. Since SNAPSHOT_PERSONAS maps emails to personas, snapshotRespond should read the posted email, look up the persona, call setPersona, and return a matching userId. As-is, the snapshot appears to support multiple accounts but will always show River’s data.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 21bc72d46d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (isSnapshotMode()) { | ||
| const snapshot = await snapshotRespond<T>(path, method); | ||
| if (snapshot.ok) return snapshot.data; | ||
| throw new ApiError(snapshot.message, snapshot.status, null, null); |
There was a problem hiding this comment.
Route every snapshot request through the fixture responder
In the static Cloudflare export, this intercepts only calls made through api-client, while snapshot tour routes such as app/realms/page.tsx, app/realms/[slug]/page.tsx, and app/practitioner/page.tsx still call /api/... with fetch directly. Cloudflare has no API or rewrite layer, so those requests return errors instead of the captured fixture data; the realm detail consequently renders without its captured stats/contracts, and the practitioner chapter renders an API error. The snapshot mode needs to cover these direct callers as well (or migrate them to the fixture-aware client).
Useful? React with 👍 / 👎.
| if (verb === 'POST' && path === '/auth/login') { | ||
| // A fixed placeholder, not a credential: the snapshot has no auth to grant. | ||
| return { ok: true, data: { token: 'snapshot-session', userId: getPersona() } as T }; // allow-secret: placeholder session marker |
There was a problem hiding this comment.
Set the requested persona during snapshot login
When someone signs into the snapshot as Moira, HR, Alecto, or Sage, this branch ignores the submitted email and returns getPersona() without ever calling setPersona; the request body is not even passed to snapshotRespond. Since local storage therefore remains unset, all subsequent reads use River's fixture, making the four other committed persona fixtures unreachable and causing their chapter-specific endpoints to return snapshot 404s.
Useful? React with 👍 / 👎.
| const file = path.join(outDir, `${persona}.json`); | ||
| await writeFile(file, `${JSON.stringify(fixtures, null, 2)}\n`, "utf8"); | ||
| written.push({ persona, routes: routes.length, fixtures: Object.keys(fixtures).length }); |
There was a problem hiding this comment.
Validate captures before overwriting existing fixtures
If a capture produces no API responses—the failure mode explicitly handled below—this write has already replaced the persona's previously valid fixture with {} before the script throws. snapshot.sh build checks only that JSON files exist, not that they contain entries, so a user who retries the build after the failed capture can publish an empty snapshot; collect and validate all persona results before replacing the committed files.
Useful? React with 👍 / 👎.
| 'c1000000-0000-0000-0000-000000000001', | ||
| 'c1000000-0000-0000-0000-000000000002', | ||
| 'c1000000-0000-0000-0000-000000000003', | ||
| 'c1000000-0000-0000-0000-000000000004', |
There was a problem hiding this comment.
Export River's second contract route
River's captured GET /contracts response contains c1000000-0000-0000-0000-00000000000a, and the dashboard maps every returned contract to /contracts/${id}, but this static-parameter list stops at ...0004. In the Cloudflare export, clicking River's second visible contract therefore reaches a route for which no HTML was generated; include ...000a and capture its detail fixture as well.
Useful? React with 👍 / 👎.
| const empty = written.filter((entry) => entry.fixtures === 0); | ||
| if (empty.length) { |
There was a problem hiding this comment.
Reject incomplete persona captures
The success check accepts any persona with one captured response, even when the persona's primary tour route failed. The committed alecto.json, for example, contains only GET /users/me, while /fury immediately requests GET /fury/queue and GET /fury/stats; after persona switching is fixed, both calls receive snapshot 404s and the core auditor chapter shows an empty, disconnected workbench. Validate required endpoints or route failures per persona rather than checking only whether the total is zero.
Useful? React with 👍 / 👎.
| NEXT_PUBLIC_STYX_SNAPSHOT=true \ | ||
| NEXT_PUBLIC_STYX_TEST_MONEY_MODE=true \ | ||
| NEXT_PUBLIC_STYX_PRIVATE_BETA=true \ | ||
| NEXT_PUBLIC_STYX_ENV_LABEL=cloudflare-snapshot \ | ||
| NEXT_PUBLIC_STYX_FEATURE_B2B_HR_UI=true \ |
There was a problem hiding this comment.
Configure hosted feedback separately from the LAN collector
This enables the guided tour in the hosted build through NEXT_PUBLIC_STYX_TEST_MONEY_MODE, but it leaves the tour's feedback client targeting its default same-host port 4312. On an HTTPS Cloudflare Pages URL that resolves to https://<pages-host>:4312, where the presenter's LAN-only collector cannot exist, so all route telemetry is discarded and every remote viewer who submits a visible note receives a failure. Supply a reachable hosted collector URL or disable the feedback controls for snapshot builds.
Useful? React with 👍 / 👎.
Why
The LAN demo covers the people beside you. This covers everyone else — an investor, a remote tester, anyone who just needs to see and understand it.
The Cloudflare question, answered honestly
Cloudflare was the right instinct for the wrong half of the stack. The NestJS API cannot run on Workers —
sharpis a native binary,bullmq/ioredisneed long-lived connections, and Cloudflare has no managed Postgres.But the presentation half ships fine: 47 of 54 built routes were already prerendered, the seven dynamic ones are parameterised over a small known set of synthetic ids, and the guided tour is entirely client-side. So this is a full static export with no API, no database and no Redis behind it.
How the data stays honest
Fixtures are captured by driving the real demo per persona and recording what the app actually requests. Hand-written fixtures drift silently and render a plausible, wrong screen — the worst failure a demo has.
Static export mechanics
generateStaticParamswas required for the two dynamic segments. Both pages are client components, which cannot carry that export, so it lives on segment layouts — and the realm chrome moved toRealmChrome.tsxso its layout can be a server component. Outside a snapshot build the helpers return[], leaving those routes rendering on demand exactly as before.Three defects this surfaced, each fixed at its base
trailingSlashbroke the guided tour on the hosted build.usePathname()reports/tour/while every registry key is/tour, so the panel silently vanished. Normalised, with a test.public/would have dropped every fixture and shipped a snapshot with no data. Overridden in the repo, where the intent belongs.Verification
Against the built export, with the local stack irrelevant:
serve -smasked exactly that, and made a text assertion pass against the tour panel's own words while every route served the landing page./dashboard/renders its synthetic contracts, streak, truth log and test-credit balance.npx tsc --noEmitclean;jest lib/guided-tour7/7.Commands
Deploy is not run by this PR — publishing a public URL is the owner's call.
🤖 Generated with Claude Code
https://claude.ai/code/session_01XmP1SLhXcZBmA7g4NM6PTY
Summary by Sourcery
Introduce a Cloudflare-friendly static snapshot mode of the demo that serves captured fixtures without any backend, including static exports for key dynamic routes and guided tour support.
New Features:
Bug Fixes:
Enhancements:
Build:
Tests:
Chores: