deckhearth/.convoys/ship-readiness.md
Randall Stillwell eefc390636 docs: post-convoy cleanup for adopt-playwright-smoke
Reflects the merged adopt-playwright-smoke convoy (PR #18, squash
commit 7b6f751) in repo documentation. Closes the test-infrastructure
side of P1 #10 step 2 (launch sequence step 10). One commit in the
convoy: Brief 1, with two small lint-baseline-preserving deviations
from the brief's verbatim shape that the implementer report flagged.

.convoys/adopt-playwright-smoke.md:
  - frontmatter status: in-progress -> shipped (added shipped:
    2026-05-24)
  - new ## As-shipped section: operator-ratified Decisions (D1 keep
    .ts, D4 defer baselines, D6 simple scripts); two implementer
    deviations (removed unused eslint-disable-no-console directive
    that would have regressed lint 128 -> 129; placed @playwright/test
    first in devDeps for alphabetical correctness); cross-validation
    that smoke test 2 ("sign-in page renders") locks in PR #15's
    logged-out CTA work in components/Layout.js; empirical metrics
    from post-merge run 26376162598 (59s workflow, 3/3 in 2.9s,
    0-leak); operator-action-required note pointing at the queued
    seed-visual-baselines-on-linux follow-up; What did NOT change
    audit trail.

