---
phase: 03-event-write-back-pwa-install
fixed_at: 2026-06-09T15:06:11Z
review_path: .planning/phases/03-event-write-back-pwa-install/03-REVIEW.md
iteration: 2
findings_in_scope: 8
fixed: 8
skipped: 0
status: all_fixed
---

# Phase 3: Code Review Fix Report (Iteration 2)

**Fixed at:** 2026-06-09T15:06:11Z
**Source review:** .planning/phases/03-event-write-back-pwa-install/03-REVIEW.md
**Iteration:** 2

**Summary:**

- Findings in scope: 8 (fix_scope: all — Critical + Warning + Info)
- Fixed: 8
- Skipped: 0

All fixes verified with `tsc --noEmit` AND the full vitest suite in BOTH apps:

- `apps/api`: 108 tests pass (was 103 baseline; +5 new regression tests)
- `apps/pwa`: 145 tests pass (was 141 baseline; +4 new regression tests)
- Typecheck clean in both packages.

## Fixed Issues

### CR-01: Edit-as-move silently strips a recurring series' RRULE

**Files modified:** `apps/api/src/routes/events.ts`, `apps/api/src/broker/outboxWorker.ts`, `apps/api/tests/routes/events.test.ts`, `apps/api/tests/broker/outboxWorker.test.ts`
**Commit:** 5168920
**Applied fix:** Used the review's approach 1 (forward the RRULE from the route). The PATCH edit lookup now also selects `calendarEvents.rawVevent`. In the edit-as-move branch, when the edit payload carries no explicit `recurrence`, the route extracts the source RRULE via `extractRruleString()` and stashes it on the create outbox payload as `_preservedRrule`. The worker's `create` branch now mirrors the `update` branch's recurrence logic: it re-applies `_preservedRrule` when the payload omits `recurrence`, while an explicit `recurrence` (including `'none'`) still wins. Added two worker regression tests (moved event → emitted ICS contains `RRULE:`; explicit `recurrence:'none'` suppresses RRULE even when `_preservedRrule` present) and one route regression test (move stashes the source `FREQ=WEEKLY;BYDAY=MO` on the create row).
**Note:** Logic-sensitive fix. Backed by direct regression tests asserting the RRULE survives the move on both the route side (payload stash) and the worker side (ICS re-apply), so behavior is locked rather than relying on syntax verification alone.

### WR-01: Edit form provides no indication recurrence is locked

**Files modified:** `apps/pwa/src/components/EventForm.tsx`, `apps/pwa/src/components/EventForm.test.tsx`
**Commit:** eed178f
**Applied fix:** Additive helper text only (no logic change). In edit mode, explanatory copy renders beneath the disabled recurrence select: "Repeat can't be changed yet — edits keep the existing schedule." Added two tests (text present in edit mode; absent in create mode).

### WR-02: `handleAllDayToggle` can leave end-date inconsistent

**Files modified:** `apps/pwa/src/components/EventForm.tsx`, `apps/pwa/src/components/EventForm.test.tsx`
**Commit:** eed178f
**Applied fix:** On toggle-on, `endDate` is clamped to `max(startDate, endDate)` deterministically (snaps a behind-end up to the start day) and any stale end-time error from the timed view is cleared. Added a test toggling all-day ON with end behind start, asserting the clamp and clean validation. Per the review's prescribed `max(startDate, endDate)` fix, a genuinely midnight-spanning event (end day after start day) still yields a 2-day all-day span — the clamp only repairs the behind-case, matching the review's suggested fix exactly.

### WR-03: Missing cached etag becomes an unconditional PUT/DELETE

**Files modified:** `apps/api/src/broker/write.ts`
**Commit:** 5b720ff
**Applied fix:** Took the review's minimum (observability). `updateCalendarEvent` and `deleteCalendarEvent` now `console.warn` when dispatched with a null/empty etag, making the unconditional-write (conflict-detection-disabled) path observable instead of silent. The write is not blocked (blocking would strand the user's edit).

### WR-04: `sync-status` masks an earlier failure behind the newest row

**Files modified:** `apps/api/src/routes/events.ts`, `apps/api/tests/routes/events.test.ts`
**Commit:** fd13852
**Applied fix:** The `sync-status` query now orders by a status-priority CASE (`failed`/`dead` rank 0, `pending` rank 1, `done` rank 2) before `createdAt DESC`, so any failed/dead row for the uid is surfaced ahead of a later `done` row. Added a test seeding a dead row, asserting the handler returns `dead` + its error and that the ORDER BY contains the priority CASE expression (scanned via the Drizzle sql `queryChunks` to avoid the circular-structure JSON.stringify pitfall).

### IN-01: `RRULE_PRESETS` round-trip is lossy for parameterized RRULEs

**Files modified:** `apps/api/src/broker/vevent.ts`
**Commit:** f95760e
**Applied fix:** Documentation only. Added a v1-limitation note at `RRULE_PRESETS` explaining that applying a bare preset to a previously-rich rule drops BYDAY/INTERVAL/UNTIL/COUNT, and that recurrence editing must modify the parsed RECUR in place rather than replacing it with a preset.

### IN-02: `parseDateTime` silently rewrites a malformed edit value to today/09:00

**Files modified:** `apps/pwa/src/components/EventForm.tsx`, `apps/pwa/src/components/EventForm.test.tsx`
**Commit:** eed178f
**Applied fix:** `parseDateTime` now returns an `ok` flag. A new `initFormDateTime()` helper falls back to today/09:00 only on the CREATE path (benign default for a new event); in EDIT mode a parse failure leaves the field blank. `validate()` blocks submit when start/end (or time for non-all-day) is blank, surfacing "Couldn't read this event's date — re-open it from the calendar." Added a test: edit mode with an unparseable start leaves the date blank and blocks `updateEvent`.
**Note:** Logic-sensitive (changes validation flow). Backed by a regression test asserting the blank field + blocked submit; CREATE-mode defaults remain covered by existing tests.

### IN-03: Unchecked `as` casts on JSON-parsed outbox payload fields

**Files modified:** `apps/api/src/broker/outboxWorker.ts`, `apps/api/tests/broker/outboxWorker.test.ts`
**Commit:** b8c1864
**Applied fix:** Added a worker-local zod schema (`outboxPayloadSchema`, mirroring `eventFieldsSchema` and `.passthrough()`-ing the CR-01 `_preservedRrule` field). Both the `update` and `create` branches now `safeParse` the JSON payload after parsing and hard-fail the row (no retry) on validation error, so a schema-invalid row can never dispatch `SUMMARY:undefined`/Invalid Date. Added a test: a create row missing `title` is hard-failed and never dispatched.

---

_Fixed: 2026-06-09T15:06:11Z_
_Fixer: Claude (gsd-code-fixer)_
_Iteration: 2_
