Skip to content

feat(web): the homepage ends with test-and-ship and the production checklist - #854

Merged
blove merged 47 commits into
mainfrom
blove/homepage-ship-checklist
Sep 25, 2026
Merged

blove merged 47 commits into
mainfrom
blove/homepage-ship-checklist

Conversation

@blove

@blove blove commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Stacked on #851, whose branch is this PR's base. Once #851 merges, this PR gets rebased onto main.

PR 3 of 3 from docs/superpowers/specs/2026-09-25-homepage-files-tour-design.md. The plan is docs/superpowers/plans/2026-09-25-homepage-files-tour-pr3.md. It adds the last two homepage sections after the route shapes, before the closing call to action.

Test and ship (#test-and-ship): "Test it offline, then pick where it runs."

  • Test replay. npm test -- --reporter=verbose and npx b4 eval stream recorded output. The recordings came from real runs:
    • apps/web/scripts/export-homepage-demos.mjs --record-tests --record-builds scaffolds app-basic with this checkout's create-b4-app.
    • It then runs the real commands with no model key.
    • It keeps only an allow-listed environment and strips only ANSI codes, vitest timing lines and the temp path.
  • What CI pins. CI doesn't re-record; that takes about 22s and may use the registry. Instead, tests pin the recordings to:
    • the template
    • the CLI's eval-reporter and b4 build line templates
    • the docs
  • Deploy targets. The tabs are node, langsmith, hono, vercel and kubernetes.
    • Each target's b4.config.ts is a typechecked fixture that lists only that target, and its b4 build output is recorded.
    • "node and langsmith are the defaults; setting build.targets replaces them" is proven by the recordings.
    • Kubernetes is the node build, then docker build, docker push and helm install, in the docs' order, checked by a test.
    • hono and vercel name the packages the app must also depend on.

The last mile (#last-mile): "The parts you'd write next are already here."

  • Twelve tiles that turn over to show the file, config or command that handles each job.
  • Every excerpt is 1–2 real lines. Each comes from a typechecked fixture or a template src/ file.
  • Every claim is pinned to the source or the docs.
  • No JavaScript: every answer shows. With JavaScript, the tiles turn face down after load, without an announcement.

Spec corrections (details in the plan)

  • Streaming endpoint: it's POST /threads/:thread_id/runs/stream.
  • Retries: they cover model calls failing with a rate limit, a server error or a network error, before anything has streamed.
    • Agent routes always stream, and that path ignores retry.baseDelay, so the tile doesn't show it.
    • Queued separately: reconcile the runtime or /docs/retry.
  • Persistence: it's three Postgres stores. The default SQLite files already survive restarts, so the tile is about sharing threads across servers and replaceable containers.
  • Eval output: b4 eval prints a per-case line with the scorer detail, then a suite line.
  • Other corrections:
    • b4 inspect shows long-term memory, not traces.
    • Without thread-access.ts, thread endpoints are open, so the tile makes no "deny by default" claim.
    • Kubernetes isn't a build target.
    • There's no sandbox network or denylist claim anywhere. The Docker and Kubernetes providers don't enforce the default denylist; that's queued separately too.

UI/UX (ui-ux-pro-max)

  • Controls: native radios for the targets, and <button aria-pressed> tiles with the docs link outside the button. All targets are 44px.
  • Announcements:
    • The replay output is a role="log" with aria-live="off", so the lines aren't read one by one.
    • There's one polite announcement per action, set on the action itself.
    • Announcements are short: the dependency lists are shown but not spoken.
  • Motion:
    • GSAP only animates, and the next action, Skip, unmounting or switching to reduced motion kills it.
    • Reduced motion runs no tweens.
    • The flip is a clipped rotateY with one perspective.
  • No layout shift:
    • Stacked target panels.
    • Tile faces share one grid cell.
    • The replay panel is sized to its final height.
  • Focus is never stranded on an inert face or panel, including WebKit moving focus on mousedown.

Verification

  • Gates: pnpm --dir apps/web test (64 files: 977 passed, 1 skipped), lint, typecheck, node scripts/check-docs.mjs and pnpm check:build-cache all pass.
  • Playwright + axe at 320–1440px, with motion on and reduced, in Chromium and WebKit:
    • no horizontal scroll, and 44px targets
    • 0px layout shift for replays, targets and tiles, measured mid-animation too
    • the flip is clipped inside its tile
    • keyboard-only use of all three demos
    • rapid actions end in the final state, with one announcement each
    • axe finds nothing on the whole page
  • lastmod: only the / entry changes. It will be regenerated again after the rebase onto main, which has since gained fix(web): gate lastmod entries recorded against changed content #850's lastmod check.

New devDependency

@b4run/postgres-storage (workspace:*), so the persistence snippet typechecks.

🤖 Generated with Claude Code

blove and others added 29 commits September 25, 2026 10:48
… shapes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s decisions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…by route discovery

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion, sandbox and delegation code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under load, the act() flush after a pick can take longer than the 200ms
fade, so checking for a still-running tween failed intermittently. The
test now spies on gsap.fromTo; the kill-and-clear checks are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…able traces

The motion test checks each trace was started over its own board (read from
the timelines the island created) rather than that a 180ms tween is still
running after act(), which a slow flush under load can outlast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bash caption claimed the default network policy blocks 169.254.169.254;
Docker's allow mode is a plain bridge network and enforces no denylist. A new
test resolves an app without a network line through the CLI's own
resolveSandboxManager and asserts the keeper container starts with
--network bridge and no 169.254 anywhere, and the caption now says just that.

refund now runs through wrapToolWithApproval, as the runtime serves it:
Allow once returns the tool's { refunded: 500 } and saves nothing; Deny
returns exactly "[B4_E3001] Permission denied by user: tool refund", which
the board quotes. readFile runs through the same pausable graph on the
fixture's sandbox: no interrupt, every docker exec in the keeper container.

The keeper docker run is selected by --name, and the recording sandboxes take
scope and image from the options the fixture config passed to dockerSandbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Safari and Firefox on macOS don't focus a radio when its label is clicked.
With focus on Allow once, Ask again or the caption's docs link, picking a
call that doesn't pause made that control inert and dropped focus to
<body>. When focus is inside the tracer but outside the radio group, the
pick now moves it to the newly checked radio. The focus effect runs as a
layout effect, so focus moves before paint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The motion test checked only that killed traces left no inline opacity. It
now checks transform as well, so a stopTrace that forgot to clear the
y offset fails the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tour

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r on a label click

WebKit moves focus on mousedown to the nearest focusable ancestor of the
clicked label (the page's <main tabindex="-1">) or to <body> before the
radio changes, so picking read after refund left focus on <main>. Treat
focus on an ancestor of the tracer, or none, as stranded too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 375px the four options wrap to two rows; .option + .option shifted the
row-two cell 1px left and the rows' borders doubled. Every cell now
overlaps its right and bottom neighbours by 1px, with the padding giving
the last overlap back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Kubernetes tab went from docker build straight to helm install; it now
runs docker push between them, and the test checks the whole sequence
against the docs page's order. The hono and vercel tabs name the packages
their recorded builds needed (the recorder installs them first), and the
test checks each against the recorder's list, the docs pages and the
scaffold's provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Recorded commands get an allow-listed environment (PATH, HOME, TMPDIR,
  locale, pnpm/npm store and cache paths) instead of a copy of the shell's,
  so no model key, token or tracing setting reaches the scaffold.
- The normaliser strips trailing timings only from vitest's ✓/× result
  lines, and drops only vitest's indented "Start at"/"Duration" lines.
- A failed command says whether it could not spawn, was killed by a
  signal or exited non-zero, instead of "exited null".
- A failed scaffold or install removes its temp directory, and the
  scaffold's own output is in the error rather than discarded.
- Tests pin the b4 build output lines to the command's template literals,
  derive the eval scorer detail and case count from the app-basic
  template, and the header says what to re-record after.

Re-recorded with the new environment: both recordings are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ands

- The replay announcement names the run by its button label ("npm test",
  "b4 eval") rather than the full command; the target announcement drops
  the written files, which the panel shows, so arrowing across the radios
  doesn't queue long texts. A repeat still changes the wording.
- The `$ command` line streams in with the output, so a replay visibly
  starts over; it stays in the DOM for the no-JS page.
- Drop the two class names with no rule behind them, the unused
  container-type on .panel; hover only where hover exists; the prompt
  isn't selectable.
- Tests: reduced motion switching on mid-replay and mid-fade leaves no
  tween and no inline style; focus on the old target's docs link moves to
  the newly checked radio; env stubs are undone in afterEach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The streaming retry only runs before anything has streamed and ignores
baseDelay, so the tile says so and the excerpt drops it. The default
stores already survive a restart; the Postgres tile is about sharing
them. The offline-tests excerpt now comes from the eval, which the
generated app typechecks. Pin the retry classifier and the hasYielded
gate, the three Postgres stores, the streaming route patterns and the
remember/recall tools against their source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Turn the tiles face down in a layout effect, so a client navigation
never paints a frame of open tiles. Apply the perspective once, on
.faces, and clip it there; wrap long answers at 320px; darken the
toggle's rule on hover. Cover a flip stopped by reduced motion or an
unmount, the closing flip, Handled. hiding again below twelve, and a
hydration with no recoverable errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uncement

hono and vercel's summary ended with "The app must also depend on …",
which made their live announcement long every time a visitor arrowed
across the deploy-target radios. Move that sentence into a new
`requires` field that the panel still renders visibly under the
summary, but `describeTarget` (built from `summary` alone) leaves out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
b4-run Ignored Ignored Preview Sep 25, 2026 9:59pm UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

blove and others added 15 commits September 25, 2026 14:27
… shapes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s decisions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…by route discovery

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion, sandbox and delegation code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under load, the act() flush after a pick can take longer than the 200ms
fade, so checking for a still-running tween failed intermittently. The
test now spies on gsap.fromTo; the kill-and-clear checks are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…able traces

The motion test checks each trace was started over its own board (read from
the timelines the island created) rather than that a 180ms tween is still
running after act(), which a slow flush under load can outlast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bash caption claimed the default network policy blocks 169.254.169.254;
Docker's allow mode is a plain bridge network and enforces no denylist. A new
test resolves an app without a network line through the CLI's own
resolveSandboxManager and asserts the keeper container starts with
--network bridge and no 169.254 anywhere, and the caption now says just that.

refund now runs through wrapToolWithApproval, as the runtime serves it:
Allow once returns the tool's { refunded: 500 } and saves nothing; Deny
returns exactly "[B4_E3001] Permission denied by user: tool refund", which
the board quotes. readFile runs through the same pausable graph on the
fixture's sandbox: no interrupt, every docker exec in the keeper container.

The keeper docker run is selected by --name, and the recording sandboxes take
scope and image from the options the fixture config passed to dockerSandbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Safari and Firefox on macOS don't focus a radio when its label is clicked.
With focus on Allow once, Ask again or the caption's docs link, picking a
call that doesn't pause made that control inert and dropped focus to
<body>. When focus is inside the tracer but outside the radio group, the
pick now moves it to the newly checked radio. The focus effect runs as a
layout effect, so focus moves before paint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The motion test checked only that killed traces left no inline opacity. It
now checks transform as well, so a stopTrace that forgot to clear the
y offset fails the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tour

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r on a label click

WebKit moves focus on mousedown to the nearest focusable ancestor of the
clicked label (the page's <main tabindex="-1">) or to <body> before the
radio changes, so picking read after refund left focus on <main>. Treat
focus on an ancestor of the tracer, or none, as stranded too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 375px the four options wrap to two rows; .option + .option shifted the
row-two cell 1px left and the rows' borders doubled. Every cell now
overlaps its right and bottom neighbours by 1px, with the padding giving
the last overlap back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@blove
blove force-pushed the blove/homepage-tracer-shapes-impl branch from 61bb2a2 to 59f6bb7 Compare September 25, 2026 21:29
blove and others added 2 commits September 25, 2026 14:33
…l' into blove/homepage-ship-checklist

# Conflicts:
#	apps/web/app/components/homepage/DeveloperHome.tsx
#	apps/web/app/components/homepage/homepage.test.tsx
#	apps/web/app/seo/lastmod.generated.json
#	apps/web/package.json
#	pnpm-lock.yaml
…ranch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@blove
blove deployed to vercel-preview September 25, 2026 21:35 — with GitHub Actions Active
Base automatically changed from blove/homepage-tracer-shapes-impl to main September 25, 2026 21:49
…hecklist

# Conflicts:
#	apps/web/app/components/homepage/DeveloperHome.tsx
#	apps/web/app/components/homepage/homepage.test.tsx
#	apps/web/app/seo/lastmod.generated.json
#	apps/web/package.json
#	pnpm-lock.yaml
@blove
blove deployed to vercel-preview September 25, 2026 22:01 — with GitHub Actions Active
@blove
blove merged commit 980ba8a into main Sep 25, 2026
30 of 31 checks passed
@blove
blove deleted the blove/homepage-ship-checklist branch September 25, 2026 22:26

This branch was successfully deployed

2 active (1 outdated) deployments
vercel-preview — bb4a7a8b Deployed Sep 25, 2026 by blove via vercel-native #2427
Preview — 8b777a45 Deployed Sep 25, 2026 by vercel[bot]
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