Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
192 lines
9.4 KiB
Markdown
192 lines
9.4 KiB
Markdown
---
|
||
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 `<select>` is disabled — editing recurrence is deferred until the
|
||
occurrence contract exposes it.
|
||
|
||
### WR-02: Default-calendar selection on create is non-deterministic
|
||
|
||
**Files modified:** `apps/api/src/routes/events.ts`
|
||
**Commit:** 5499f83
|
||
**Applied fix:** Added `.orderBy(calendars.id).limit(1)` to the default-calendar query in
|
||
POST `/create`, giving a stable insertion-order default instead of an arbitrary `[0]`.
|
||
|
||
### WR-03: `parseDateTime` all-day check uses the raw `iso`, not the cleaned string
|
||
|
||
**Files modified:** `apps/pwa/src/components/EventForm.tsx`
|
||
**Commit:** d34edec
|
||
**Applied fix:** The all-day regex test and early return now use `clean` (IANA-suffix
|
||
stripped) instead of the raw `iso`, matching the documented strip intent.
|
||
|
||
### WR-04: Worker cron schedules start on bare module import — pollutes the test process
|
||
|
||
**Files modified:** `apps/api/src/index.ts`
|
||
**Commit:** 7bc129f
|
||
**Applied fix:** `startBrokerPoller()` and `startOutboxWorker()` moved out of top level
|
||
into the `isMainModule()` entrypoint guard, so importing `./index.js` in route tests no
|
||
longer registers real `node-cron` schedules or leaks open handles.
|
||
|
||
### WR-05: `index.ts` direct-run guard is fragile and can mis-fire
|
||
|
||
**Files modified:** `apps/api/src/index.ts`
|
||
**Commit:** 22d1bc2
|
||
**Applied fix:** Replaced the basename-tail `endsWith` heuristic with
|
||
`isMainModule()` comparing `fileURLToPath(import.meta.url)` against
|
||
`realpathSync(process.argv[1])` (symlink-resolved), guarded by try/catch.
|
||
|
||
### WR-06: Edit-as-move create-412 dead-ends the move with no retry path
|
||
|
||
**Files modified:** `apps/api/src/broker/outboxWorker.ts`, `apps/pwa/src/components/SyncStateToast.tsx`
|
||
**Commit:** 1c71f8c
|
||
**Status:** fixed: requires human verification (UX/conflict-flow logic)
|
||
**Applied fix:** When a create row carrying a `groupId` (edit-as-move) hits 412, the
|
||
worker now writes a distinct `move-failed:` `lastError` (no `'412'` substring).
|
||
`SyncStateToast` detects it (`error.startsWith('move-failed')`), routes it away from the
|
||
etag-conflict copy, and shows "Couldn't move the event. Open it and save again." No
|
||
contract change — surfaced via the existing `sync-status` `error` field.
|
||
|
||
### IN-01: `triggerTargetedResync` re-loads and re-decrypts the credential per row
|
||
|
||
**Files modified:** `apps/api/src/broker/outboxWorker.ts`
|
||
**Commit:** 95f9d8c
|
||
**Applied fix:** `triggerTargetedResync` accepts an optional per-drain-cycle
|
||
`Map<number, FastmailClient>` cache; `runOutboxDrain` creates one per cycle and passes it
|
||
to both call sites, so each member's credential is decrypted at most once per cycle
|
||
(narrows the decrypted-password-in-memory window, T-03-13). Cache is discarded when the
|
||
drain returns.
|
||
|
||
### IN-02: Unknown-status responses retried for the full backoff window before giving up
|
||
|
||
**Files modified:** `apps/api/src/broker/outboxWorker.ts`
|
||
**Commit:** e29d6c1
|
||
**Status:** fixed: requires human verification (error-classification logic)
|
||
**Applied fix:** `dispatchRow` now classifies any unmapped 4xx (status 400–499, after the
|
||
explicit 408/429 transient set and 400/401/403 hard-fail set are handled) as a hard fail,
|
||
so permanent client errors (405/409/422) settle immediately instead of burning the retry
|
||
budget. 5xx, network, and truly unknown statuses still fall through to transient.
|
||
|
||
### IN-03: `InstallPrompt` reads `localStorage` synchronously without try/catch
|
||
|
||
**Files modified:** `apps/pwa/src/components/InstallPrompt.tsx`
|
||
**Commit:** 7e4ea71
|
||
**Applied fix:** Added guarded `readDismissed()` / `persistDismissed()` helpers
|
||
(try/catch, mirroring `calendarStore.ts`) used by the `useState` initializer and
|
||
`dismiss()`, so a throwing `localStorage` (private mode / SSR) degrades to "not dismissed"
|
||
instead of crashing the component on mount.
|
||
|
||
### IN-04: `resolveUserId` typed as `any`
|
||
|
||
**Files modified:** `apps/api/src/routes/events.ts`
|
||
**Commit:** 6d2fd79
|
||
**Applied fix:** Parameter typed as Hono's `Context` (imported as a type) instead of
|
||
`any`, removing the eslint-disable. `c.get('user')` resolves through the existing
|
||
`ContextVariableMap` augmentation in `auth/devBypass.ts` and `getAuth(c)` accepts a
|
||
`Context`. Used `Context` rather than the reviewer's literal
|
||
`Context<{ Variables: { user?: { id: number } } }>` because the latter would conflict
|
||
with the global `ContextVariableMap` augmentation (which types `user` non-optionally as
|
||
the DEV_USER shape).
|
||
|
||
## Skipped Issues
|
||
|
||
### IN-05: `deleteCalendarEvent` relies on tsdav ignoring `data: ''`
|
||
|
||
**File:** `apps/api/src/broker/write.ts:93`
|
||
**Reason:** skipped: reviewer specifies "None required for v1; note on the tsdav upgrade
|
||
checklist." No source change is warranted — the finding asks for a process/checklist note,
|
||
not a code fix. The existing inline comment already documents the dependency on tsdav
|
||
internals. Flagged here so the developer can add a tsdav-upgrade-checklist entry.
|
||
**Original issue:** Passes an empty `data` placeholder because tsdav requires the
|
||
`DAVCalendarObject` shape. Relies on tsdav internals; a future version validating `data`
|
||
would break this silently.
|
||
|
||
---
|
||
|
||
_Fixed: 2026-06-09_
|
||
_Fixer: Claude (gsd-code-fixer)_
|
||
_Iteration: 1_
|