wip: phase 03 Gate 2 paused — live bring-up working; edit/delete join fix is the next blocker

This commit is contained in:
Lucas Berger
2026-06-06 22:42:06 -04:00
parent bdbb9b8324
commit 3ae7724df1
2 changed files with 70 additions and 69 deletions
@@ -1,78 +1,75 @@
---
context: phase
phase: 03-event-write-back-pwa-install
task: 1
total_tasks: 3
task: "Gate 2 Part D — edit/delete join fix (diagnosed, not started)"
total_tasks: null
status: in_progress
last_updated: 2026-06-06T01:47:16.059Z
last_updated: 2026-06-07T02:39:57.236Z
---
<current_state>
Phase 03 **gap-closure execution is complete** — all four gap plans (03-09, 03-12, 03-10, 03-11) executed in dependency order, merged to `gsd/v1.0-milestone`, with 92 API + 126 PWA tests green and both `tsc --noEmit` clean. Working tree is clean.
# Critical Anti-Patterns
Work then shifted to an **operational task: spin up the Docker stack for newt/Pangolin external exposure**. This is paused mid-investigation, awaiting two decisions from the user (see Human Actions). The user installed newt into WSL2 and wants to map the stack's ports.
| Pattern | Description | Severity | Prevention Mechanism |
|---------|-------------|----------|---------------------|
| playwright-cli wedges in this WSL2 env | After loading a never-settling page (e.g. the infinite-spinner state), the playwright-cli daemon hangs and even `open`/`run-code` fail afterward. Burned a lot of effort on it. | blocking | Do NOT use playwright-cli for verification here. Verify via `curl` against the tunnel + ask the operator to test in their real browser/incognito. |
| Drizzle: referencing joined-table columns without the join | `events.ts` edit + delete handlers select `calendars.url`/`calendars.userId` from `.from(calendarEvents)` with no `.innerJoin(calendars,...)` → runtime 503 "table calendars is not part of the query". Unit tests mock `db.select()` so they DON'T catch it. | blocking | Any handler selecting another table's columns MUST `.innerJoin` it. Regression tests for write endpoints must exercise the REAL query builder (test DB), not a mocked `db.select()`. |
| `.env` is permission-locked | The Read/Edit/Bash tools are denied on `.env` (and `.env.spike`). | advisory | Hand the operator exact `.env` lines to apply via the `!` prefix; never assume you can read/write it. |
| docker compose recreate drops the newt target ~30s | Every `docker compose up -d` recreates the api container, resetting newt's held TCP connection → tunnel returns 503 "no available server" for ~30s, then self-recovers. | advisory | After any recreate, poll `/health` through the tunnel until 200 before testing. Not a bug — do not chase it. |
<current_state>
Phase 03 **Gate 2 live verification is largely working.** The PWA now loads through Pangolin/newt, real Authelia OIDC login works, and create-event round-trips to Fastmail correctly (right time, right user) after this session's fixes. Working tree is clean (all committed).
**Immediate blocker:** deleting (and latently editing) an event 503s — the `events.ts` edit/delete handlers are missing a `calendars` join. Diagnosed, fix NOT yet applied. The operator paused right as I asked how to land the fix.
</current_state>
<completed_work>
Gap-closure plans (all merged, all SUMMARY.md committed):
- 03-09: Route-layer reachability — zod `title/start/end` contract (CR-01) + real OIDC `iss/sub``users.id` via upsertUser (CR-06). merge `578315c`.
- 03-12: EventForm edit/a11y — WR-03 (blank/recurrence edit), WR-05 (zone-consistent dates), WR-07 (focus trap), IN-03/04. merge `1fc56f4`.
- 03-10: Outbox real VEVENT dispatch — CR-02 (`buildVeventString` wired), CR-03 (fail-closed creds), WR-01 (backoff index), WR-04 (all-day DTEND+1), WR-08, IN-01. merge `416e813`.
- 03-11: Outbox durability — CR-04 (create-before-delete gating), CR-05 (drain concurrency guard), WR-02 (etag re-read). merge `0e4a263`.
Review-finding coverage: **16/19 closed — all 6 critical (CR) resolved.** STATE.md + ROADMAP.md updated and committed (`b3eff3c`).
This session (commits b46b25b → bdbb9b8):
- **Tunnel works** — operator set `newt -mtu 1200` (was 1280 == eth0 underlay; WireGuard overhead blackholed large packets). THIS was the real cause of every "spinner" — the 510KB JS bundle never downloaded. Plus API now serves the full `./public` tree (431ab31), so manifest/sw/icons stop returning HTML.
- **Auth works** — `/api/login` route + redirect (quick task 260606-tv8), `fetchMe` uses `redirect:'manual'` (1adb460), `OIDC_CLIENT_ID=familysync-dev`, `OIDC_SCOPES=openid profile email offline_access`, redirect URI corrected. Operator added `offline_access` to the Authelia client.
- **Bring-up** (b46b25b) — PWA built into the API image (single port :3000), `NODE_ENV=production`, broker credential seeded (reused the spike app password).
- **BUG A (timezone)** + **BUG B (calendar identity)** fixed via /gsd-debug (a9d3de6, session `.planning/debug/write-path-event-bugs.md`). Migration `0001_calendars_user_url_unique.sql` applied to the live DB.
- **Spike cleanup** — deleted obsolete user id=1 ("Dev User") + calendar id=1 + cached events (operator-approved DB op).
- **Backlog 999.2** added — slick unauthenticated-entry (no login flash).
</completed_work>
<remaining_work>
1. **BLOCKING — edit/delete join fix.** `apps/api/src/routes/events.ts`: the `PATCH /:uid/edit` lookup (~line 295) and `DELETE /:uid` lookup (~line 392) select `calendarUrl: calendars.url` + `userId: calendars.userId` from `.from(calendarEvents).where(eq(calendarEvents.uid, uid))` with NO join. Add `.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))` to both (mirror the working `GET /api/events` query at ~line 153). Add a regression test that runs the real query builder.
2. `me.ts` blank displayName — passes the (missing) `email` claim as displayName, never reads `name`/`preferred_username`; `oidc_iss` also blank. Legend name is blank (user id=2 `display_name=''`). May also need Authelia to include name/email in the ID token, or call userinfo.
3. `GET /api/events` has no `userId`/`isShared` filter (returns all users' events) — latent now (1 real user), real bug for a 2nd member.
4. "Syncing" toast not animated / ~27s — UI polish (backlog candidate).
5. Gate 2 remaining: A2 (session persistence), A3 (2nd-member color), B (iOS standalone install+login — device-only), C (5-min SSE smoke — Phase 4 entry gate).
Phase 03:
- **03-08** — Gate 2 live verification (real Authelia OIDC over Pangolin + iOS standalone install + end-to-end Fastmail write round-trips). Human/device checkpoint — NOT a gap plan, cannot be driven by playwright-cli. This is the only incomplete plan; phase stays "In Progress" until it passes.
- Deferred (non-blocking, not scoped into any gap plan): WR-06 (N+1 fetchCalendars perf), IN-02 (resolveDefaultView SSR-guard nit), IN-05 (terminal outbox rows never pruned).
Operational (the paused task):
- Bring up the Docker stack for newt exposure once the two decisions below are made.
The operator was choosing how to land #1: **(a)** batch #1+#2+#3 in one /gsd-quick, **(b)** /gsd-quick just #1, or **(c)** fix #1 inline. Re-offer that.
</remaining_work>
<decisions_made>
- Executed Phase 03 with `--gaps-only`; 03-08 intentionally excluded (human/device Gate 2).
- Left WR-06 / IN-02 / IN-05 open — non-critical; all critical findings closed.
- `newt -mtu 1200` — fixes large-asset blackhole through the WireGuard tunnel.
- Deleted the spike user (id=1) + its calendar/events — obsolete test identity, re-syncable cache.
- `fetchMe` `redirect:'manual'`, serve full `./public`, constrained `OIDC_SCOPES` — see HANDOFF.json.
</decisions_made>
<blockers>
- Docker bring-up paused pending two user decisions (auth mode + PWA serving).
- `OIDC_ISSUER` and `OIDC_CLIENT_SECRET` are **empty** in `.env` → the production/real-OIDC path cannot run yet.
- No PWA build in `apps/api/public` → API serves nothing at `/` (root 404s); `/health` and `/api/*` work.
- Delete/edit events 503 (missing calendars join) — trivial fix, not yet applied.
- playwright-cli broken in this env — verify via curl + operator's browser.
</blockers>
## Required Reading (in order)
1. `docker-compose.yml` + `docker-compose.dev.yml` — stack definition. Base = production target (OIDC unconditional); dev override = dev target, `NODE_ENV=development`, exposes mariadb:3306 + redis:6379.
2. `apps/api/src/index.ts` (lines ~15-61) — `devBypassActive` logic (`NODE_ENV!='production' && DEV_AUTH_BYPASS==='true'`) and the `serveStatic('./public')` PWA-serving + catch-all.
3. `apps/api/Dockerfile` — note: production stage does NOT build the PWA into `./public`; comment says PWA "built and served separately".
4. `.planning/HANDOFF.json` — machine-readable mirror of this state.
## Critical Anti-Patterns
_None discovered through failure this session. The "Exited (255)" on api/redis was a WSL/daemon stop 12h ago, NOT a crash — the api boots cleanly on :3000 (logs show "FamilySync API running on http://localhost:3000", only a harmless `serveStatic: root './public' not found` warning). Do not chase it as a bug._
1. `.planning/HANDOFF.json` — machine-readable mirror of this state.
2. `apps/api/src/routes/events.ts` — the edit (~line 295) + delete (~line 392) handlers missing the `calendars` join; compare to the working GET query (~line 153).
3. `.planning/debug/write-path-event-bugs.md` — the resolved timezone + calendar-identity debug session (context for the write path).
4. `.planning/phases/03-event-write-back-pwa-install/03-GATE2-RESULTS.md` — the Gate 2 checklist (still needs updating with what now passes).
## Infrastructure State
- `familysync-mariadb-1`: **Up (healthy)**, `0.0.0.0:3306->3306`.
- `familysync-api-1`: Exited (255) — stale from WSL stop, not a crash. Boots fine when restarted.
- `familysync-redis-1`: Exited (255) — same; present-but-unused until Phase 4.
- Docker 29.3.1, Compose v5.1.1.
- newt installed in WSL2 (per user) for Pangolin tunnel; the ports it maps are unconfirmed from the repo side.
## Human Actions Pending (blocking the Docker bring-up)
1. **Auth mode** — dev-bypass (`DEV_AUTH_BYPASS=true` + dev compose, works immediately, OIDC skipped) **vs** real OIDC (fill `OIDC_ISSUER`/`OIDC_CLIENT_SECRET` for the actual 03-08 Gate 2 login over Pangolin).
2. **PWA serving** — build PWA into `apps/api/public` (single port :3000 for newt) **vs** vite dev on :5173 (two ports) **vs** API-only for now.
3. (non-blocking) Confirm which ports the Pangolin/newt config actually maps.
- Stack: `docker compose` (production target) — api + mariadb (healthy) + redis up. `/health` 200 locally AND through `https://familysync-dev.bergerhouse.net`.
- newt: systemd service, `-mtu 1200` drop-in applied by operator; WireGuard tunnel carries large transfers now.
- DB: 1 user (id=2, real OIDC, `display_name=''`, color #E8734A); calendars id=2 "Calendar" (509 ev) + id=3 "USA Holidays" (32). `uniq_calendar_user_url(user_id,url)` present.
- OIDC: `client_id=familysync-dev` registered in Authelia with `offline_access`; redirect `https://familysync-dev.bergerhouse.net/callback`.
- An old test event (uid `92dd5a80…`) exists in Fastmail at the WRONG time (written pre-BUG-A-fix) — operator should delete it via the app once delete works.
<context>
Mid-flow when paused: I had just run `AskUserQuestion` with the two decisions above and the user interrupted to `/gsd-pause-work` instead of answering. Nothing was started/changed for the Docker task — purely investigation. The gap-closure work is done and safe.
The whole session was a Gate 2 bring-up that turned into a bug hunt. The keystone was the newt MTU fix — until then the JS bundle couldn't traverse the tunnel, so the app showed a perpetual spinner that looked like (and got misdiagnosed as) auth problems. After that unlocked, live testing surfaced a cascade of real write-path bugs, most now fixed. Remaining work is small and well-understood; the edit/delete join is a 2-line fix gated only on the operator's choice of how to land it.
</context>
<next_action>
Ask the user the two pending decisions (auth mode + PWA serving). Then bring up the stack:
- Dev-bypass path: ensure `DEV_AUTH_BYPASS=true` in `.env`, then `docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build`. If serving the PWA via API, first `pnpm --filter @familysync/pwa build` and place output in `apps/api/public`; otherwise run `pnpm --filter @familysync/pwa dev` on :5173.
- Real-OIDC path: wait for the user to fill `OIDC_ISSUER`/`OIDC_CLIENT_SECRET`, then `docker compose up -d --build` (production), and build the PWA into `apps/api/public` so the root URL serves.
Start with: re-offer the operator the landing choice for the edit/delete join fix (batch #1+#2+#3 / quick-only #1 / inline). Then apply `.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))` to the edit (~line 295) and delete (~line 392) handlers in `apps/api/src/routes/events.ts`, add a real-query regression test, `docker compose up -d --build`, wait for tunnel `/health` 200, and have the operator re-test delete in the browser.
</next_action>