--- phase: 03-event-write-back-pwa-install reviewed: 2026-06-09T00:00:00Z depth: standard files_reviewed: 29 files_reviewed_list: - apps/api/src/broker/outboxWorker.ts - apps/api/src/broker/sync.ts - apps/api/src/broker/vevent.ts - apps/api/src/broker/write.ts - apps/api/src/db/schema.ts - apps/api/src/index.ts - apps/api/src/routes/events.ts - apps/api/tests/broker/outboxWorker.test.ts - apps/api/tests/broker/vevent.test.ts - apps/api/tests/broker/write.test.ts - apps/api/tests/routes/events.test.ts - apps/pwa/index.html - apps/pwa/package.json - apps/pwa/src/api/client.test.ts - apps/pwa/src/api/client.ts - apps/pwa/src/components/CalendarShell.tsx - apps/pwa/src/components/DeleteConfirmationDialog.test.tsx - apps/pwa/src/components/DeleteConfirmationDialog.tsx - apps/pwa/src/components/EventDetailPopover.test.tsx - apps/pwa/src/components/EventDetailPopover.tsx - apps/pwa/src/components/EventForm.test.tsx - apps/pwa/src/components/EventForm.tsx - apps/pwa/src/components/InstallPrompt.test.tsx - apps/pwa/src/components/InstallPrompt.tsx - apps/pwa/src/components/SyncStateToast.test.tsx - apps/pwa/src/components/SyncStateToast.tsx - apps/pwa/src/store/calendarStore.ts - apps/pwa/vite.config.ts - apps/pwa/vitest.config.ts findings: critical: 1 warning: 4 info: 3 total: 8 status: issues_found --- # Phase 3: Code Review Report (Re-Review, Iteration 2) **Reviewed:** 2026-06-09 **Depth:** standard **Files Reviewed:** 29 **Status:** issues_found ## Summary This is a re-review of the event write-back + PWA-install phase after a 13-item fix pass. I verified each of the previously flagged fixes the orchestrator called out: - **CR-01 / CR-02 member-scoped lookups** — VERIFIED FIXED. `events.ts` PATCH/DELETE now scope the `calendarEvents` lookup to the acting member's writable set and add a deterministic `ORDER BY (calendars.userId = currentUserId) DESC LIMIT 1` (events.ts:340-347, 453-460). `outboxWorker.ts`'s fresh-etag re-read now joins `calendars` and filters on `calendars.userId = row.userId AND calendars.url = row.calendarUrl` (outboxWorker.ts:228-239), so a shared-account duplicate uid can no longer resolve to the wrong member's etag. - **CR-03 all-day inclusive/exclusive DTEND** — VERIFIED FIXED and now symmetric. `vevent.ts:106-118` advances the inclusive end by one UTC day on write; `EventForm.tsx:86-96` `exclusiveEndToInclusiveDate()` rolls it back on pre-fill. The round-trip no longer grows multi-day all-day spans. `vevent.test.ts:140-160` asserts DTEND = DTSTART + 1. - **WR-01 RRULE preserve-on-edit** — PARTIALLY FIXED. The same-calendar `update` path correctly preserves the stored RRULE (`outboxWorker.ts:244-248` reads `rawVevent`, extracts the RRULE, re-applies when the payload omits `recurrence`). **The edit-as-move path (D-04) still silently strips recurrence** — see CR-01. This is a real, demonstrable correctness regression of exactly the class WR-01 set out to prevent, so it is filed as a BLOCKER. Other fixes (backoff index `outboxWorker.ts:533-535`, fail-closed credentials `outboxWorker.ts:163-167`, durable create-before-delete `outboxWorker.ts:427-464`, move-failed toast copy `SyncStateToast.tsx:53-58`, localStorage guards `InstallPrompt.tsx:284-298`) are present and correct. ## Critical Issues ### CR-01: Edit-as-move silently strips a recurring series' RRULE **File:** `apps/api/src/broker/outboxWorker.ts:267-297`, `apps/api/src/routes/events.ts:371-401` **Issue:** WR-01 was fixed only for the same-calendar `update` branch. When a recurring event is edited *and moved to a different calendar*, the PATCH handler (`events.ts:371-398`) enqueues a `delete` of the old object plus a `create` with a brand-new `newUid` and the edit payload. The edit payload omits `recurrence` by design (`EventForm.tsx:323`; the recurrence picker is disabled in edit mode). The worker's `create` branch then builds the VEVENT with: ```ts rruleString: fields.recurrence && fields.recurrence !== 'none' ? RRULE_PRESETS[fields.recurrence as string] : undefined, // ← recurrence absent → undefined → no RRULE ``` Unlike the `update` branch, the `create` branch performs **no** `rawVevent` read and **no** `extractRruleString` fallback. The original event's RRULE lives in `calendar_events` under the OLD uid/calendar; the create uses `newUid` and never reads it. Net effect: moving any recurring event to another calendar converts the whole series into a single one-off occurrence on Fastmail — silent data loss — and the original series is deleted once the paired delete runs. This is the identical failure mode WR-01 was meant to eliminate, on a different code path. **Fix:** Carry the existing RRULE through the move. Two viable approaches: 1. In `events.ts`, have the edit lookup also select `rawVevent`, extract the RRULE, and stash it on the create outbox row so the worker re-applies it: ```ts // events.ts — add rawVevent to the eventRow select, then in the move branch: const preservedRrule = extractRruleString(eventRow.rawVevent ?? '') await tx.insert(calendarOutbox).values({ /* ...create row... */ payload: JSON.stringify({ ...payload, _preservedRrule: preservedRrule }), groupId, }) ``` …and in the worker `create` branch, fall back to `fields._preservedRrule` when `recurrence` is absent. 2. Or, in the worker `create` branch, when the row has a `groupId` (move) and the payload lacks `recurrence`, look up the RRULE from the sibling delete row's original uid/calendar via `calendarEvents.rawVevent` and feed it to `buildVeventString`, mirroring `outboxWorker.ts:244-248`. Add a regression test: move a recurring event → assert the created ICS contains `RRULE:`. ## Warnings ### WR-01: Edit form cannot edit recurrence and provides no way to remove an RRULE **File:** `apps/pwa/src/components/EventForm.tsx:311-327, 715-742` **Issue:** The recurrence `