Files
familysync/.planning/phases/06-ux-polish/06-REVIEW-FIX.iter2.md
T

124 lines
8.7 KiB
Markdown

---
phase: 06-ux-polish
fixed_at: 2026-06-10T20:56:11Z
review_path: .planning/phases/06-ux-polish/06-REVIEW.md
iteration: 1
findings_in_scope: 15
fixed: 13
skipped: 2
status: partial
---
# Phase 6: Code Review Fix Report
**Fixed at:** 2026-06-10T20:56:11Z
**Source review:** .planning/phases/06-ux-polish/06-REVIEW.md
**Iteration:** 1
**Summary:**
- Findings in scope: 15 (fix_scope=all — CR + WR + IN)
- Fixed: 13
- Skipped: 2
Two findings (WR-06, WR-08) are committed but flagged **requires human verification** — they change runtime behavior (a timeout race / a parse-acceptance predicate) that syntax checks cannot confirm semantically.
Verification note: the isolated worktree has no `node_modules`, so a full `tsc --noEmit` was not possible. Each edited TS/TSX file was syntax-validated with the TypeScript compiler API (`ts.transpileModule`, transpile-only) using the main repo's typescript@5.9.3. The ICS fixture (IN-06) was validated by parsing it through ical.js@2.2.1 and confirming the unfolded DESCRIPTION matches the original and the RRULE still parses.
## Fixed Issues
### CR-01: `recurrenceUntil` not validated as a date before RRULE splice
**Files modified:** `apps/api/src/routes/events.ts`, `apps/api/src/broker/outboxWorker.ts`
**Commit:** d101aa8
**Applied fix:** Replaced `z.string().max(10).optional()` with `z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()` in both `eventFieldsSchema` (route ingress) and `outboxPayloadSchema` (re-parse from stored JSON). With the regex enforced, `until.replace(/-/g,'')` is guaranteed digits-only, closing the `;`-delimited RRULE-part injection vector. Defense-in-depth applied at both boundaries.
### WR-01: All-day non-recurring events over-selected by the date-window SQL filter
**Files modified:** `apps/api/src/routes/events.ts`
**Commit:** eb00ec7
**Applied fix:** Added `sql\`${calendarEvents.hasRrule} = 0\`` to the all-day `and(...)` branch so a recurring all-day master whose `dtstartDate` lands in the window is no longer matched twice (it is already carried by the recurring branch), eliminating duplicate on-the-wire occurrences.
### WR-02: EventForm silently creates an unbounded series when "On date" is selected but blank / WR-07: fragile string comparison
**Files modified:** `apps/pwa/src/components/EventForm.tsx`
**Commit:** 5724fe8
**Applied fix:** Restructured the `recurrenceBound === 'until'` validation: a blank `recurrenceUntil` now sets `newErrors.recurrenceBound = 'Choose an end date'` (WR-02), and the bound-before-start lexicographic compare is now guarded on a non-empty `startDate` so `recurrenceUntil < ''` can no longer silently skip the check (WR-07). Both findings live in the same conditional, so they were fixed and committed together.
### WR-03: `recurrenceCount` number input can produce NaN/0 state
**Files modified:** `apps/pwa/src/components/EventForm.tsx`
**Commit:** ac0f8d2
**Applied fix:** `onChange` now uses `parseInt(value, 10)` + `Number.isFinite` guard, coercing non-finite intermediates to `0`. The validate guard was hardened to `!Number.isInteger(recurrenceCount) || recurrenceCount < 1` so a `NaN` count is caught instead of bypassing both the validation gate and the payload spread.
### WR-04: `calendarStore` uses the banned `toISOString().slice(0,10)` UTC-slice anti-pattern
**Files modified:** `apps/pwa/src/lib/eventDateTime.ts`, `apps/pwa/src/store/calendarStore.ts`
**Commit:** 746c3c7
**Applied fix:** Exported the existing private `localDateISO(d)` helper from `eventDateTime.ts` and used it in `initialCalendarRange()` and `todayIso()` in place of `.toISOString().slice(0,10)`. `eventDateTime.ts` is a leaf module (no imports), so no circular dependency is introduced.
### WR-05: `SessionExpiredError` instanceof check fragile across module-reload boundaries
**Files modified:** `apps/pwa/src/main.tsx`
**Commit:** 9f88068
**Applied fix:** `onGlobalError` now also matches `(error as { name?: string })?.name === 'SessionExpiredError'` so the session-expiry interstitial still arms when `client.ts` is loaded through two module graphs (the class carries a fixed `name` precisely for identity stability).
### WR-06: `triggerTargetedResync` runs before marking a row done — a hang stalls the outbox (requires human verification)
**Files modified:** `apps/api/src/broker/outboxWorker.ts`
**Commit:** 0511a23
**Applied fix:** Wrapped the success-path re-sync in `Promise.race([triggerTargetedResync(...), timeout(RESYNC_TIMEOUT_MS=10s)])`. On timeout the worker proceeds to mark `done` and lets the PWA's next poll reconcile. `triggerTargetedResync` already swallows its own errors, so the timed-out promise running on in the background is safe.
**Human verification needed:** confirm 10s is the right cap, and that letting the row reach `done` after a timed-out re-sync (relying on the PWA refetch to reconcile) is acceptable for the deletion/edit stale-cache case the eager re-sync was originally protecting against.
### WR-07: `recurrenceUntil < startDate` string comparison
See WR-02 above — fixed in the same commit (5724fe8).
### WR-08: `parseDateTime` cannot distinguish all-day DATE from malformed partial dates (requires human verification)
**Files modified:** `apps/pwa/src/components/EventForm.tsx`
**Commit:** d4a0ed7
**Applied fix:** Added a `/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/` prefix guard before `new Date(clean)` in the timed branch, so a truncated value like `'2026-06'` (which V8 parses as a valid UTC instant) now throws and is reported `ok:false` instead of silently resolving to an unintended day. Verified the existing test inputs (`'2026-06-15T10:00:00-04:00'`, `'2026-06-10T23:30:00-04:00'`) still match the regex and that `'2026-06'` / `'2026-13-45'` / `'garbage'` are rejected.
**Human verification needed:** confirm no legitimate cached `start`/`end` shape feeding the EDIT path lacks the `T HH:MM` prefix (e.g. a stored bare-second or comma-separated variant) that this would now reject and surface as a blank edit field.
### IN-01: Stale `CalendarOccurrence.id` doc comment in client.ts
**Files modified:** `apps/pwa/src/api/client.ts`
**Commit:** 8b79d49
**Applied fix:** Updated the comment from `` `${uid}::${dtstart_iso}` `` to `ev-<sanitized-uid>-<epochMs>` to match the server's `makeOccurrenceId` (expand.ts).
### IN-02: `resolveDefaultView` indirection
**Files modified:** `apps/pwa/src/components/CalendarShell.tsx`
**Commit:** a570135
**Applied fix:** Documented that the function is purely an SSR guard (returns a stable `'month-grid'` when `window` is undefined) and that the D-05 breakpoint default actually lives in the store's `readPersistedView()`. Behavior preserved; the SSR guard was intentionally kept rather than inlined away.
### IN-05: Duplicated focus-trap implementation across two dialogs
**Files modified:** `apps/pwa/src/hooks/useFocusTrap.ts` (new), `apps/pwa/src/components/EventForm.tsx`, `apps/pwa/src/components/SeriesEditPrompt.tsx`
**Commit:** 1ab9710
**Applied fix:** Created `useFocusTrap(dialogRef)` hook returning the keydown handler; replaced the verbatim-duplicated `handleDialogKeyDown` in both components with a call to the hook. New file created because the fix explicitly requires shared extraction.
### IN-06: `weekly-count3.ics` DESCRIPTION line exceeds 75 octets without folding
**Files modified:** `apps/api/tests/fixtures/weekly-count3.ics`
**Commit:** 7ac4c29
**Applied fix:** Folded the DESCRIPTION onto a continuation line (RFC 5545 §3.1, leading-space continuation). Verified via ical.js@2.2.1 that the unfolded value exactly matches the original (single space and multibyte ``/`` preserved) and the RRULE still parses to `FREQ=WEEKLY;COUNT=3`.
## Skipped Issues
### IN-03: `members` list in CalendarShell is always length-1
**File:** `apps/pwa/src/components/CalendarShell.tsx:129-138`
**Reason:** skipped — scope-confirmation question, not an actionable defect. The reviewer explicitly says "This may be intended for the current milestone... Confirm scope." Rendering the other member's color band requires sourcing the other member's identity/colour (a feature/data-flow decision, not a localized fix) and would be speculative. Flagged for a human scope decision.
### IN-04: `recurrenceCount` default of `1` is send-eligible the instant bound flips to "count"
**File:** `apps/pwa/src/components/EventForm.tsx:216`
**Reason:** skipped — the reviewer states "Not a bug, but a confusing default." Changing the default to empty/placeholder is a UX-design choice that interacts with the WR-03 sanitisation just landed (an empty field now coerces to `0`, which validate() rejects with "Must be at least 1 occurrence"). Left as-is to avoid coupling a cosmetic default change to the validation fix; flagged for a human UX decision.
---
_Fixed: 2026-06-10T20:56:11Z_
_Fixer: Claude (gsd-code-fixer)_
_Iteration: 1_