Closes the operator caveat from the `drop-public-setup` convoy: deployed
envs that ran `npm run setup-db` BEFORE `ff80753` (2026-05-22) still
carry the historical `admin123` bcrypt hash. The seed is idempotent
(`ON CONFLICT (email) DO NOTHING`), so re-running setup-db is a no-op
on existing rows.
## Design — D1: which option from the 3-option menu?
| Option | Picked? | Why |
|---|---|---|
| A. Close as no-op (defer rotation to manual app login) | No | Leaves a real-world residue if any pre-drop-public-setup env still exists — and an audit is harder than just shipping the script. |
| B. One-shot parameterized rotation script | **Yes** | Tightly scoped (~120 lines). Audit-trail-preserving (`updated_at` bump). Reusable for future rotations. No new auth surface in the app. |
| C. First-login forced password reset flow in the app | No | Right product answer, but heavier scope (new route, new flag column, UI work). Deferred as the queued `force-admin-password-reset-flow` convoy. |
## Script shape
`scripts/rotate-admin-password.js`:
- Reads `POSTGRES_URL` + `ADMIN_NEW_PASSWORD` from env (or `.env.local`).
- Optional `ADMIN_EMAIL` override; defaults to `admin@deckhearth.com`.
Pass `admin@tcgvault.com` for envs that pre-date `pick-a-name`
(squash `9abbab6`, 2026-05-24).
- Fail-loud-exits BEFORE opening any DB connection if:
- `POSTGRES_URL` is unset
- `ADMIN_NEW_PASSWORD` is unset or empty
- `ADMIN_NEW_PASSWORD` is shorter than 12 chars
- Validates the target row EXISTS AND has `role = 'admin'` before
touching it. Refuses to rotate non-admin rows even if `ADMIN_EMAIL`
points at one. Refuses to rotate when multiple rows match (impossible
given the UNIQUE(email) constraint, but checked anyway).
- Hashes with bcryptjs at 12 rounds — same as `setup-neon-db.js`.
- After UPDATE, re-fetches the row and runs `bcrypt.compare(newPassword,
row.password_hash)`; exits non-zero if the compare fails (extremely
unlikely, but catches silent UPDATE failures).
- NEVER echoes the password to stdout / stderr / shell history. The
only output is the row id, email, role, and updated_at.
Same import shape as the existing `scripts/migrations/2026-05-24-rename-admin-email.js`
(ESM, `dotenv.config({ path: '.env.local' })`, `import { neon } from
'@neondatabase/serverless'`, tagged-template SQL) — keeps the "11
scripts/* using neon() directly" graveyard from gaining new patterns;
fits the `purge-neondatabase-serverless-fully` follow-up convoy's
existing audit shape.
## Out of scope
- Sibling test users (alice / bob in `scripts/create-test-users.js`) —
dev fixtures, not real auth surfaces. Documented inline + in
AGENTS.md Gotcha #4.
- First-login forced password reset flow — deferred as the queued
`force-admin-password-reset-flow` convoy (it's the right product
answer, but heavier scope than this hygiene PR).
- Email rotation (already handled by
`scripts/migrations/2026-05-24-rename-admin-email.js`).
## Test plan
- [x] `node --check scripts/rotate-admin-password.js` — syntax OK
- [x] `npm run lint` — clean (1 pre-existing unrelated warning)
- [x] `npm run test:run` — 118 tests pass
- [ ] CI on this PR
- [ ] Operator-side smoke test (NOT covered by CI):
- Set `ADMIN_NEW_PASSWORD=test-rotation-12chars` against a throwaway
Neon branch DB, run the script, log in via the app with the new
password, run the script again with a different password, log in
again. Skip if there's no convenient throwaway DB.
Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up PR to #95 (Liquid Glass foundation + primitives + Layout shell)
that closes out the remaining sub-convoy briefs in a single sweep.
Operator-instructed scope: "finish off the design changes."
After this PR, **all 8 Liquid Glass sub-convoys are MERGED to main**;
the deferred-from-#5 `fix-card3d-state` convoy is dropped (its target,
`components/Card3D.js`, turned out to be dead code).
## #2 Brief 2 — Remaining 8 modals migrated to <Modal> primitive
- `CollectionsSuccessModal.js` — wrap in <Modal hideCloseButton>; 2 Buttons.
- `CollectionsEditModal.js` — full <Modal> + <Input> + <Button> rewrite (4 fields, tag chip section, public-toggle preserved, 2 footer Buttons).
- `CollectionEditModal.js` — same pattern as above (4 fields + public-toggle + 2 Buttons).
- `CardDetailDeckModal.js` — <Modal> + native select (Select primitive not in scope) + 2 Buttons; sweep `gradient-bg-purple` → `<Button variant="primary">`.
- `UploadImageModal.js` — <Modal> + token-driven URL/file tab switcher + drag-drop using `--accent-ember` rim + 2 Buttons (one with `loading` prop).
- `CollectionSelectionModal.js` — largest of the set (header summary + SearchBar + scrollable list w/ checkbox toggles + footer); migrated to <Modal size="lg"> while preserving the per-collection card preview thumbnails.
- `OCRSettings.js` — trivial <Modal> wrap + single primary <Button>.
- `pages/decks.js` — both inline modals (Create Deck + Edit Deck) and `components/ScannerPageView.js` (Create List) migrated; ScannerPageView dropped its `useFocusTrap` named-import (Modal's internal focus trap owns the panel ref now).
- **`.github/workflows/ci.yml` `forbidden-modal-shell-without-primitive`** — grandfather list emptied to zero entries; gate is now strict.
## #3 Brief 2 — Forms migrated to <Button> / <SearchBar>
- `pages/dashboard.js` — 3 CTAs → <Button> (Create List with leadingIcon, Create Your First List, View All Lists).
- `pages/my-cards.js` — empty-state CTA → <Button variant="primary" size="lg">. View-mode toggle buttons intentionally left native (icon-only, doesn't match Button variants).
- `pages/community/collections.js` — Go to My Lists CTA → <Button>.
- `components/CollectionsPageView.js` — Discover Community + Create List header CTAs → <Button>; search input → <SearchBar>.
- Card-grid per-row icon buttons (CollectionsPageView, my-cards, CardsPageView) intentionally left native — tiny per-card actions whose styling doesn't match Button variants and would invalidate visual-diff baselines.
## #5 — scope revised + landed
`components/Card3D.js` deletion: surveyed every importer with grep — **zero consumers** in `pages/**` or `components/**`. Only references were in convoy docs. The "pre-existing state-management bug" (state setters used without useState declarations) never affected the running app because the component was never rendered. -505 LOC. The `fix-card3d-state` convoy is dropped from the roadmap as a result.
The actual card-grid component (`components/CardItem.js`) is intentionally **not** modified in this sweep — it has per-rarity glow tuning that the existing visual-diff baseline locks in, and the architect's #5 deferral note specifically called out the dedicated baseline re-seed cost. A future implementer turn can apply rim-light tokens to CardItem with its own baseline re-seed when an operator wants that polish.
## #6 Brief 1 — Landing + invite pages glass-migrated
- `pages/index.js` — top nav: `var(--glass-surface-mid)` + `--glass-blur-mid` + rim-light. 3 feature cards: `<GlassSurface tint="mid" rim="subtle" elevation="ambient">`. Featured-list cards (the public collection grid): same `<GlassSurface>` recipe with motion-token transitions. All 6 CTA buttons → <Button variant="primary"|"secondary"|"ghost"> with proper sizes. Pulse-loading placeholders tagged `.motion-essential` so reduced-motion users still see them animate (state-meaningful).
- `pages/invite/accept.js` + `pages/invite/decline.js` — both outcome panels wrapped in `<GlassSurface tint="mid" rim="subtle" elevation="pronounced">`. Loading spinner border colors corrected from `--text-accent` (which didn't exist) to `--accent-ember`. All 8 buttons → <Button>. `gradient-bg-ember` consumers retained (the canonical warm-palette utility class is fine).
## #8 Brief 2 — Legacy alias sweep + CI gate graduation
- Swept `gradient-bg-purple` → `gradient-bg-ember` across **8 files** / **13 occurrences**: `CardDetailQuantityModal`, `CardEditorView`, `CardEditorForm`, `AdminProtected`, `pages/card/[id]`, `pages/invite/{accept,decline}`, `pages/admin/card-import`. `gradient-bg-purple` was a dangling class name with no CSS definition (it was rendering no styling), so the sweep is also a bug fix — those buttons now actually get the ember gradient.
- Deleted the 5 dead CSS classes from `styles/globals.css`: `.gradient-text-blue`, `.gradient-text-purple`, `[data-theme="dark"] .glow-blue`, `[data-theme="dark"] .glow-purple`, `[data-theme="dark"] .glow-pink`. Each was zero-consumer post-sweep.
- **Graduated the `forbidden-deprecated-color-aliases` CI job from WARN to FAIL.** All 9 patterns (`gradient-text-{purple,pink,blue}`, `glow-{purple,pink,blue}`, `gradient-bg-{purple,blue,pink}`) now block the build if any consumer is reintroduced.
## Verification (local + CI gates locally exercised)
- Lint: 0 errors, 2 pre-existing warnings (`CardEditorForm.js` + `CollectionsPageView.js` carry-overs from before #95; out of scope).
- Vitest: 104/104 passing — unchanged from #95.
- Build: clean (Turbopack default; passes both light + dark theme prerender).
- `forbidden-modal-shell-without-primitive` gate: locally clear (`grep -lE 'fixed inset-0 bg-black bg-opacity-' pages components -r --include='*.js'` returns no matches).
- `forbidden-deprecated-color-aliases` gate: locally clear (all 9 patterns return no matches in `pages/` or `components/`).
## What still needs human action
- **Linux visual-diff baselines** must re-seed via the Docker workflow in `AGENTS.md` § 6. This PR's landing-page + invite-page changes will produce baseline drift on the homepage screenshot (which is currently the only baseline committed) AND additional baselines will be generated for the landing's glass-card sections once the visual spec is expanded. Recommended: run the Docker re-seed against this PR's Vercel preview, commit the result to this branch, push, verify CI green, then merge.
- Vercel auto-promotes the merge to production.
## Closes / supersedes
- Closes `.convoys/liquid-glass-modal-and-surface-primitive.md` Brief 2 (status → merged).
- Closes `.convoys/liquid-glass-form-primitives.md` Brief 2 (status → merged with explicit per-row-icon-button deferral note).
- Closes `.convoys/liquid-glass-public-and-auth.md` Brief 1 (status → merged).
- Closes `.convoys/cleanup-legacy-design-css.md` Brief 2 (status → merged + CI gate FAIL).
- Drops `.convoys/liquid-glass-card-surfaces.md` Brief 1 prerequisite (`fix-card3d-state` no longer needed; Card3D deleted).
- Drops the queued `fix-card3d-state` follow-up from the roadmap (target deleted).
- Updates `.convoys/ship-readiness.md` § "Design-system redesign portfolio" with a "Finish-portfolio sweep" subsection documenting final status of all 8 sub-convoys.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move scanner page markup into ScannerPageView.js. pages/scanner.js is now
a thin composer of session state, useScannerQueue, and the view.
Mark pages/scanner.js resolved in ship-readiness god-component-split table.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move camera feed markup, overlays, and disambiguation dialog wiring into
CameraScannerView.js. CameraScanner is now a ~45-line hook composer.
Update ship-readiness god-component-split CameraScanner slice as resolved.
Co-authored-by: Cursor <cursoragent@cursor.com>
Close stale convoy frontmatter for merged scanner, lint, and hygiene work;
record P1 #11.5 and queued follow-ups as RESOLVED with PR references.
Co-authored-by: Cursor <cursoragent@cursor.com>
Record audit-redesign-scanner-flow-44 rollup and queue follow-up convoys
from reviewer, design-system, and a11y fan-out on PRs #42–#44.
Co-authored-by: Cursor <cursoragent@cursor.com>
When vision reads a set+number missing from the catalog, route to
card_submissions rather than sibling disambiguation. Adds a not-listed
modal action, background vision refine, foil-friendly prompt, and
submit-for-review API. Queues catalog-sync-vercel-cron convoy for later.
Co-authored-by: Cursor <cursoragent@cursor.com>
Delete the public /api/config/gemini endpoint and remove client auto-load
paths so GEMINI_AI_API_KEY stays server-side only. Add a scan rate-limit
class for the upcoming server-side identify route and a CI gate that blocks
reintroducing config key leaks or new browser LLM URLs.
Co-authored-by: Cursor <cursoragent@cursor.com>
Fill in fix-reset-db-script.md § As-shipped with the merged-state record
(PR #25, squash 3ab9bf8, +161/-18) — three bugs atomically resolved by
mirroring setup-neon-db.js post-drop-public-setup shape; CI green
including 5th-consecutive smoke-spec cross-validation; lint-against-
cjs-in-esm-scripts surfaced as new P3 queue entry.
Update ship-readiness.md Queued convoys:
- convert-reset-db-to-esm → RESOLVED (audit-trail kept, not removed)
- purge-weak-creds-from-helpers → scope reduced (reset-db.js half done;
create-test-users.js + TESTING_GUIDE.md remain)
- lint-against-cjs-in-esm-scripts → NEW (P3 polish; would have caught
both DPS-B2 and PR #25 at lint time)
Co-authored-by: Cursor <cursoragent@cursor.com>
Post-merge doc cleanup for PR #21. Resolves the AGENTS.md line-5 'Pick one before launch' branding question. Updates AGENTS.md top branding note + Gotcha #4 + Gotcha #12, adds full as-shipped to .convoys/pick-a-name.md, adds 4 queued-convoy entries to ship-readiness.md, adds pick-a-name attribution to .cursor/rules/api-routes.mdc. Pure docs (+357/-15 across 4 files).
Reflects the merged add-rate-limiting convoy (PR #20, squash commit
708ef45) in repo documentation. **This is the milestone cleanup** —
add-rate-limiting closed P0 #6 (No rate limiting anywhere), the LAST
open P0 ship-blocker. `.convoys/ship-readiness.md`'s § Status summary
flips from "7 of 8 RESOLVED; 1 remains" to **"8 of 8 RESOLVED.
Launch-readiness P0 checklist is empty."** One brief in the convoy:
Brief 1 shipped as planned with no scope expansions and no implementer
deviations from the verbatim spec; all six architect decisions ratified
verbatim at gate 1 (D1 operator-ratified Option A; D2-D6
architect-self-ratified).
.convoys/add-rate-limiting.md:
- frontmatter status: in-progress -> shipped (added shipped: 2026-05-24)
- new ## As-shipped section. Opens with the milestone language
pointing back at ship-readiness.md's flipped § Status summary.
Decisions section captures all 6 ratifications (D1 operator-
ratified Option A — Critical: WHY the atomic admin UI fix in
pages/admin/card-import.js was Decision 1's hidden coupling
requirement, since API gating alone would have broken every
"Import Cards" click; D2 hybrid named-limiter shape with
Map<className, Ratelimit> cache; D3 per-class table including
the two D3 tuning-evidence raises — search 30 -> 60/min because
ShareModal.handleSearch has no debounce so a 17-char email = 16
requests in <5s, and generate kept at 5/hour because DiceBear is
free not paid AI; D4 two-extractor shape with defensive THROW
on null/empty userId; D5 uniform 429 message; D6 no new vitest
or playwright specs deferred to fill-vitest-handler-coverage).
As-shipped surface broken into 4 layers (1 lib refactor + 6 route
gates + 1 atomic admin UI fix + 1 rule extension) mirroring the
cors-tighten cleanup's pattern-split shape. Empirical CI metrics
from post-merge run 26382185019 (Playwright smoke 59s 3/3 in
3.8s, forbidden-cors-headers pass, vitest 21/21, lint 128 baseline,
Screenshot diff continue-on-error swallow per Decision 4).
Cross-validation finding: smoke test 2 still passes against the
post-rate-limit preview — that's three convoys in a row (PR #15
Layout default-user, PR #19 CORS-tighten, PR #20 rate-limiting)
where the same 3-test smoke spec defended the auth surface
through sweeping changes. Operator-action-required: none. What
did NOT change audit trail.
.convoys/ship-readiness.md:
- § Status summary at the top flipped from 7/8 to 8/8 RESOLVED.
Header text updated to "Launch-readiness P0 checklist is empty."
P0 #6 row in the table flips from PARTIAL to RESOLVED with the
two-convoy lineage (fix-auth-bypass Brief 4 + add-rate-limiting).
Trailing paragraph rewritten as a milestone note: security gate
closed; remaining launch work is P1 quality bar + P2/P3 polish.
- P0 #6 entry flipped from PARTIAL to RESOLVED with the full
add-rate-limiting as-shipped block (8 sub-bullets covering the
lib refactor shape, the per-class table, the defensive THROW,
the three import routes' auth-gating, the atomic admin UI fix
and WHY, the rule extension, the 6 decisions, and the diff
breakdown). Brief 4's 2026-05-23 partial is preserved as the
prior as-shipped layer to maintain the audit trail.
- Launch sequence step 4 marked RESOLVED 2026-05-24 with the
squash commit + smoke metrics inline.
- Queued convoys: removed the add-rate-limiting entry (it shipped).
Added a new delete-dead-lorcana-import entry (P3 polish; the
Lorcana import route was gated defensively in PR #20 despite
zero current frontend callers — pages/admin/card-import.js's
<select> only offers mtg + pokemon — so if Lorcana stays
permanently out of the admin UI, this is the cleanup PR).
Added three "flagged but kept out of scope" follow-ups per the
convoy's § What did NOT change: harden-multipart-parser (P2;
5MB body still consumed before the 429 path on avatar.js),
god-function-split / refactor-cards-search-sql (P2; 240-line
7-branch SQL in cards/search.js), and withAdmin(handler) wrapper
extraction (P3 DX; the three import routes are call sites #3-5
in the codebase but uniform inline shape was preserved for
convoy atomicity). Updated tighten-visual-diff-path-filter to
note PR #20 also tripped the same false-positive.
AGENTS.md:
- Gotcha #12 extended end-to-end. Was the single-class auth-only
lib + the env-var contract; is now the 5-class reality with a
full per-class table (helper / limit-window / key / routes),
the defensive THROW pattern in extractUserIdentifier, the
gate-ordering rule for per-user limiters, and the
auth → admin-role → rate-limit ordering for the three import
routes. Prominent milestone line opens the new content:
"add-rate-limiting convoy (squash 708ef45, PR #20, 2026-05-24)
closed P0 #6 — all 8 P0s now RESOLVED." Original env-var
contract paragraph (KV_REST_API_URL / KV_REST_API_TOKEN,
fail-closed-in-prod / warn-and-noop-in-dev) is preserved
verbatim above the new content.
- § 6 Testing: intentionally untouched (no test surface changed;
vitest 21/21 and smoke 3/3 still apply).
- § 7 Deployment: intentionally untouched (no deployment-shape
changed; same KV_REST_API_* env vars from Brief 4).
.cursor/rules/api-routes.mdc:
- The implementer extended § Rate limiting in PR #20 with the
per-class table + verbatim call shape + gate-ordering rules +
identifier-extraction + uniform 429 + fail-closed env-var
contract + fail-open Upstash-outage behavior. Doc-writer pass
verified completeness; added a one-sentence convoy-attribution
line at the top of § Rate limiting citing the two-convoy
lineage (fix-auth-bypass Brief 4 for the auth class +
add-rate-limiting for the other four classes and 7 newly-gated
routes), mirroring the post-cors-tighten § CORS attribution
shape. No other touch-ups needed.
No changes to: package.json, package-lock.json, lib/rate-limit.js,
pages/**, components/**, scripts/**, test/**, tests/**,
.github/workflows/**, README.md, TESTING_GUIDE.md, playwright.config.js,
eslint.config.mjs.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reflects the merged cors-tighten convoy (PR #19, squash commit da50d78)
in repo documentation. Closes P0 #5 (Wildcard CORS on API surface) from
PARTIAL -> RESOLVED, leaving only P0 #6 (full add-rate-limiting) open
of the original P0 ship-blocker set. One brief in the convoy: Brief 1
shipped as planned with no scope expansions and no implementer deviations
from the verbatim spec.
.convoys/cors-tighten.md:
- frontmatter status: in-progress -> shipped (added shipped: 2026-05-24)
- new ## As-shipped section: all 5 architect-self-ratifiable decisions
ratified verbatim (D1 Option B / D2 delete OPTIONS / D3 moot / D4
no new tests / D5 add CI lock); Pattern split (16 Pattern A + 8
Pattern B) per architect's 10-file audit + implementer's per-file
diff review; diff size (25 files, +29/-261); empirical CI metrics
from post-merge run 26378806555 (forbidden-cors-headers 4s PASS,
Playwright smoke 56s 3/3 in 3.3s, Screenshot diff continue-on-error
0 with the documented Decision-4 missing-baseline failure beneath);
cross-validation that Playwright smoke continues to pass post-CORS
removal (the auth + public surfaces don't depend on the wildcard
header); implementer subagent-retry footnote (HEAD already at
a843736 when retry woke up - transient retry, work is canonical);
operator-action-required-going-forward: none; What did NOT change
audit trail.
.convoys/ship-readiness.md:
- new ## Status summary at the top (right after the code-graph line):
P0 set is now 7/8 RESOLVED; only #6 (rate-limiting) remains. Table
lists each P0 with its resolving convoy + squash commit for a quick
scan of remaining work.
- P0 #5 marked RESOLVED 2026-05-24. Added the cors-tighten as-shipped
block (24 files swept, new CI job, 16/8 Pattern split, 5 decisions
ratified, diff stat, post-merge CI metrics, transient retry
footnote, operator-action: none). Brief 4's 2026-05-23 partial
is preserved as the prior as-shipped layer above the cors-tighten
layer to maintain the audit trail.
- Queued convoys: removed the cors-tighten entry (no longer queued).
Added a new tighten-visual-diff-path-filter entry (P3 polish) -
Screenshot diff workflow triggered on API-only PR #19 because its
paths: filter is pages/** which matches pages/api/** too. ~55s of
CI waste per API-only PR; one-line YAML tweak; verify GitHub
Actions' negated-glob semantics before merging.
.cursor/rules/api-routes.mdc:
- new ## CORS section near the existing ## Dev/test endpoints
(removed) section. Documents the no-CORS-by-default convention,
the brief-4 + cors-tighten lineage, the new forbidden-cors-headers
CI gate, and three forward-conventions (no setHeader for CORS,
no OPTIONS preflight handlers, design a proper middleware layer
if a future cross-origin caller is needed - not wildcards in
individual handlers).
AGENTS.md intentionally untouched. Gotcha #5 (the public
setup-database.js endpoint) is already RESOLVED by fix-auth-bypass
Brief 3 and unrelated to this convoy. The new convention belongs in
.cursor/rules/api-routes.mdc (where API conventions live) rather than
AGENTS.md; the convoy file + the new CI gate are sufficient
documentation for the audit trail. Per convoy spec, no new gotcha
entry needed.
No changes to: package.json, package-lock.json, pages/api/**, lib/**,
components/**, scripts/**, test/**, tests/**, .github/workflows/**,
README.md, TESTING_GUIDE.md, playwright.config.js.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
Reflects the merged fix-vercel-deployment-protection-in-ci convoy
(PR #17, squash commit 9a3e077) in repo documentation. Closes the
CI-infra side-effect of P0 #7. Three commits in the convoy: Brief 1
(bypass plumbing per spec), plus two scope expansions discovered
during CI validation (shell-injection hardening, and a Decision-A
shape correction to drop the cookie-bypass param).
.convoys/ship-readiness.md:
- Queued convoys: mark fix-vercel-deployment-protection-in-ci as
RESOLVED 2026-05-24 with 9a3e077. Document the 3-commit reality
(365e9f0 Brief 1 bypass plumbing, b6f8688 shell-injection
hardening of the gate Decide step, 043a6ee dropping
&x-vercel-set-bypass-cookie=true), the empirical metrics
(wait-action: 10-min timeout -> 194ms; workflow runtime:
10+ min -> 59s), the documented expected red on Playwright
smoke (npx playwright test fails because playwright.config.js
doesn't exist yet -- adopt-playwright-smoke owns that), and the
operator-rotation caveat (R6).
AGENTS.md:
- Section 7 Deployment: correct the noun "header" -> "query param
on wait-action's path:" since that's what actually landed per
Decision A. Also document the without-cookie form (the cookie
variant 401s through axios's missing cookie jar) and the
operator re-seed runbook for token rotation.
- Section 7 Deployment: fold in a one-liner about the GitHub
Actions ${{ }}-in-shell-is-injection-vector pattern, with the
env: + quoted-shell fix shape. Picked Section 7 over a new
Gotcha #13 because the existing Gotchas list is dominated by
app-level pitfalls (auth, SQL clients, ESLint), and CI YAML
hardening is naturally co-located with deployment.
.convoys/fix-vercel-deployment-protection-in-ci.md:
- frontmatter status: in-progress -> shipped (added shipped:
2026-05-24)
- new ## As-shipped section: 3-commit reality, Decision-A shape
deviation (we shipped without &x-vercel-set-bypass-cookie=true),
empirical timings (194ms wait, 59s total), remaining-red
attribution to adopt-playwright-smoke, and the
operator-rotation caveat.
No changes to: package.json, lib/**, pages/**, components/**,
scripts/**, .github/workflows/**, .cursor/rules/**, README.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
Doc-drift fix discovered after the fix-layout-default-user cleanup
pass. The bump-next-js convoy shipped on 2026-05-23 as commit
e57ea17, and the commit body explicitly states "Closes P0
ship-blocker #8" — but no dedicated doc-writer pass ever ran for
that convoy, so .convoys/ship-readiness.md still had P0 #8 listed
as open even though Vercel has been deploying main + every PR
successfully since.
Patches only P0 #8 to RESOLVED with the same shape used by P0
#1, #2, #3, #4, #7:
- Title: "— **RESOLVED 2026-05-23**"
- Resolved by: bullet citing e57ea17
- As-shipped: 7-point summary of the bump's actual deliverables
(next 15.4.3->16.2.6, ESLint v9-fallback, typescript devDep,
eslint.config.mjs, scripts.lint update, images.remotePatterns
migration, build verification)
- Side-effects deferred: bump-react, bump-eslint-10, bump-typescript-6,
fix-lint-baseline, App Router migration
- Doc drift note acknowledging this entry was added ~24h late
No code changes. No package.json / lib/** / pages/** / components/**
touched. AGENTS.md was already current (Gotchas #9, #10, #11 already
reflect Next 16 + ESLint v9 + Turbopack post-bump state).
Co-authored-by: Cursor <cursoragent@cursor.com>
Reflects the merged fix-layout-default-user convoy (PR #15) and the
companion CI permissions fix (PR #16) in repo documentation. Also
queues the new fix-vercel-deployment-protection-in-ci convoy that
PR #16 exposed.
.convoys/ship-readiness.md:
- P0 #7: mark RESOLVED 2026-05-24 with squash commit ca302a8.
Document the as-shipped Layout default-null change, the 7-page
sweep, the 5 new regression-lock vitest assertions, and the
queued follow-ups (single-auth-provider, MobileNavigation
cleanup) that stayed explicitly out of scope.
- Queued convoys: add fix-vercel-deployment-protection-in-ci
(P2, CI infra) — PR #16's permissions fix exposed that Vercel
Deployment Protection 401s anonymous CI requests; needs a
bypass-secret plumb to land cleanly. New section also captures
other in-flight follow-ups (rotate-default-admin, cors-tighten,
add-rate-limiting, purge-weak-creds-from-helpers,
single-auth-provider, cleanup-mobile-nav-dead-props,
bump-eslint-10) so the audit trail is centralized.
AGENTS.md:
- § 4 Gotcha #8: mark RESOLVED with commit ref ca302a8. Mirror
the convention used by the prior cleanup commits for #2, #3,
#4, #5 (entry kept, not renumbered).
- § 3 Key conventions: add a new "Layout user prop" bullet
documenting the new default-null + logged-out-CTA contract so
the convention is discoverable from the conventions list, not
just the resolved-gotcha entry.
.convoys/fix-vercel-deployment-protection-in-ci.md (new):
- Queued scaffold. Operator must seed
VERCEL_AUTOMATION_BYPASS_SECRET as a repo secret before the
implementer can run. Decisions to ratify (query param vs.
header), known constraints, acceptance criteria, and
out-of-scope all enumerated.
No changes to: package.json, lib/**, pages/**, components/**,
scripts/**, .github/**, README.md, .cursor/rules/**.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reflects the merged drop-public-setup convoy (PR #13) in repo
documentation. Small focused pass; no behavior changes.
AGENTS.md:
- § 1 auth bullet: replace "seed admin row still ships in
setup-neon-db.js" claim with the new env-var-gated reality and
the R1 operator-rotation caveat.
- § 4 Gotcha #4: mark RESOLVED with commit refs (ff80753 + b63b509),
document the as-shipped behavior, the Brief 2 CJS→ESM Node 22.x
fix, and the R1 operator caveat. Entry kept (not renumbered) per
the same convention used for resolved gotchas #2, #3, #5.
- § 5 Running locally: add ADMIN_INITIAL_PASSWORD to the env-var
template list with a note that setup-db exits 1 if it's unset.
.convoys/ship-readiness.md:
- P0 #3: mark RESOLVED 2026-05-23 with commit refs, document the
full as-shipped behavior including Brief 2's CJS→ESM bonus,
the R1 operator caveat (Decision A — going-forward only),
and the deferred sibling weak-cred references queued for
purge-weak-creds-from-helpers.
.cursor/rules/no-go-zones.mdc:
- Editing rules of thumb: clarify the schema-vs-operational
distinction for scripts/setup-neon-db.js. drop-public-setup
set the precedent that operational changes (env-var gating,
pre-flight validation, module-system fixes) are allowed in
place, while DDL changes still need a separate migration
script. Documented so future agents don't have the same
confusion the drop-public-setup architect did (see Decision B
in .convoys/drop-public-setup.md).
No changes to: package.json, lib/**, pages/**, components/**,
scripts/**, .github/**, README.md (already updated in PR #13).
Co-authored-by: Cursor <cursoragent@cursor.com>
The throwaway bootstrap PR exposed three pre-existing issues that
weren't visible before the pipeline was installed:
1. ESLint had no config (`.eslintrc.json` missing) even though the
`lint` script and deps were both present. `next lint` was prompting
interactively in CI. Added `.eslintrc.json` extending
`next/core-web-vitals` (Next.js Strict).
2. Running lint surfaced ~100 pre-existing errors, including several
real bugs (conditional React hook calls in components/pages).
Marked the CI lint job `continue-on-error: true` with an explicit
TODO so PRs aren't blocked while a follow-up convoy
(fix-lint-baseline) cleans up the codebase. Lint output is still
visible in PR logs.
3. Vercel is platform-blocking every deployment with "Vulnerable
version of Next.js detected" — locked at 15.4.3, latest is 16.2.6.
The last successful Vercel deploy on main was 2025-08-01. Until
Next.js is bumped, every preview-smoke / visual-diff gate is
non-functional. Added as P0 #8 with a new `bump-next-js` convoy at
the front of the launch sequence.
Updated `.convoys/ship-readiness.md`:
- P0 #8: Vercel deploy blocked by Next.js CVE
- P1 #11.5: pre-existing lint baseline
- Launch sequence: prepend `bump-next-js` at step 0, add
`fix-lint-baseline` at step 3.5
Co-authored-by: Cursor <cursoragent@cursor.com>