--- phase: 03-event-write-back-pwa-install fixed_at: 2026-06-09T00:00:00Z review_path: .planning/phases/03-event-write-back-pwa-install/03-REVIEW.md iteration: 1 findings_in_scope: 14 fixed: 13 skipped: 1 status: partial --- # Phase 3: Code Review Fix Report **Fixed at:** 2026-06-09 **Source review:** .planning/phases/03-event-write-back-pwa-install/03-REVIEW.md **Iteration:** 1 **Summary:** - Findings in scope: 14 (fix_scope: all — Critical + Warning + Info) - Fixed: 13 - Skipped: 1 **Note on recovery:** a prior `--fix` run was interrupted (orphan worktree `/tmp/sv-03-reviewfix-uxjhc1` + branch `gsd-reviewfix/03-53993` + recovery sentinel). That run's 3 commits had mismatched finding labels and its branch had diverged from the current branch tip (which had advanced with docs commits, making a fast-forward impossible). Per the recovery protocol the orphan worktree/branch/sentinel were cleaned up and all fixes were re-applied fresh from the current branch tip. All 13 commits below are new. **Verification environment:** the isolated worktree had no `node_modules` (gitignored, not carried into a fresh worktree). `node_modules` from the main repo were symlinked in so `tsc --noEmit` could resolve dependencies for Tier-2 syntax/type checks. The symlinks are gitignored and were never committed. Every fix was Tier-2 verified (full `tsc --noEmit` per affected package, clean). ## Fixed Issues ### CR-01: Event edit/delete ownership check resolves an arbitrary member's row (uid-only lookup) **Files modified:** `apps/api/src/routes/events.ts` **Commit:** 54addb1 **Status:** fixed: requires human verification (ownership/authorization logic) **Applied fix:** Both the PATCH `/:uid/edit` and DELETE `/:uid` lookups now scope the `calendarEvents` → `calendars` join to the acting member's writable set (`or(calendars.userId = currentUserId, calendars.isShared)`), add `orderBy(sql\`(calendars.userId = currentUserId) desc\`)` so the user's own row ranks ahead of a shared/other copy, and `limit(1)` for determinism. This stops `[0]` from resolving to another member's calendar row for a shared-account uid (D-16). ### CR-02: Outbox WR-02 "freshest etag" re-read also queries uid-only **Files modified:** `apps/api/src/broker/outboxWorker.ts` **Commit:** a596f52 **Status:** fixed: requires human verification (etag-selection logic) **Applied fix:** The pre-PUT freshest-etag re-read now joins through `calendars` and filters on the outbox row's own `userId` + `calendarUrl` with `limit(1)`, so the etag used in `If-Match` belongs to the writing member's calendar instead of an arbitrary shared-account row. `calendars` added to the schema import. ### CR-03: All-day end date exclusive on write but inclusive on edit pre-fill **Files modified:** `apps/pwa/src/components/EventForm.tsx` **Commit:** f645644 **Status:** fixed: requires human verification (date-arithmetic / data-correctness) **Applied fix:** Added `exclusiveEndToInclusiveDate()` (DST-safe UTC-component subtraction) and apply it when pre-filling the end-date input for all-day occurrences — both in the initial `useState` and the open/reset effect. Keeps `occurrence.end` exclusive everywhere (reviewer option a); `buildVeventString` still rolls forward to exclusive at the ICS boundary, so a re-edit no longer grows the span by a day. **Note:** the reviewer also suggested a regression test (edit an all-day multi-day event twice, assert the span is stable). Not added — flagged for the developer. ### WR-01: Recurrence silently reset to `none` on every edit — data loss **Files modified:** `apps/api/src/broker/outboxWorker.ts`, `apps/api/src/broker/vevent.ts`, `apps/pwa/src/api/client.ts`, `apps/pwa/src/components/EventForm.tsx` **Commit:** 02aa407 **Status:** fixed: requires human verification (data-loss-prevention logic) **Applied fix:** Coordinated change so an edit no longer strips a recurring series: - `vevent.ts`: new `extractRruleString()` parses the existing RRULE from a stored VEVENT. - `outboxWorker.ts` (update path): when the payload carries no explicit `recurrence`, the freshest-etag query also reads `rawVevent` and preserves the existing RRULE; an explicit recurrence value (including `'none'`) still overrides. - `client.ts`: `CreateEventPayload.recurrence` made optional (matches the API Zod schema, which already had it optional). - `EventForm.tsx`: on edit, `recurrence` is omitted from the payload (signals "unchanged") and the recurrence `