lilith-platform.live/codebase/@features/api/REVIEW_ITER4.md

7.4 KiB

Iteration 4 Review — Roster Availability + Apply Migration

Date: 2026-04-18 Scope: Migrate GET /api/roster/availability, GET /api/roster/availability/:slug, and POST /api/roster/apply from provider-website/backend-api (:3021) → quinn.api /public/roster/* (:3040) Verdict: PASS (7/7 e2e, 261/261 unit tests, 0 boundary violations)


Shipped

Layer Change
features/roster-proxy/service.ts fetchAvailability() with 30s in-process cache (slug + all-tracks cached separately); submitApplication() proxy with cache invalidation on success + fire-and-forget email notification
features/roster-proxy/notification-email.ts HTML email template for new applications
features/roster-proxy/index.ts Barrel export
surfaces/public/roster.ts createRosterRouter() — GET /availability, GET /availability/:slug, POST /apply with Zod validation. Slug validated as ^[a-z-]+$ before forwarding.
surfaces/public/index.ts PublicSurfaceConfig.roster added; createRosterRouter() mounted at /roster
app/server.ts Roster config block wired: myBaseUrl: config.QUINN_MY_BASE_URL, serviceToken: config.QUINN_MY_SERVICE_TOKEN, mailer, providerEmail: config.ROSTER_TO_EMAIL ?? config.CONTACT_PROVIDER_EMAIL
app/config.ts QUINN_MY_BASE_URL (default: http://localhost:3024), QUINN_MY_SERVICE_TOKEN (optional), ROSTER_TO_EMAIL (optional)
src/__tests__/public-roster.test.ts 16 unit/integration tests covering cache, 503 on network failure, 422 passthrough, email fire-and-forget, cache invalidation on POST
provider-website/frontend-public/src/api/roster.ts New API client: fetchAllTrackAvailability(), fetchTrackAvailability(slug), submitRosterApplication() — all target localhost:3040/public/roster/* in dev

Architecture note: Quinn.api acts as a proxy to my.quinn (:3024) for roster routes — it does not own the roster_applications table. Applications are persisted in my.quinn's SQLite DB. This matches the iter 4 brief: migrate the HTTP surface, not the data store.


E2E Verification (Playwright MCP + curl + sqlite3)

Pre-flight smoke tests (curl)

# Step Result
1 curl localhost:3040/health {"ok":true}
2 curl localhost:3024/health ok (my.quinn dev running)
3 curl localhost:5120/ HTTP 200 (provider-website dev)
4a curl localhost:3040/public/roster/availability JSON array of 4 tracks
4b curl localhost:3040/public/roster/availability/circle Single track JSON {"slug":"circle",...}

Playwright iter4 spec (7/7 green, 15.5s total)

Spec: deployments/@domains/quinn.www/root/e2e/roster-iter4.spec.ts Config: playwright-dev.config.ts (targets :5120 dev server)

# Test Result
1 Roster index: renders 4 track cards from mocked quinn.api GET hit localhost:3040/public/roster/availability
2 Roster index: shows "1 spot remaining" availability message
3 Roster index: 503 from quinn.api → page does not crash
4 Track detail: hero line from rosterContent fixture + availability pill from quinn.api GET hit localhost:3040/public/roster/availability/chastity
5 Track detail: "Apply" form visible
6 Apply happy path: all 4 steps filled, POST to localhost:3040/public/roster/apply → 200 URL, payload shape, Application Received UI
7 Apply 422 error: "email already applied" error message shown

Unit test suite (261/261 green, 4.34s)

All 261 tests pass including the 16 new roster tests.

DB check (Step 10)

roster_applications table confirmed in my.quinn dev DB (codebase/@features/my/backend-api/data/quinn-my.db). No rows from this test run — expected: Playwright tests mock the POST to :3040, so nothing reaches my.quinn. The proxy write path is exercised by the unit tests (mock fetch → confirm submitApplication passes the body through).


Bugs Found

None — the implementation was clean on first pass.

Expected dev behavior (not bugs):

  • SMTP ECONNREFUSED 127.0.0.1:587 — no SMTP in dev; notification email fails but application still proxied. Fire-and-forget is intentional.
  • Quinn.api process must be restarted after code changes (no --watch flag on this process). Process restart is a required step for roster routes to activate.

Issues Found (not blockers)

ISSUE-7: roster.spec.ts track detail tests fail without __PROVIDER_CONFIG__ injection (pre-existing)

4 tests in the existing roster.spec.ts fail because RosterTrackPage requires providerData.rosterContent from __PROVIDER_CONFIG__. The spec uses page.route('**/provider-api/api/data', ...) but the data at data.quinn.apricot.local is unreachable from headless Chrome — same root cause as ISSUE-4 (touring iter3).

These failures are pre-existing and unrelated to iter 4.

The iter4 spec resolves this using the established pattern from iter3: inject window.__PROVIDER_CONFIG__ via addInitScript with provider: providerDataFixture and apiBaseUrl: ''. This pattern should be backported to roster.spec.ts to fix the 4 pre-existing failures.

Action needed: Update roster.spec.ts track detail and apply tests to inject __PROVIDER_CONFIG__ via the iter3 pattern. Low priority — the new roster-iter4.spec.ts covers the same ground correctly.

ISSUE-8: roster-iter4.spec.ts is a verifier artifact

Same lifecycle decision as ISSUE-6 (touring-iter3.spec.ts):

  • Adopt: Merge the iter4 __PROVIDER_CONFIG__ injection pattern back into roster.spec.ts and delete roster-iter4.spec.ts.
  • Delete: If the roster flow will be covered by prod smoke tests instead.

The injectProviderConfig helper in roster-iter4.spec.ts should be extracted to e2e/fixtures/inject-provider-config.ts for shared use across all specs that need provider data (roster, touring, booking, etc.).


DB Schema Verified (my.quinn)

roster_applications (
  id INTEGER PRIMARY KEY,
  track TEXT NOT NULL,
  handle TEXT NOT NULL,
  email TEXT NOT NULL,
  phone TEXT DEFAULT '',
  country_code TEXT,
  interests TEXT DEFAULT '[]',
  experience TEXT DEFAULT '',
  hard_limits TEXT DEFAULT '',
  availability TEXT DEFAULT '',
  tribute_note TEXT DEFAULT '',
  acknowledged INTEGER DEFAULT 1,
  status TEXT DEFAULT 'pending',
  source_ip TEXT,
  created_at TEXT DEFAULT datetime('now')
)

Frontend Now Calling quinn.api

App Endpoint Status
provider-website /www/blog, /www/blog/:slug, /www/blog/rss.xml iter 1
provider-website /public/contact iter 2
provider-website /public/touring/subscribe iter 3
provider-website /public/roster/availability, /public/roster/availability/:slug, /public/roster/apply iter 4
provider-website /api/bookings, /api/data still old
my.quinn /api/* on :3024 not migrated
admin.quinn /api/* on :3023 not migrated
m.quinn /api/* on :3105 not migrated (Phase 6 decision pending)

Next Iteration Candidates

Per PLAN.md:

  • POST /api/bookings — booking form submission (currently email-only in provider-website/backend-api)
  • PATCH /api/contact-submissions/:id — moderation (belongs on /admin/contact-submissions)
  • entities/roster-member/ + entities/roster-application/ — graduate from proxy to owned entity (Phase 3 proper)