The CR-01 fix appended .orderBy().limit(1) to the edit/delete event lookups
and CR-02 added .innerJoin(calendars).limit(1) to the freshest-etag re-read.
The existing test doubles terminated the mock chain at .where(), so the new
chain calls hit undefined methods → handlers caught the throw and returned 503
(events.test.ts) and the worker skipped the PUT (outboxWorker.test.ts).
Extend the mocks to match the corrected production chains. Behaviour-preserving:
mockWhereCalEvents stays the awaited terminal so etag override assertions still drive.
8 failing tests now green; full suite: api 103, pwa 141.
phase.add appended the entry after the 999.x backlog (the decimal backlog
convention confused the auto-placement). Moved it into Phase Details before
## Progress, added it to the top checklist + Progress table + execution order,
gave it goal/success-criteria/candidate-scope (promotes backlog 999.2/3/6/7/8/9),
renamed the dir to 06-ux-polish, and bumped STATE to 6 phases (50%).
Weekly recurring create writes valid RRULE and recurred in Fastmail; repeat-bound
+ per-occurrence-duration UX gaps backlogged (999.7/999.8). Deleting the recurring
series cleared master + all occurrences in one delete. Remaining deferred by
design: B5 (Android), C (SSE smoke — Phase 4 entry gate).
Captured from Gate 2 live testing 2026-06-07:
- 999.6 all-day events need distinct visual treatment
- 999.7 event form: auto-advance end when start moves; + latent all-day EDIT
off-by-one (edit grows the event by a day — write/display convert inclusive
<-> exclusive but the edit form does not)
- 999.8 recurrence bound (repeat-until/count) so a recurring event isn't one
giant multi-month event; verify daily-vs-weekly selection
- 999.9 edit a recurring series (whole-series edit; per-occurrence already v1.x)
All-day: a single-day all-day event displayed across two days. iCal all-day
DTEND is EXCLUSIVE (1-day event = DTSTART:24/DTEND:25) and the server occurrence
carries that exclusive end, but Schedule-X treats all-day end as INCLUSIVE.
hydrateEvents now subtracts one day (clamped to >= start) so a 1-day event shows
on one day and an N-day event spans N days. Write path was already correct
(verified against stored VEVENTs). +regression test.
Color: a member's coral (#E8734A) was mistaken for the shared-family rose
(#F25C7A). Reorder COLOR_PALETTE so warm near-rose hues (amber, coral) are
assigned LAST; early members get cool, clearly-distinct colors (blue/green/teal).
Gate 2 A3 fail: a second member (amelia) got the same color as the first (luc),
both #E8734A. Color was assigned by COUNT(*) % palette; a deleted spike user
shifted the count so two live members landed on the same slot. Replace with
'first palette color not already in use by another user' (fall back to count
round-robin only once the palette is exhausted) — guarantees distinct, stable
colors for up to palette length members. +1 regression test (deletion frees a
slot → next member fills it, no collision).
The calendar flashed whenever the event popup/form closed or a post-write events
refetch landed. Root cause: CalendarContent was a function component DEFINED
INSIDE CalendarShell's render and used as <CalendarContent />. A nested component
has a new identity every render, so React unmounted+remounted its whole subtree
— including <ScheduleXCalendar> — on ANY CalendarShell re-render. The earlier
Bug B work only minimized re-renders (Zustand selectors) to dodge this; the
resync-before-done fix made the post-write ['events'] refetch deliver changed
data again, so the remount/flash returned.
Fix: render the content as a plain JSX element value (const calendarContent)
referenced at both layout sites instead of a nested component type. Element
values reconcile in place across re-renders — no remount, no flash.
Captured from Gate 2 live testing: when the OIDC session expires mid-use, the
app hangs the action and shows a generic 'couldn't load events' instead of
recognizing the signed-out state and redirecting to /api/login. Re-auth is
currently only wired to the initial /api/me failure (one-shot).