deckhearth/.convoys/ship-readiness.md
Randall Stillwell 1944b1ed48 bootstrap: agent pipeline v0.5.0 + ship-readiness review
Installs the three-layer agent-pipeline scaffold (https://github.com/varutasu/agent-pipeline @ v0.5.0):

L1 — Context (curated brain)
- AGENTS.md: orientation, conventions, 8 explicit gotchas
- .cursor/rules/: no-go-zones, api-routes, auth-and-permissions,
  db-and-schema, ui-and-theming, schema-map
- .cursor/skills/: add-api-route, add-page recipes
- docs/agent-context/README.md: layer explainer
- docs/SCHEMA_MAP.md: hand-curated Neon Postgres reference
  (replaces Prisma schema map since stack is raw SQL)

L2 — Subagent roles (copied verbatim from upstream templates)
- 9 .cursor/agents/role-*.md files: Conductor, IA-Architect,
  UX-Reviewer, Architect, Implementer, Reviewer,
  Design-System-Auditor, A11y-Auditor, Doc-Writer

L3 — Pipeline scaffolding (Vercel variant)
- CI: lint + schema-map-drift only (no duplicate build —
  Vercel handles it). Test job commented out until vitest lands.
- preview-smoke + visual-diff via wait-for-vercel-preview
- pr-health-rollup sticky comment aggregator
- agent-context-drift weekly cron
- PULL_REQUEST_TEMPLATE, CODEOWNERS (auth/admin paths tagged)
- .convoys/ folder + seed ship-readiness.md review
- lib/flags/index.js (JS — converted from TS template)
- scripts/wt.sh (Cursor 3.2 deprecation stub),
  scripts/log-convoy-event.sh
- tests/smoke/app.smoke.spec.ts (Playwright skeleton)

Manifest
- .agent-context-manifest.yml: tracks 31 artifacts by sha256
  for future sync-agent-context drift detection

Review
- .convoys/ship-readiness.md: 16 findings (7 P0 ship-blockers,
  5 P1 quality-bar, 4 P2 refactor, P3 UX/IA/a11y/docs) with
  proposed 13-convoy launch sequence.

No production code changed in this commit. All findings in
the ship-readiness review will be addressed in follow-up convoys
starting with fix-auth-bypass.

Structural brain: user-code-review-graph MCP has indexed the
codebase (122 files, 628 nodes, 5602 edges, 11 communities,
84 flows). Per-developer; not committed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 02:31:26 -05:00

232 lines
18 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
- **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).
- **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
- **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.
- **Owns:** `role-architect` + `role-implementer`.
### 3. Default admin credentials in seed + README
- **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".
- **Owns:** `role-implementer`.
### 4. Dev-only test endpoints shipped to production
- **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.
- **Owns:** `role-implementer`.
### 5. CORS `Access-Control-Allow-Origin: *` on auth endpoints
- **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).
- **Owns:** `role-implementer`.
### 6. No rate limiting anywhere
- **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).
- **Owns:** `role-architect` (pattern) → `role-implementer` (per-route).
### 7. Layout default-prop leaks maintainer email
- **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).
- **Owns:** `role-implementer`.
## 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.
2. Install `@playwright/test`. Wire up `tests/smoke/app.smoke.spec.ts` (already drafted; needs `playwright.config.ts`).
3. Re-enable the `test:` job in `.github/workflows/ci.yml` (commented out at install time).
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.
- **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).
### 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.
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.
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.
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: ~13 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.
## 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.