.convoys/ship-readiness.md:
  - Queued convoys: new entry seed-visual-baselines-on-linux (Linux-
    Docker baseline generation per Decision 4 + Boot-the-brief
    Finding 7; Mac-generated baselines would silently overwrite Linux
    CI baselines because the custom snapshotPathTemplate has no
    {platform} token).
  - Queued convoys: new RESOLVED block for adopt-playwright-smoke
    (PR #18, 7b6f751) — as-shipped surface, implementer deviations,
    empirical metrics (59s workflow / 3/3 in 2.9s / 0 secret leaks),
    the PR #15 cross-validation finding, operator-action-required
    going forward (the seed-visual-baselines-on-linux follow-up),
    flagged-but-deferred items, and ownership trail (3 architect-
    self-ratified decisions + 3 operator-ratified).
  - Launch sequence step 10: marked RESOLVED 2026-05-24 with the
    commit + metrics inline.
  - P1 #10 No tests Fix sequence: step 2 marked RESOLVED with the
    convoy + metrics ref; step 3 (re-enable test: job in
    ci.yml) called out as the next remaining task; step 5 (wire
    preview-smoke.yml to the Vercel preview URL) marked RESOLVED
    across PR #17 + PR #18 since both contributed.

AGENTS.md:
  - Section 6 Testing: rewritten end-to-end. Was "E2E/smoke runner
    still pending"; is now "@playwright/test@^1.60.0 wired, two
    projects (smoke + visual), npm run test:smoke / test:visual /
    test:visual:update". Documents the local-run convention (boot
    next dev separately, then BASE_URL=... npm run test:smoke);
    the one-time npx playwright install --with-deps chromium step;
    the no-baselines-yet state + the Linux-Docker seed command +
    the cross-platform mismatch reason (no {platform} token in
    snapshotPathTemplate); the CI behavior split (vitest blocking,
    smoke on every PR with pipeline:skip-smoke escape hatch,
    Screenshot diff path-filtered with the first-red-on-missing-
    baseline state documented). Updates vitest coverage count
    16 -> 21 (the +5 Layout regression-lock tests from PR #15).
    Notes TESTING_GUIDE.md is being eclipsed and will be renamed
    to docs/MANUAL_QA.md in a future cleanup convoy.
  - Section 7 Deployment: rewrites the Vercel-bypass paragraph from
    a single "query param now / header reserved for future" bullet
    into a two-shape audit ((1) query param on the wait-action's
    path: input per PR #17; (2) HTTP header in playwright.config.js's
    use.extraHTTPHeaders per PR #18). Documents the Decision-2
    fail-loud-in-CI / warn-in-dev predicate and references Gotcha #12
    as the established precedent (lib/rate-limit.js). Picked Section
    7 over a new Gotcha because the bypass plumbing is operationally
    a deployment concern, not an app pitfall.

No changes to: package.json, package-lock.json, playwright.config.js,
eslint.config.mjs, lib/**, pages/**, components/**, scripts/**,
.github/workflows/**, .cursor/rules/**, README.md,
tests/visual/homepage.spec.ts (JSDoc is already neutral-tense, no
future-tense references to clean up).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 19:33:43 -05:00

350 lines
47 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: ship-readiness
classification: epic
success_metric: tcg-vault is safe to expose to anonymous internet traffic with a documented launch checklist green
skip: []
status: open
created: 2026-05-22
---
# Ship-readiness convoy
Umbrella convoy capturing the full agent-pipeline review of tcg-vault as of 2026-05-22. Findings are grouped by L2 role lens (Reviewer / Architect / Design-system / A11y / IA / Doc-writer) and severity. Each item points to the convoy that will execute the fix.
Code graph: 122 files, 628 nodes, 5602 edges, 11 communities. Indexed by `user-code-review-graph` MCP.
## P0 — ship-blockers (security)
These MUST land before any anonymous traffic touches the production URL.
### 1. `getUserFromRequest` returns a hardcoded admin when no Bearer token is present — **RESOLVED 2026-05-23**
- **Resolved by:** `fix-auth-bypass` Brief 2, commit `258e479` (PR #8). Follow-up Brief 6 hotfix `1fca3aa` added explicit 401 guards to the cards-collection POST/PUT/DELETE branches that previously masked the bug as 500s.
- **File:** `lib/permission-middleware.js` lines 13-17.
- **Impact:** Every API route that calls `getUserFromRequest` (30+ handlers — see `user-code-review-graph` cross-community edges from `api-handler``lib-admin`) accepts unauthenticated requests as admin user 1.
- **Repro:** `curl https://<host>/api/collections` with no `Authorization` header returns admin's collections.
- **Fix:** Delete lines 13-17. Return `null` when no Bearer token. Update every caller to handle `null` properly (most already do; the broken fallback was masking the right path).
- **As-shipped:** The helper now returns `null` for any unauthenticated request. 16 unit tests in `test/lib/permission-middleware.test.js` lock in the contract (including a negative regression against the old synthetic-admin shape). `pages/api/auth/verify.js` returns 401 on the no-token branch instead of fetching the seed admin row.
- **Owns:** `role-architect` + `role-implementer` (one PR; small surface area in the helper, callers already check `!user`).
### 2. JWT_SECRET hardcoded fallback in 7 files — **RESOLVED 2026-05-23**
- **Resolved by:** `fix-auth-bypass` Brief 1, commit `4a10dce` (PR #7).
- **Files:**
- `pages/api/auth-utils.js` (`'your-secret-key'`)
- `pages/api/auth/login.js`, `pages/api/auth/register.js`, `pages/api/auth/verify.js`
- `pages/api/favorites.js`, `pages/api/users/search.js`
- `lib/permission-middleware.js`
- **Impact:** If `JWT_SECRET` env var is unset (e.g. preview/staging misconfig), tokens are signed with `'your-secret-key-change-in-production'` — an attacker can sign their own admin token in 5 seconds.
- **Fix:** Centralize JWT_SECRET access in one helper that `throw`s at module load if `process.env.JWT_SECRET` is unset. Every other file imports from there.
- **Bonus:** Token expiry is inconsistent (`/api/auth/login.js` uses 24h, `pages/api/auth-utils.js` uses 7d). Pick one.
- **As-shipped:** `lib/auth-secret.js` is the single source of truth and throws at module load if `JWT_SECRET` is unset. Canonical TTL is `JWT_TOKEN_TTL = '24h'`. All 7 literal fallback sites are converted to import-and-throw. `test/lib/auth-secret.test.js` (3 tests) covers the fail-loud path.
- **Owns:** `role-architect` + `role-implementer`.
### 3. Default admin credentials in seed + README — **RESOLVED 2026-05-23**
- **Resolved by:** `drop-public-setup` Brief 1 (commit `ff80753`) + Brief 2 (commit `b63b509`). PR #13.
- **Files:**
- `scripts/setup-neon-db.js` lines 130-138 — creates `admin@tcgvault.com` / `admin123`
- `README.md` documents the credentials
- `pages/api/setup-database.js` — duplicates the setup AND is an UNAUTHENTICATED public POST endpoint with `Access-Control-Allow-Origin: *`
- **Impact:** Anyone who hits `/api/setup-database` can re-trigger DDL. The `admin123` password is one Google away from public knowledge.
- **Fix:**
1. Delete `pages/api/setup-database.js`. Schema setup is a one-time job; it should not be a route.
2. Change `setup-neon-db.js` to require a `ADMIN_INITIAL_PASSWORD` env var (no default).
3. Strip the admin password from README — replace with "run `npm run setup-db` and follow the prompt".
- **As-shipped:**
1. `pages/api/setup-database.js` already deleted by `fix-auth-bypass` Brief 3 (commit `fc0dd73`); the `forbidden-endpoints` CI job blocks re-introduction.
2. `scripts/setup-neon-db.js` now reads `ADMIN_INITIAL_PASSWORD` from `process.env`; if unset or empty, the script writes an actionable error (names the env var, points at `.env.local`, suggests `openssl rand -base64 24`, mentions CI-secret alternative, references README) and exits with code 1 **before** opening any DB connection. The bcrypt input is the env-var value, not the literal `admin123`. The two `console.log` lines that previously echoed `Admin User: admin@tcgvault.com` + `Admin Password: admin123` are deleted (R3 — stdout-leak prevention into CI logs); replaced with a single `Admin user ready (email: admin@tcgvault.com)` line that does NOT echo the password.
3. `README.md`'s "Default Admin Account" section replaced with "First-time admin setup" copy that documents the env-var requirement, the `openssl rand -base64 24` generation tip, the CI-secret alternative, and an operator-rotation note for envs that pre-date this convoy.
4. **Bonus (Decision D, Brief 2):** `scripts/setup-neon-db.js` converted from CommonJS to ESM so `npm run setup-db` actually executes on Node 22.x. The `bump-next-js` convoy added `"type": "module"` to `package.json` for ESLint v9 flat config; the seed script's `require()` calls were silently broken since that landed. Without Brief 2, Brief 1's env-var gate would have been theatrical (script throws `ReferenceError` before reaching the gate).
- **Operator caveat (R1, Decision A — going-forward only):** the seed is idempotent (`ON CONFLICT (email) DO NOTHING`); re-running `npm run setup-db` on an env that already has the admin row does NOT rotate the password. Any deployed env that ran setup before this convoy still has the weak `admin123` hash in its DB — operators must rotate manually via the app's profile settings, or wait for the queued `rotate-default-admin` follow-up convoy. Documented in `AGENTS.md` Gotcha #4 and the README's First-time admin setup blockquote.
- **Sibling weak-cred references deferred:** `scripts/reset-db.js`, `scripts/create-test-users.js`, and `TESTING_GUIDE.md` still hardcode `admin@tcgvault.com` / `admin123` — out of scope here per the no-go-zones rule (historical scripts) and the convoy spec. Queued for `purge-weak-creds-from-helpers` follow-up (or fold into `pick-a-name` since the email is also changing).
- **Owns:** `role-implementer`.
### 4. Dev-only test endpoints shipped to production — **RESOLVED 2026-05-23**
- **Resolved by:** `fix-auth-bypass` Brief 3, commit `fc0dd73` (PR #6).
- **Files:** `pages/api/simple.js`, `pages/api/test-auth.js`, `pages/api/test-db.js`, `pages/api/setup-database.js`.
- **Impact:** Unknown — depends on what they expose. `/api/test-db` likely returns the DB connection string; `/api/test-auth` may leak token-handling details.
- **Fix:** Delete all four. Add a CI grep that fails the build if any file matching `pages/api/(test-|simple|setup-)*.js` exists.
- **As-shipped:** All four files deleted. `.github/workflows/ci.yml` has a new `forbidden-endpoints` job (blocking) that fails the build if any of the four paths reappear OR if a new `pages/api/test-*.js` file is added. Local simulation in the implementer PR confirmed clean → OK, with `test-fake.js` → FAIL, post-cleanup → OK.
- **Owns:** `role-implementer`.
### 5. CORS `Access-Control-Allow-Origin: *` on auth endpoints — **PARTIAL 2026-05-23**
- **Partially resolved by:** `fix-auth-bypass` Brief 4, commit `297afca` (PR #9). Login + register only; `pages/api/auth/verify.js` is **deferred** to the queued `cors-tighten` follow-up convoy.
- **Files:** at minimum `pages/api/auth/login.js`, `pages/api/auth/register.js`, `pages/api/setup-database.js` (verify others).
- **Impact:** Any origin can submit credentials. Combined with the no-rate-limit problem below, credential stuffing is wide open.
- **Fix:** Set `Access-Control-Allow-Origin` to the literal frontend origin (`https://tcgvault.com` / preview domain), or remove the header entirely if the API and the frontend are same-origin (they are, on Vercel).
- **As-shipped:** `pages/api/auth/login.js` and `pages/api/auth/register.js` drop the four `setHeader` calls + the OPTIONS preflight handler. `pages/api/setup-database.js` was deleted entirely by Brief 3. `pages/api/auth/verify.js` still has the wildcard header — see follow-up convoy `cors-tighten`.
- **Owns:** `role-implementer`.
### 6. No rate limiting anywhere — **PARTIAL 2026-05-23**
- **Partially resolved by:** `fix-auth-bypass` Brief 4, commit `297afca` (PR #9). Login + register only; the rest of the listed endpoints are **deferred** to the queued `add-rate-limiting` convoy.
- **Impact:** Login endpoint accepts unlimited attempts; card-search endpoint can be hammered; image upload endpoints can be exhausted. The `pages/api/cards/import-*.js` endpoints externally hit Scryfall/Pokémon APIs with no caller throttling.
- **Fix:** Adopt `@upstash/ratelimit` (free tier covers a small launch) or Vercel's built-in middleware-based rate limiting. Apply to: `/api/auth/login`, `/api/auth/register`, `/api/users/search`, `/api/cards/search`, all `/api/cards/import-*`, and `/api/user/avatar*` (upload).
- **As-shipped:** `lib/rate-limit.js` (new) provides `checkAuthRateLimit(req)` via `@upstash/ratelimit@^2.0.8` + `@upstash/redis@^1.38.0` (5 attempts / 15-min sliding window per IP). Wired into login + register. Env vars are `KV_REST_API_URL` / `KV_REST_API_TOKEN` (auto-provisioned by Vercel's Upstash Marketplace integration — note this is a rename from the brief's original `UPSTASH_REDIS_REST_*` spec; see `.convoys/fix-auth-bypass/brief-4-tighten-auth-surface.md` § Post-merge addendum). Fails closed in prod when env vars are unset; warn-and-no-ops in dev. Search / import / avatar endpoints are unchanged.
- **Owns:** `role-architect` (pattern) → `role-implementer` (per-route).
### 7. Layout default-prop leaks maintainer email — **RESOLVED 2026-05-24**
- **Resolved by:** `fix-layout-default-user` convoy (PR #15, squash commit `ca302a8`). Brief 1 (pre-squash `ddf8fd2`) shipped the Layout default-null + logged-out branch + vitest lock-in; Brief 2 (pre-squash `8c7d127`, rebased to `0f6bfbb` pre-merge) swept the 7 pages that needed page-level fixes.
- **File:** `components/Layout.js` line 562: `function Layout({ children, user = { email: 'me@randallstillwell.com', role: 'user' }, ... })`.
- **Impact:** Any page that renders Layout without passing a `user` prop displays your real email and impersonates you as the logged-in user.
- **Fix:** Default `user = null` and render a logged-out state branch. Verify every page passes `user` explicitly (the graph shows ~13 pages call `Layout`; audit each).
- **As-shipped:**
1. `components/Layout.js` default prop changed from hardcoded `{ email: 'me@randallstillwell.com', role: 'user' }` to `null`. `UserProfileDropdown` now branches on `user === null` and renders a `<Link href="/login">Sign in</Link>` CTA in place of the avatar + email + dropdown menu (`NavigationContent`'s `authenticatedNavigation` / `myCollectionNavigation` / `adminNavigation` were already null-safe via existing optional chains; no change there).
2. **7 pages swept** (Brief 2, 11 `<Layout>` call sites total). `pages/scanner.js` (×1), `pages/decks.js` (×3), `pages/deck-builder.js` (×4), `pages/deck/[id].js` (×3) now pass `user={user}` explicitly. `pages/profile.js` and `pages/settings.js` replaced their leaky `useState({ email: 'me@randallstillwell.com', role: 'admin' })` initializer with `useState(null)` (15 sync `user.*` reads in profile + 1 in settings got null-guards). `pages/card/[id].js` replaced its hardcoded `const user = { email: 'me@…', role: 'user' }` with `const { user } = useAuth()` from `lib/use-auth.js`.
3. **10 pages already correct** (architect's per-page audit, Decision B in `.convoys/fix-layout-default-user.md`): `dashboard`, `my-cards`, `cards`, `collections`, `collection/[identifier]`, `community/collections`, `admin/card-import`, `admin/card-editor`, `invite/accept`, `invite/decline`. No changes there.
4. **Test coverage:** `test/components/Layout.test.js` (new) adds 5 regression-lock assertions — no maintainer email when `user` is `null`/omitted; "Sign in" link present when logged out; supplied email renders when supplied; no accidental `Guest` placeholder. Vitest 21/21 green at merge (16 pre-existing auth tests still green).
5. **New devDeps:** `jsdom@^29` + `@testing-library/react@^16` (test-only). `vitest.config.js` got a 3-line `esbuild` block to parse JSX in `.js` files (per-file `// @vitest-environment jsdom` directive — no global env change).
6. **Verification at merge:** `rg 'me@randallstillwell.com' pages/` → 0 hits; anonymous `curl /cards` returned HTTP 200 with no maintainer email; lint baseline match (128 problems, unchanged); CI Aggregate gate / Lint / Vitest / Vercel preview / forbidden-endpoints all green. `Playwright smoke` + `Screenshot diff` red but for an unrelated CI-infra reason — see CI infrastructure side-effect note below.
- **Flagged-but-deferred** (deliberately out of scope per the convoy spec):
1. 4 pages still import `useAuth` from `lib/auth-context.js` (`pages/scanner.js`, `pages/decks.js`, `pages/deck-builder.js`, `pages/deck/[id].js`) — collapsing the three parallel client-side auth surfaces is the queued `single-auth-provider` convoy (P1 #9 in this file), not this one.
2. `components/MobileNavigation.js` still receives a dead `user` prop (it accepts `{ user, onMenuOpen }` but never reads `user.*` — the bottom-bar items are static). Queued as `cleanup-mobile-nav-dead-props` (or fold into `god-component-split` if that lands first).
3. `pages/card/[id].js` still imports `useIsAdmin` from `lib/admin-auth.js` — third parallel auth surface; same `single-auth-provider` convoy will collapse it.
- **CI infrastructure side-effect (not part of this convoy).** PR #16 (squash commit `7e97254`) landed alongside as a CI permissions fix, adding scoped `permissions:` blocks to `.github/workflows/preview-smoke.yml` + `.github/workflows/visual-diff.yml`. That fixed the 5-second 403 "Resource not accessible by integration" failure on both workflows but exposed a second issue: with permissions correct, both now reach the actual deployment check and 10-min-timeout against Vercel Deployment Protection's 401 SSO challenge (anonymous GitHub runner GETs the preview URL). New queued convoy `fix-vercel-deployment-protection-in-ci` (`.convoys/fix-vercel-deployment-protection-in-ci.md`) tracks that follow-up.
- **Owns:** `role-implementer`.
### 8. Next.js 15.4.3 — Vercel platform blocks deploys (vulnerable version) — **RESOLVED 2026-05-23**
- **Resolved by:** `bump-next-js` convoy, single-brief PR commit `e57ea17` ("bump: next 15.4.3 -> 16.2.6, ESLint flat config (v9 fallback), typescript devDep"). The Vercel platform gate cleared with the first successful deploy on the same date; every subsequent PR (`fix-auth-bypass`, `drop-public-setup`, `fix-layout-default-user`, the CI permissions fix) has had a green Vercel preview.
- **Discovered:** 2026-05-22 during the bootstrap PR CI run. Vercel build completes successfully (~29s) but the deployment exits with status `Error` and `"Vulnerable version of Next.js detected, please update immediately"`.
- **Files:** `package.json` line 22 (`"next": "^15.4.2"` → locked at `15.4.3`), `package-lock.json`.
- **Impact:** **Vercel will not deploy any branch — including `main` — until Next.js is bumped.** Preview URLs are unavailable, which means `preview-smoke.yml` and `visual-diff.yml` can't fire. The last successful deploy on `main` was 2025-08-01; production may already be running an outdated build.
- **CVE context:** Next.js shipped a middleware auth-bypass advisory (CVE-2025-29927) patched in 15.2.3, plus subsequent advisories. The exact CVE Vercel is flagging on 15.4.3 needs confirmation via `npm audit` and the Next.js security advisory page.
- **Fix:** Bump `next` to the latest secure 15.x (`npm install next@^15.5` and run smoke tests) OR the latest 16.x (`next@^16.2.6` — major bump; review breaking changes in [Next.js 16 release notes](https://nextjs.org/blog/next-16)).
- **As-shipped (Decision A in `.convoys/bump-next-js.md` — leapfrog to 16):**
1. `next`: `^15.4.2``^16.2.6` (resolves to `16.2.6`).
2. `eslint-config-next`: `15.4.2``^16.2.6`. Config migrated from `.eslintrc.json` to `eslint.config.mjs` (eslint-config-next@16 is flat-config-only).
3. `eslint`: `^8``^9.39.4` (Decision D fallback — v10 surfaced Risk R15 empirically because `@typescript-eslint/scope-manager@8.59.4` bundled by `eslint-config-next@16` doesn't implement v10's new `addGlobals` API; v10 adoption deferred to a separate `bump-eslint-10` convoy, upstream-blocked on typescript-eslint).
4. `typescript`: newly added at `^5.9.3` as a devDep (Decision C — required by the typescript-eslint chain regardless of ESLint major; no project source migration to TS).
5. `scripts.lint`: `"next lint"``"eslint ."` (next lint removed in 16). Lint baseline grew from ~100 to **128 problems** (81 errors, 47 warnings) due to `eslint-plugin-react-hooks@7.1.1` + `@next/eslint-plugin-next@16.2.6` rule additions; CI tolerates this via the `|| true` wrapper in `.github/workflows/ci.yml` per P1 #11.5 (`fix-lint-baseline`).
6. `next.config.js`: `images.domains``images.remotePatterns` (deprecated and removed in 16; preserves Scryfall, Pokémon TCG, Lorcana API hosts for eventual `next/image` adoption).
7. Verification at merge: `npm install` clean (no ERESOLVE), `npm run build` exit 0 with Turbopack (~1.4s compile, 23 static pages + 47 API routes), first green Vercel deploy on `main` since 2025-08-01.
- **Side-effects (deliberately deferred, not part of this convoy):**
- `bump-react` (React 18 → 19) — held until 18.x EOL or until a feature needs it.
- App Router migration — multi-month effort; queued indefinitely.
- `adopt-vitest` ✅ shipped as `fix-auth-bypass` Brief 5; `adopt-playwright-smoke` partially shipped via the Vercel-bound workflows (CI infra now blocked by `fix-vercel-deployment-protection-in-ci`).
- `fix-lint-baseline` (P1 #11.5) — drop the CI `|| true` wrapper once the 128-problem baseline is cleared.
- `bump-eslint-10` + `bump-typescript-6` — upstream-blocked on typescript-eslint shipping v10-tested releases.
- **Doc drift note:** this resolution was applied as part of the `fix-layout-default-user` post-convoy cleanup (commit reflecting `b7ddd08`'s sibling) — the `bump-next-js` convoy never ran a dedicated doc-writer pass, so this RESOLVED entry was added ~24h after the fix actually shipped.
- **Owns:** `role-architect` (pick target version + assess breaking changes) → `role-implementer` (bump + verify dev/build/start + smoke).
- **Convoy:** `bump-next-js` — ran before `fix-auth-bypass`. **Without this convoy, every L3 gate that depends on a Vercel preview was non-functional.**
## P1 — pre-launch quality bar
### 8. Two SQL clients in parallel (`@neondatabase/serverless` + `@vercel/postgres`)
- **Impact:** Two different param-handling APIs, two different transaction stories, two different connection-pool stories. Plus `lib/database.js`'s manual interpolation + `sql.unsafe(query)` is a SQL-injection vector if any caller passes user input through.
- **Fix:** Pick `@vercel/postgres` (tagged-template, no injection vector). Migrate every call site of `lib/database.js::db.query`. Delete `lib/database.js`.
- **Reviewer/Architect call:** small enough to fit in one convoy; touches ~3 files based on graph.
### 9. Three parallel client-side auth implementations
- **Files:** `lib/auth-context.js` (`AuthProvider` / `useAuth`), `lib/admin-auth.js` (`AdminProvider` / `useAdmin` / `useIsAdmin`), `lib/use-auth.js` (`useAuth`).
- **Impact:** Pages randomly import from one of three places. State is duplicated. Logout in one provider doesn't necessarily clear the others. Token-verify roundtrips happen 3× on initial page load if all three providers mount.
- **Fix:** Collapse to `lib/use-auth.js` as the canonical hook. Migrate every importer. Delete `auth-context.js` and `admin-auth.js`. Roll up `useIsAdmin` semantics into `useAuth().user?.role === 'admin'`.
- **Owns:** `role-architect` (decision) → `role-implementer` (per-page migration; ~30 importers).
### 10. No tests
- **Impact:** The first agent-driven refactor of `getUserFromRequest` (P0 #1) is high-blast-radius with no safety net.
- **Fix sequence:**
1. Install `vitest`. Add `npm run test:run` script. **RESOLVED** by `fix-auth-bypass` Brief 5, commit `1629afb`.
2. Install `@playwright/test`. Wire up `tests/smoke/app.smoke.spec.ts` (already drafted; needs `playwright.config.ts`). **RESOLVED 2026-05-24** by `adopt-playwright-smoke`, PR #18 squash `7b6f751` — 3/3 smoke tests pass in 2.9s, full workflow 59s, zero secret leaks. See § Queued convoys and `.convoys/adopt-playwright-smoke.md` § As-shipped.
3. Re-enable the `test:` job in `.github/workflows/ci.yml` (commented out at install time). Next remaining step in this fix sequence.
4. Add unit tests for `lib/permission-middleware.js`, `lib/slug-utils.js`, `pages/api/auth-utils.js`.
5. Wire `preview-smoke.yml` to run against the Vercel preview URL. **RESOLVED 2026-05-24** by `fix-vercel-deployment-protection-in-ci` (PR #17, `9a3e077`) + `adopt-playwright-smoke` (PR #18, `7b6f751`).
- **Owns:** `role-architect` (test strategy) → `role-implementer` (initial suite).
### 11. No migration tool — `scripts/add-*.js` graveyard
- **Files:** 27 scripts in `scripts/` of the form `add-foo-column.js`, `fix-bar-constraint.js`, `seed-baz.js`. No idempotency tracking, no `schema_migrations` table, no rollback.
- **Impact:** Onboarding a new env requires re-running every script in the right order. No way to know what's been run on a given Neon branch. Every new column is at risk of being missed in prod.
- **Fix:** Adopt `node-pg-migrate` (lightweight, matches the existing pattern best) OR migrate to `drizzle-kit` if the team wants schema-as-code. Backfill a single "initial" migration matching current prod schema. From there, every new column ships as a migration file.
- **Owns:** `role-architect` (tool selection) → `role-implementer` (backfill + first new migration).
### 11.5. Codebase has ~100 pre-existing ESLint errors
- **Discovered:** 2026-05-22 during the bootstrap PR. The repo had `"lint": "next lint"` in `package.json` but no `.eslintrc.json` — meaning lint was never run. Bootstrap added the config; lint now surfaces ~100 errors.
- **Most serious:** `react-hooks/rules-of-hooks` violations (hooks called conditionally) in several components. These are **real bugs** — React's hook ordering is undefined when hooks are called after early returns. They likely manifest as state-loss / stale-closure bugs in edge cases.
- **Less serious:** `react/no-unescaped-entities` (cosmetic), `react-hooks/exhaustive-deps` (warnings about missing useEffect deps), `@next/next/no-img-element` (cosmetic).
- **Impact:** The L3 CI lint job is currently `continue-on-error: true` (see `.github/workflows/ci.yml`) so it doesn't block PRs. Lint output is visible in logs but PRs merge regardless of lint state until this is cleaned up.
- **Fix:** Triage each error. The rules-of-hooks ones need genuine code restructuring (move hooks before any early returns). The unescaped-entities are mechanical (`'` → `&apos;`). After cleanup, remove `continue-on-error: true`.
- **Convoy:** `fix-lint-baseline` — run after `fix-auth-bypass` and `drop-public-setup`. Multitask-safe: split into briefs by file group.
- **Owns:** `role-architect` (group strategy) → `role-implementer` (per-group fan-out).
### 12. Branding mismatch — "TCG Vault" vs. "Deck Hearth"
- **Files:** README, `package.json`, seed data say "TCG Vault" / `admin@tcgvault.com`. `components/Layout.js` lines 596 + 689 render "Deck Hearth" + "DH" logo. The `.env.local` template, `vercel.json`, and Vercel project name should also be audited.
- **Impact:** Confusing for users. Confusing for marketing. Confusing for analytics. Pick one.
- **Fix:** Brand workshop → final name → global replace → update README, package.json `"name"`, every UI string, Vercel project name, email sender, support pages. Schedule a redirect from the old domain.
- **Owns:** `role-ia-architect` (which name? — needs human decision) → `role-implementer`.
## P2 — refactor priorities
### 13. God components (10 files over 500 lines)
| File | Lines | Notes |
| --- | --- | --- |
| `pages/cards.js` | 1499 | `AuthenticatedCards` (886) + `Card3D` (502) live in one file. Split into `pages/cards/index.js` + `components/Card3D.js`. |
| `pages/collection/[identifier].js` | 1044 | `CollectionView` is one mega-component. Extract: header, card-grid, share-modal-wrapper, edit-form. |
| `pages/collections.js` | 989 | Similar structure to collection/[identifier]. Possibly share extracted pieces. |
| `pages/card/[id].js` | 913 | `CardDetail` — split into header, owned-badge, add-to-collection-flow. |
| `pages/deck-builder.js` | 823 | `DeckBuilder` — extract card-search, deck-list, mana-curve panels. |
| `components/CameraScanner.js` | 817 | Camera + AI-OCR + detection-loop — extract the detection loop into a hook. |
| `pages/admin/card-editor.js` | 778 | Form heavy. Use a `useFormState` pattern + separate the search-results subview. |
| `pages/scanner.js` | 776 | Mirror of CameraScanner concerns plus queue management. |
| `pages/settings.js` | 669 | One screen per settings section is the usual fix. |
| `pages/profile.js` | 625 | Avatar generation logic alone is ~150 lines — extract `useGeneratedAvatar` hook. |
Each is one convoy of its own. Use the architect role's `slice_dependencies:` to fan out implementers safely.
### 14. Schema-design smells (documented in `docs/SCHEMA_MAP.md`)
- `users` has two avatar columns (`profile_image_url` + `avatar_url`). Reconcile.
- `collections` has two visibility flags (`is_public BOOLEAN` + `visibility VARCHAR`). Reconcile.
- `cards.quantity` + `cards.favorited` are unused (they belong on `user_cards` / `user_favorites`). Drop.
- `user_settings` table duplicates several `users` columns. Reconcile.
- All enum-shaped VARCHARs (`role`, `condition`, `theme`, `game`, `visibility`) should be CHECK-constrained or proper Postgres ENUMs.
- `collections.tags` is `TEXT` (comma-separated). Migrate to `JSONB` or a join table.
### 15. Component coupling warning from graph
`user-code-review-graph` flagged:
- High coupling (44 edges) between `components-handle` and `pages-handle` (largely `Layout`, `CardItem`, `ManaCost` — expected for a shared UI surface).
- High coupling (34 edges) between `lib-admin` and `api-handler` — almost all via `getUserFromRequest`. After P0 #1 is fixed, this number stays high because the auth check is genuinely shared — that's fine.
### 16. Lots of inline SVG and emoji
The `getIcon` registry in `Layout.js` and `MobileNavigation.js` redefines the same SVG paths. Extract to `components/icons/` with named exports. Then audit the codebase for inline SVG that should be a named import. Bonus: lazy-load the larger icon families.
## P3 — UX, IA, design-system
### Role-ia-architect findings
- **URL structure** — solid. `/cards`, `/collections`, `/collection/[slug]`, `/deck-builder`, `/community/collections`. Coherent. One quirk: `/card/[id]` (singular) for detail vs. `/cards` (plural) for index — typical Next.js shape but worth a redirect rule so `/cards/[id]` also resolves.
- **Logged-out homepage** — current `pages/index.js` is 316 lines; needs an editorial pass. What's the value prop in one sentence? Right now it's mostly "we have cards".
- **Onboarding** — signup → profile setup → first collection → scan-or-import card. Currently each step is a separate page. Consider a multi-step wizard at `/onboarding` to keep the new user in flow.
- **Discoverability** — `/community/decks` and `/community/forums` are in the nav but flagged as placeholders. Either ship the MVP for each before launch (forums likely too big) or hide the nav items until they exist.
### Role-ux-reviewer findings
- **Loading states** — most data fetches set `loading: true` then re-render; very few show skeletons. Card grids should use shimmer placeholders; modals should disable submit while in flight.
- **Error states** — error messages bubble to `console.error` and toast nothing. Add a global toast system (e.g. `sonner`) and wire every catch block.
- **Empty states** — `/my-cards` and `/collections` when empty drop to "no cards yet". Replace with first-time CTA: "Scan your first card" or "Browse popular sets".
- **Mobile drawer** — `MobileNavigation` is solid (recent commit `442e906`). One thing: the bottom-bar's active state contrast looks low in light mode; verify against AA.
- **Camera scanner UX** — 817 lines of detection loop. Add a one-line "scanning…" status under the viewfinder and a single "captured N cards" badge. The current toolbar is busy.
### Role-design-system-auditor findings
- **Two visual languages mixing** — Tailwind classes AND CSS variables on the same elements. This is documented in `.cursor/rules/ui-and-theming.mdc`; the cleanup is to define which property goes where and enforce.
- **Hardcoded hex colors** — grep for `bg-\[#` and `style={{ backgroundColor: '#`. There are still a handful; convert to theme tokens.
- **Logo + brand** — see P1 #12. Then once the name is settled, the "DH" logo + AnimatedFireLogo need to be unified into one brand mark.
- **Modal patterns** — `CollectionSelectionModal`, `ShareModal`, `UploadImageModal` each have their own backdrop + focus-trap implementation. Extract `<Modal>` primitive. Use `headlessui` or `radix-ui`'s Dialog to get focus management for free.
- **Card grid spacing + density** — `pages/cards.js` (the 1499-line monster) does responsive grid math inline. Extract a `<CardGrid>` component that handles density (compact / comfortable / spacious) + sort + filter chrome.
### Role-a11y-auditor findings
- **Focus traps in modals** — none of the modals trap focus. Tab through `ShareModal` and you leave to the background. Critical for keyboard users + screen readers.
- **ESC to close modals** — inconsistent. Some have it, some don't.
- **Skip-to-content** — no `<a href="#main" class="sr-only focus:not-sr-only">`. Add to `_app.js`.
- **Image alts** — card images use `alt={card.name}` (good); avatar images sometimes have empty alts. Audit.
- **Color contrast** — verify the muted text colors (`var(--text-secondary)`) hit AA on both themes. The mobile bottom-bar inactive state is a likely fail.
- **Form errors** — login/signup form errors are visually red but not connected to inputs via `aria-describedby`. Screen readers don't know which field failed.
- **Keyboard ops on non-button elements** — most clickable `<div>`s already have `onKeyDown` but a few don't (audit with `rg "onClick" components pages | rg -v "<button"`).
### Role-doc-writer findings
- **README** — needs a public-facing rewrite. Currently mixes user docs + dev setup + admin credentials. Split into `README.md` (project landing) + `docs/DEVELOPMENT.md` (dev setup) + delete the admin credentials section entirely.
- **`docs/SCHEMA_MAP.md`** — installed at bootstrap (this convoy). Keep it fresh on every schema change.
- **CHANGELOG** — none yet. Adopt Keep-a-Changelog format. Backfill `[0.1.0] — initial private alpha` covering everything to date.
- **`TESTING_GUIDE.md`** — currently the only test doc; rename to `docs/MANUAL_QA.md` once `vitest` + `playwright` land.
- **`docs/API_REFERENCE.md`** — would help. Could be auto-generated by walking `pages/api/**/*.js` and extracting JSDoc; or hand-curated to start.
- **Privacy policy / Terms of service** — required before public launch. Use a template (Termly / Iubenda) and customize.
## Proposed launch sequence
Each phase is one Conductor-created convoy. Don't run more than two in parallel until tests exist.
0. **`bump-next-js`** (P0 #8). One PR. **MUST land first** — Vercel is currently blocking all deployments, which makes every other PR's preview-smoke / visual-diff gate non-functional. Trivial bump; risk is breaking changes if going to 16.x.
1. **`fix-auth-bypass`** (P0 #1, #2, #4, #5, #6 partial). One PR. Highest risk; needs human review.
2. **`drop-public-setup`** (P0 #3, #4). One PR. Trivial; do as a hotfix.
3. **`fix-layout-default-user`** (P0 #7). One PR. Trivial.
3.5. **`fix-lint-baseline`** (P1 #11.5). 2-4 PRs via multitask. Closes the lint gate (drops `continue-on-error`).
4. **`add-rate-limiting`** (P0 #6 full). One PR. Adds @upstash/ratelimit + applies to listed routes.
5. **`pick-a-name`** (P1 #12). Human decision first, then one or two PRs.
6. **`adopt-vitest`** (P1 #10 step 1). One PR. Enables testing every future change.
7. **`migration-tool`** (P1 #11). One PR. Backfill + first new migration.
8. **`single-sql-client`** (P1 #8). 2-3 PRs, fanned out via multitask once per-file briefs are written.
9. **`single-auth-provider`** (P1 #9). 3-5 PRs via multitask.
10. **`adopt-playwright-smoke`** (P1 #10 step 2). One PR. **RESOLVED 2026-05-24** — PR #18 squash `7b6f751`; smoke 3/3 green in 2.9s, full workflow 59s, zero secret leaks. See § Queued convoys for the full as-shipped block.
11. **`schema-cleanup`** (P2 #14). Multi-PR convoy via multitask.
12. **`god-component-split`** (P2 #13). One convoy per file; fan out via multitask once architect's `slice_dependencies` are written.
13. **`launch-polish`** (P3). UX/IA/a11y/docs convoy.
Total: ~14 convoys to get from current state to public-launch-ready. Estimate 4-8 weeks at one human-in-the-loop reviewer per convoy. Multitask + Cursor 3.2 worktrees compress steps 8-12 substantially.
## Queued convoys
Follow-ups surfaced mid-convoy or mid-PR that didn't fit the original launch sequence but need to land before public traffic. Listed in priority order; not all will be P0/P1 — most are CI / DX / hygiene polish.
- **`rotate-default-admin`** (priority: P2 hygiene). Operator-rotation script for envs that ran `setup-neon-db.js` before `drop-public-setup` and still carry the weak `admin123` bcrypt hash. Surfaced in P0 #3 § Operator caveat. Optional: do nothing if no audit finds a deployed env with the weak hash.
- **`cors-tighten`** (priority: P1 quality). Drop the wildcard `Access-Control-Allow-Origin` header from `pages/api/auth/verify.js`. Surfaced in P0 #5 (deferred from `fix-auth-bypass` Brief 4).
- **`add-rate-limiting`** (priority: P1 quality, also listed in launch sequence step 4). Extend `lib/rate-limit.js` to `/api/users/search`, `/api/cards/search`, all `/api/cards/import-*`, and `/api/user/avatar*`. Login + register already wired in `fix-auth-bypass` Brief 4.
- **`purge-weak-creds-from-helpers`** (priority: P2 hygiene). Sweep `scripts/reset-db.js`, `scripts/create-test-users.js`, and `TESTING_GUIDE.md` for the literal `admin@tcgvault.com` / `admin123` references. May fold into `pick-a-name` since the email itself is changing.
- **`single-auth-provider`** (priority: P1 quality, also listed as launch sequence step 9). Collapse `lib/auth-context.js` + `lib/admin-auth.js` into `lib/use-auth.js`. Surfaced again as a follow-up in P0 #7 § Flagged-but-deferred (4 pages still import the legacy `useAuth`).
- **`cleanup-mobile-nav-dead-props`** (priority: P3 polish). `components/MobileNavigation.js` accepts a dead `user` prop; remove it. Surfaced in P0 #7 § Flagged-but-deferred. May fold into `god-component-split` (P2 #13) if that lands first.
- **`bump-eslint-10`** (priority: P2 hygiene; upstream-blocked). Bump ESLint from v9 to v10 once `typescript-eslint` ships a v10-tested release and `eslint-config-next` bundles it. Surfaced in `.convoys/bump-next-js.md` § Decisions D.
- **`seed-visual-baselines-on-linux`** (priority: P2 CI infra; **operator action required**). Generate Linux baselines for `tests/visual/__screenshots__/` in the `mcr.microsoft.com/playwright:v1.60.0-noble` Docker image and commit them in a small follow-up PR. Mac-generated baselines would silently overwrite Linux CI baselines because `playwright.config.js`'s custom `snapshotPathTemplate` has no `{platform}` token (Risk R3 + Boot-the-brief Finding 7 in `.convoys/adopt-playwright-smoke.md`). Until this PR lands, every `Screenshot diff` run on a PR touching `pages/**` / `components/**` / `styles/**` / Tailwind/PostCSS config fails at the test step and posts a "Visual Diff — view run" comment with empty artifacts — that's the documented Decision-4 end state of `adopt-playwright-smoke`, not a regression. One small PR with just the PNG baseline(s). Surfaced 2026-05-24 as the follow-up to `adopt-playwright-smoke` (PR #18).
- **`adopt-playwright-smoke`** (priority: P1 quality, also listed as launch sequence step 10 / P1 #10 step 2) — **RESOLVED 2026-05-24**.
- **Resolved by:** squash commit `7b6f751` (PR #18, architect-commit `3ac527e`, implementer-commit `c72d006`). Brief 1 shipped as planned with two small lint-baseline-preserving deviations from the brief's verbatim shape (documented in the convoy file's § As-shipped).
- **As-shipped surface:** `@playwright/test@^1.60.0` added to `devDependencies`; new `playwright.config.js` at repo root (ESM, two projects partitioned by `testMatch``smoke` + `visual`, CI-fail-loud / dev-warn predicate on `VERCEL_AUTOMATION_BYPASS_SECRET` per Decision 2, `snapshotPathTemplate: 'tests/visual/__screenshots__/{arg}{ext}'` aligned with `visual-diff.yml`'s artifact upload path); new `tests/visual/homepage.spec.ts` (1 test, no baseline committed per Decision 4); three new `package.json` scripts (`test:smoke`, `test:visual`, `test:visual:update`); three new `.gitignore` entries (`/playwright-report/`, `/test-results/`, `/.playwright/`). No `eslint.config.mjs` change (Decision 5 + Finding 2 verified clean empirically). No source touched under `pages/**` / `components/**` / `lib/**`.
- **Implementer deviations** (both behavior-neutral, both lint-baseline-preserving):
1. Removed Brief 1's `// eslint-disable-next-line no-console` directive on `playwright.config.js`'s `console.warn` branch — the current ESLint config does not flag `console.warn` at all, so the disable directive itself would have regressed lint from 128 → 129 as an "Unused eslint-disable directive" error.
2. Placed `@playwright/test` first in `devDependencies` for strict alphabetical correctness — the brief's prose was internally inconsistent on neighbors (`@playwright` sorts lexically before `@testing-library/react`).
- **As-shipped metrics** (from post-merge `Playwright smoke` run 26376162598 on `main`):
- `Playwright smoke` workflow total runtime: **59 seconds**, exit 0 (was: fast-fail at "playwright not installed" / "no config" before this convoy).
- `Run smoke tests` step: **3/3 tests pass in 2.9s** against the Vercel preview with `x-vercel-protection-bypass` header applied — `home redirects or renders without 5xx` ✓ 683ms / `sign-in page renders` ✓ 459ms / `public health endpoint responds` ✓ 571ms.
- `Screenshot diff` workflow: **not triggered on PR #18 itself** because its `paths:` filter excludes test-infra-only changes; first real trigger fires on the next PR touching `pages/**` / `components/**` / `styles/**` / `tailwind.config.js` / `postcss.config.js`. At that point the documented Decision-4 end state runs live (test fails on missing baseline → `continue-on-error: true` swallows → comment-on-PR step posts run link with empty artifacts).
- Bypass secret leak check: **0 matches** in the raw workflow log. GitHub Actions auto-masks registered secrets; our Decision-2 branches name the env var but never interpolate the value into any string.
- **Cross-validation finding** (not a planned AC; surfaced organically from CI green): smoke test 2 (`'sign-in page renders'`) asserts `await expect(page.getByRole('button', { name: /sign in/i })).toBeVisible()` against `/login`, which only passes because `components/Layout.js` renders the `<Link href="/login">Sign in</Link>` CTA on the logged-out branch that PR #15 (`fix-layout-default-user`, `ca302a8`) introduced. P0 #7's resolved state is now defended by a live CI signal — if a future PR reverts to a hardcoded default user or breaks the CTA wording, smoke fails the PR (in addition to the 5 vitest assertions in `test/components/Layout.test.js`).
- **Operator action required going forward:** `seed-visual-baselines-on-linux` (above) is the follow-up. Until it lands, `Screenshot diff` runs post a "Visual Diff — view run" comment with empty artifacts on every UI-touching PR — that is the Decision-4 end state, not a regression. No operator action is required to keep `Playwright smoke` green.
- **Flagged-but-deferred** (deliberately out of scope per the convoy file, restated here for the audit trail):
1. `seed-visual-baselines-on-linux` — see above.
2. `adopt-test-smoke-local` (possible follow-up) — a `test:smoke:local` wrapper that auto-boots `next dev`. Explicitly rejected by Decision 6; queue only if dev friction proves out.
3. Deeper E2E coverage beyond the 3 existing smoke checks — per-feature work in feature convoys, not a test-infra concern.
- **Owns:** `role-architect` (3 of 6 decisions self-ratified — D2 CI predicate, D3 two-project shape, D5 no-eslint-change; 3 of 6 operator-ratified — D1 keep `.ts`, D4 defer baselines, D6 simple scripts) → `role-implementer` (Brief 1, plus the two deviations above).
- **`fix-vercel-deployment-protection-in-ci`** (priority: P2 CI infra) — **RESOLVED 2026-05-24**.
- **Resolved by:** squash commit `9a3e077` (PR #17), comprising three commits, not one. Operator prereq seeded 2026-05-24T20:03:31Z (`gh secret set VERCEL_AUTOMATION_BYPASS_SECRET`; confirmed via `gh secret list`); the implementer dispatch waited on that visibility per the convoy file's "Operator action required" gate.
- **Three-commit reality** (Brief 1 + two scope expansions found during CI validation):
1. `365e9f0` Brief 1 — bypass plumbing per spec. Both `.github/workflows/preview-smoke.yml` and `.github/workflows/visual-diff.yml` got the same shape change: `wait-for-vercel-preview@v1.3.2`'s `path:` input now carries `/?x-vercel-protection-bypass=${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}&x-vercel-set-bypass-cookie=true` (Decision A's original cookie-variant shape — later corrected in commit 3); `max_timeout: 600 → 120` (Decision B); the `gate:` job's Decide step short-circuits on `github.event.pull_request.head.repo.fork == true` with a `::notice::` annotation, before the existing PR-body skip directive runs (Decision D); and the Playwright/screenshot step exports `VERCEL_AUTOMATION_BYPASS_SECRET` as `env:` for forward-compat with `adopt-playwright-smoke`.
2. `b6f8688` shell-injection hardening (latent pre-existing bug surfaced by PR #17's own CI validation). Decision D's gate step inlined `${{ github.event.pull_request.body }}` directly into bash, which broke when the PR body contained shell metacharacters like `(` or backticks. PR #17's own description bit this with `"unexpected token \`('"` because of phrasing like *"(was: 10-minute timeout)"*. Fix is the standard GitHub Actions hardening pattern (their official "Security hardening" guide flags inline `${{ }}` in shell as both a syntax-error risk and a shell-injection vector): route the body and the fork flag through the step's `env:` block as `PR_BODY` and `PR_IS_FORK`, then quote them as `"$PR_BODY"` / `"$PR_IS_FORK"` in the shell condition. Same change in both workflows; ~9 LOC each. Documented in commit message as technically beyond Brief 1's scope but bundled into the convoy because the bug actively blocked Brief 1's success criterion from being validated.
3. `043a6ee` drop `&x-vercel-set-bypass-cookie=true` from the wait-action `path:` — corrects Decision A's exact shape. With the cookie variant, Vercel responds **307 + Set-Cookie**, and axios in Node has no cookie jar — it follows the redirect to the bare URL without the cookie, which then 401s. Empirically confirmed by operator's local curl: bare `?x-vercel-protection-bypass=X` → HTTP/2 200, while `?x-vercel-protection-bypass=X&x-vercel-set-bypass-cookie=true` → HTTP/2 307 (the broken path). For a one-shot healthcheck the per-request bypass query is enough. The cookie variant stays reserved for the future Playwright config (`adopt-playwright-smoke`) where a real browser cookie jar exists. An inline comment in `preview-smoke.yml` explains this so the next agent doesn't accidentally re-add the cookie param.
- **As-shipped metrics** (from PR #17's CI run, post-validation):
- `Wait for Vercel Preview deployment` step elapsed: **194 milliseconds** (was: 10-minute timeout before this convoy).
- `Playwright smoke` workflow total runtime: **59 seconds** (was: 10+ minutes).
- Step breakdown: `Wait for Vercel Preview deployment` → success in 194ms; `npm ci`, `setup-node`, `playwright install` → success; `Run smoke tests`**failure** (expected — see next bullet).
- `Screenshot diff` workflow: not triggered on PR #17 itself because its path filter excludes workflow-only changes; will fire on the next PR touching `pages/**` / `components/**` / `styles/**` / Tailwind/PostCSS config.
- **Documented expected red**: `Playwright smoke` now reaches `npx playwright test` and fast-fails because `playwright.config.js` doesn't exist in the tree yet. That is `adopt-playwright-smoke`'s scope (P1 #10 step 2 / launch sequence step 10), not this convoy's. Per the convoy file's Test plan § and Brief 1 acceptance criterion #1, a real downstream failure with the wait-action reaching `Received success status code` first counts as success for this convoy — the failure mode shifted from "401 timeout in the wait step" to "playwright not installed", which is precisely the target state.
- **Operator-rotation caveat** (R6 in the convoy file). The Vercel bypass token does not auto-expire. If/when it's rotated from the Vercel dashboard, the operator must re-seed the GitHub secret via `gh secret set VERCEL_AUTOMATION_BYPASS_SECRET --body "<new value>"`. Same human-responsibility pattern as `JWT_SECRET` rotation; not preventable from workflow YAML. No automation here.
- **Flagged-but-deferred** (from the convoy file's "Anything flagged but not acted on" section, unchanged at merge):
1. `replace-wait-for-vercel-preview` — the wait-action's last release was Mar 2024; could be replaced with a few lines of `gh api` + `curl`-loop. Out of scope for this convoy; queue if the action ages out further or gets a security advisory.
2. `adopt-playwright-smoke` — owns the actual `playwright.config.js`, `tests/smoke/`, and `@playwright/test` dep. The bypass plumbing here is forward-compat for that convoy (env var available on the smoke step). Listed in P1 #10 step 2 / launch sequence step 10 above.
3. `Screenshot diff` baseline authoring — orthogonal scope; the visual-diff workflow has nothing to compare against on its first real run.
- **Owns:** `role-architect` (3 Decisions ratified — A query-param, B 120s timeout, D fork-PR skip) → `role-implementer` (Brief 1) + two scope-expansion commits.
## Self-analytics
After each convoy, `scripts/log-convoy-event.sh` emits a record to `.convoys/.metrics.jsonl` (gitignored). After 3-5 convoys, run the upstream `agent-pipeline/analytics/` aggregator to see where token spend goes — that data feeds whether to add or remove rules.
## How to start
Per `.cursor/agents/role-conductor.md`, start the next convoy with:
> *"Run role-conductor: start a new convoy `fix-auth-bypass` to address P0 #1, #2, #4, #5, #6 partial in `.convoys/ship-readiness.md`. Success = `getUserFromRequest` returns null for missing tokens; no API route accepts unauthenticated requests; CI green."*
The Conductor will set classification, skip flags, and hand off to subsequent roles.