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
--watchflag 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 intoroster.spec.tsand deleteroster-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)