style(13-03): apply Prettier formatting across repo

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.
This commit is contained in:
Lucas Berger
2026-06-11 20:35:18 -04:00
parent 4bc0445173
commit 982438dc10
398 changed files with 19050 additions and 16382 deletions
+57 -57
View File
@@ -30,11 +30,11 @@ Decimal phases appear between their surrounding integers in numeric order.
**Requirements**: AUTH-01, AUTH-02, AUTH-03, CAL-01, CAL-08
**Success Criteria** (what must be TRUE):
1. Both members can reach the app URL, authenticate through Authelia OIDC, and land on the app home page without entering any Fastmail credentials
2. Sessions persist across browser restarts — neither member is asked to log in again on the next visit
3. Each member is assigned a stable, distinct display color that does not change between sessions
4. The broker successfully fetches and caches at least one event from the shared Fastmail calendar via CalDAV PROPFIND/REPORT
5. The personal-calendar ACL spike produces a documented go/no-go decision: either the broker token sees the wife's personal calendar after Fastmail share+accept, or the fallback strategy (shared-family-only or per-member app password) is chosen and recorded
1. Both members can reach the app URL, authenticate through Authelia OIDC, and land on the app home page without entering any Fastmail credentials
2. Sessions persist across browser restarts — neither member is asked to log in again on the next visit
3. Each member is assigned a stable, distinct display color that does not change between sessions
4. The broker successfully fetches and caches at least one event from the shared Fastmail calendar via CalDAV PROPFIND/REPORT
5. The personal-calendar ACL spike produces a documented go/no-go decision: either the broker token sees the wife's personal calendar after Fastmail share+accept, or the fallback strategy (shared-family-only or per-member app password) is chosen and recorded
**Verification status (D-14, 2026-06-04):** Code + **Gate 1** complete. Gate 1 = stack up (`/health` live), CAL-01 proven live (503 real events cached via REPORT), CAL-08 = **GO** (per-member app-password model, see `CAL-08-DECISION.md`). **Gate 2 deferred** — criteria 1/2/3 (live Authelia OIDC login over Pangolin, session persistence, distinct colors in a real browser) and the SSE-over-Pangolin smoke test require the operator's Authelia + Pangolin/Newt infra; tracked in `01-HUMAN-UAT.md` and `docs/deployment.md`. The live AUTH smoke (incl. iOS) is folded into **Phase 3**; the SSE smoke is a hard gate before **Phase 4**. Phases 23 develop behind a documented dev-auth bypass.
@@ -54,30 +54,30 @@ Plans:
**Requirements**: CAL-02, CAL-03, CAL-07
**Success Criteria** (what must be TRUE):
1. Opening the app shows a color-coded calendar where each member's events appear in their assigned color, with shared events distinguishable from personal events
2. The user can switch between day, week, month, and agenda views and all events render correctly in each view
3. A recurring event (e.g., weekly meeting) displays all its occurrences correctly in the current view window, including correct behavior across DST boundaries
4. All-day events (birthdays, holidays) appear as full-day banners on the correct date with no timezone shift
1. Opening the app shows a color-coded calendar where each member's events appear in their assigned color, with shared events distinguishable from personal events
2. The user can switch between day, week, month, and agenda views and all events render correctly in each view
3. A recurring event (e.g., weekly meeting) displays all its occurrences correctly in the current view window, including correct behavior across DST boundaries
4. All-day events (birthdays, holidays) appear as full-day banners on the correct date with no timezone shift
**Plans**: 5 plansPlans:
**Wave 1**
- [x] 02-01-PLAN.md — Foundation: schema (hasRrule + calendars.isShared, pushed) + dev-auth bypass + PWA vitest/jsdom harness + ICS fixtures + RED test stubs
**Wave 2** *(blocked on Wave 1 completion)*
**Wave 2** _(blocked on Wave 1 completion)_
- [x] 02-02-PLAN.md — Backend slice: expandOccurrences() (VTIMEZONE/DST + all-day + EXDATE) + windowed/joined/zod-validated /api/events + shared-calendar checkpoint (CAL-02/CAL-07)
- [x] 02-03-PLAN.md — Frontend foundation: CSS token layer + colorUtils + calendarConfig (firstDayOfWeek 0→7) + hydrateEvents (Temporal/PlainDate guard) + Zustand store + windowed fetchEvents
**Wave 3** *(blocked on Wave 2 completion)*
**Wave 3** _(blocked on Wave 2 completion)_
- [x] 02-04-PLAN.md — Vertical slice: CalendarShell mounts Schedule-X, renders real windowed Fastmail events color-coded across all four views (CAL-02/CAL-03)
**Wave 4** *(blocked on Wave 3 completion)*
**Wave 4** _(blocked on Wave 3 completion)_
- [x] 02-05-PLAN.md — UX completion: read-only EventDetailPopover + ColorLegend + nav/toolbar + skeleton/empty/error states + human visual verification
**Gap-closure waves** *(from 03-REVIEW.md — write path was broken end-to-end; Gate 2 / 03-08 is blocked on these)*
**Gap-closure waves** _(from 03-REVIEW.md — write path was broken end-to-end; Gate 2 / 03-08 is blocked on these)_
- [x] 03-09-PLAN.md — Route layer: align zod schema to client title/start/end contract (CR-01) + real OIDC iss/sub→users.id resolution on all 5 handlers (CR-06) [wave 1]
- [x] 03-12-PLAN.md — PWA EventForm: edit-mode population + recurrence preselect (WR-03), zone-consistent dates (WR-05), real focus trap (WR-07); PWA-01/02 install assets verified [wave 1]
@@ -94,12 +94,12 @@ Plans:
**Requirements**: CAL-04, CAL-05, CAL-06, CAL-07, PWA-01, PWA-02
**Success Criteria** (what must be TRUE):
1. A member can create a timed or all-day event (including recurring events) in the app and see it appear in the native Fastmail app within the next sync cycle
2. A member can edit an existing event's title, time, or description and the change persists correctly in Fastmail
3. A member can delete an event and it disappears from all views on the next sync
4. On Android, the app shows a browser install prompt and installs to the home screen; on iOS, the app shows a guided "Add to Home Screen" walkthrough with annotated screenshots that a non-technical user can follow independently
5. The installed PWA opens full-screen without browser chrome on both iOS and Android
6. **(Carried from Phase 1 Gate 2, D-14)** Live Authelia OIDC login works over the public Pangolin URL — including the **iOS standalone-PWA** flow: the wife can install to Home Screen and complete login without the redirect breaking out of standalone mode; sessions persist (AUTH-01/02) and members get distinct stable colors (AUTH-03). Verify per `docs/deployment.md` Gate 2 checklist; this is the first real external deploy (local Newt test rig is sufficient — Unraid prod is optional until go-live).
1. A member can create a timed or all-day event (including recurring events) in the app and see it appear in the native Fastmail app within the next sync cycle
2. A member can edit an existing event's title, time, or description and the change persists correctly in Fastmail
3. A member can delete an event and it disappears from all views on the next sync
4. On Android, the app shows a browser install prompt and installs to the home screen; on iOS, the app shows a guided "Add to Home Screen" walkthrough with annotated screenshots that a non-technical user can follow independently
5. The installed PWA opens full-screen without browser chrome on both iOS and Android
6. **(Carried from Phase 1 Gate 2, D-14)** Live Authelia OIDC login works over the public Pangolin URL — including the **iOS standalone-PWA** flow: the wife can install to Home Screen and complete login without the redirect breaking out of standalone mode; sessions persist (AUTH-01/02) and members get distinct stable colors (AUTH-03). Verify per `docs/deployment.md` Gate 2 checklist; this is the first real external deploy (local Newt test rig is sufficient — Unraid prod is optional until go-live).
**Plans**: 12 plans (8 original + 4 gap-closure from 03-REVIEW.md)
Plans:
@@ -107,22 +107,22 @@ Plans:
- [x] 03-01-PLAN.md — Foundation: calendarOutbox table + calendarEvents.objectUrl (pushed), vite-plugin-pwa install + legitimacy gate, sync.ts objectUrl, full Wave 0 RED test scaffold
**Wave 2** *(blocked on Wave 1)*
**Wave 2** _(blocked on Wave 1)_
- [x] 03-02-PLAN.md — TDD: VEVENT builder (vevent.ts, D-13 DATE/DATETIME + RRULE) + tsdav write wrappers (write.ts, D-12 broker boundary)
- [x] 03-03-PLAN.md — Write API: POST/PATCH/DELETE events + GET sync-status, enqueue-only, D-03 ownership, D-04 edit-as-move pair (CAL-04/05/06/07)
**Wave 3** *(blocked on Wave 2)*
**Wave 3** _(blocked on Wave 2)_
- [x] 03-04-PLAN.md — TDD: outbox worker state machine (D-05/06/07/08 retry/backoff/dead/conflict, edit-as-move ordering) + index.ts wiring
- [x] 03-05-PLAN.md — Frontend create/edit slice: write client calls + Zustand keys + EventForm (D-01/02/11) + New Event FAB
- [x] 03-07-PLAN.md — PWA install: VitePWA manifest + auth-safe SW denylist + icons + InstallPrompt (iOS walkthrough + Android prompt) (PWA-01/02)
**Wave 4** *(blocked on Wave 3)*
**Wave 4** _(blocked on Wave 3)_
- [x] 03-06-PLAN.md — Delete + sync feedback: popover Edit/Delete footer + DeleteConfirmationDialog + SyncStateToast polling (D-06/08/09) (CAL-05/06)
**Wave 5** *(blocked on Wave 4)*
**Wave 5** _(blocked on Wave 4)_
- [x] 03-08-PLAN.md — Gate 2 live verification: real Authelia OIDC over Pangolin + iOS standalone login + end-to-end Fastmail write round-trips (success criterion 6, D-14/D-15)
@@ -137,9 +137,9 @@ Plans:
**Requirements**: LIST-01, LIST-02, LIST-03, LIST-04
**Success Criteria** (what must be TRUE):
1. Either member can create a named list (e.g., "Groceries") and delete a list they no longer need
2. Either member can add items to a list, check items off, reorder them by drag-and-drop, and delete individual items
3. When one member adds or checks off an item, the other member sees the change appear in the list within a few seconds without refreshing — even if they reconnect after a brief network gap
1. Either member can create a named list (e.g., "Groceries") and delete a list they no longer need
2. Either member can add items to a list, check items off, reorder them by drag-and-drop, and delete individual items
3. When one member adds or checks off an item, the other member sees the change appear in the list within a few seconds without refreshing — even if they reconnect after a brief network gap
**Entry gate status (2026-06-08):** CLEARED — SSE-over-Pangolin smoke test PASSED (35 heartbeats over ~6 min, buffering off, no cut). Live sync may be built directly on SSE; polling fallback (D-12) retained as belt-and-suspenders.
@@ -149,24 +149,24 @@ Plans:
- [x] 04-01-PLAN.md — Foundation + app shell: deps install (+ legitimacy gate), list tables generate+migrate [BLOCKING], API test harness + Wave-0 RED stubs, react-router + BottomTabBar + empty ListsIndex (D-13/D-16/D-17/D-18)
**Wave 2** *(blocked on Wave 1)*
**Wave 2** _(blocked on Wave 1)_
- [x] 04-02-PLAN.md — TDD: scoped in-memory fan-out (listEmitter) + getAccessibleListIds access scope — the load-bearing D-04 no-leak primitive (LIST-04)
- [x] 04-03-PLAN.md — List CRUD slice: POST/GET/PATCH/DELETE /api/lists with scoped access + auto-share-on-create + ListsIndex/ListCard/CreateListSheet/ListDeleteDialog (LIST-01, D-01/D-02/D-06)
**Wave 3** *(blocked on Wave 2)*
**Wave 3** _(blocked on Wave 2)_
- [x] 04-04-PLAN.md — Item CRUD + checked-sink slice: item endpoints + fractional rank + per-field LWW PATCH + ListDetail/ItemRow/AddItemInput + optimistic UI (LIST-02, D-05/D-07/D-08/D-09)
**Wave 4** *(blocked on Wave 3)*
**Wave 4** _(blocked on Wave 3)_
- [x] 04-05-PLAN.md — Reorder slice: dnd-kit sortable + generateKeyBetween rank + one-row position PATCH + animate-on-remote (LIST-03, D-13/D-14/D-15)
**Wave 5** *(blocked on Waves 2 + 4)*
**Wave 5** _(blocked on Waves 2 + 4)_
- [x] 04-06-PLAN.md — Live-sync slice: scoped /api/sse/lists + fan-out triggers + useListSSE bounded-backoff hook + LiveSyncIndicator + polling fallback (LIST-04, D-04/D-10/D-11/D-12)
**Wave 6** *(gap closure — blocked on Waves 2 + 4)*
**Wave 6** _(gap closure — blocked on Waves 2 + 4)_
- [x] 04-07-PLAN.md — Gap closure: migrate list_items.rank to COLLATE utf8mb4_bin (LIST-03 drag-to-top) + owner-only guard on PATCH isShared (T-04-08/T-04-05) — two TDD features (LIST-03)
@@ -180,10 +180,10 @@ Plans:
**Requirements**: NOTIF-01, NOTIF-02, NOTIF-03
**Success Criteria** (what must be TRUE):
1. A member receives a push notification on their phone approximately 15 minutes before a calendar event starts — delivered to the installed PWA, including on iOS
2. When the other member adds or changes a calendar event, the first member receives a push notification with the event title and action described in the payload
3. When the other member modifies a shared list (adds, checks off, or deletes an item), the first member receives a push notification identifying the list and the change
4. After an extended period of app inactivity, push notifications are still delivered (subscription health-check prevents silent revocation on iOS)
1. A member receives a push notification on their phone approximately 15 minutes before a calendar event starts — delivered to the installed PWA, including on iOS
2. When the other member adds or changes a calendar event, the first member receives a push notification with the event title and action described in the payload
3. When the other member modifies a shared list (adds, checks off, or deletes an item), the first member receives a push notification identifying the list and the change
4. After an extended period of app inactivity, push notifications are still delivered (subscription health-check prevents silent revocation on iOS)
**Plans**: 8 plans (6 waves)
Plans:
@@ -191,25 +191,25 @@ Plans:
- [x] 05-01-PLAN.md — Foundation: install web-push + workbox deps (legitimacy gate), generate VAPID keypair, push_subscriptions table + calendar_events.title generate+migrate [BLOCKING], Wave-0 RED scaffolds (D-11/D-12)
**Wave 2** *(blocked on Wave 1)*
**Wave 2** _(blocked on Wave 1)_
- [x] 05-02-PLAN.md — TDD: pushDispatcher (VAPID send + dual-format payload + 410/404 prune) (D-11)
- [x] 05-03-PLAN.md — TDD: pushCoalescer (per-list/actor debounce, generic copy, self-suppress) (D-01/D-02/D-03)
**Wave 3** *(blocked on Wave 2)*
**Wave 3** _(blocked on Wave 2)_
- [x] 05-04-PLAN.md — Subscribe slice (end-to-end): push subscription API + setVapidDetails, generateSW→injectManifest SW migration (push/notificationclick/denylist), usePushSubscription + PushPermissionPrompt (D-08/D-11/D-14)
**Wave 4** *(blocked on Wave 3)*
**Wave 4** _(blocked on Wave 3)_
- [x] 05-05-PLAN.md — NOTIF-02 list-change slice: listChangeDispatcher + hook coalescer into mutations, reorder-silent (D-01/D-02/D-03)
- [x] 05-06-PLAN.md — TDD: NOTIF-01 reminderScheduler — shared-timed 15-min scan (query-enforced D-05), all-day excl, dedup, empty-set safe (D-05/D-06/D-07)
**Wave 5** *(blocked on Wave 4)*
**Wave 5** _(blocked on Wave 4)_
- [x] 05-07-PLAN.md — TDD: NOTIF-03 eventChangeDispatcher + syncCalendar diff/title/onChanges hook (poller + outbox), meaningful-only, actor-suppressed (D-02/D-03/D-04/D-13)
**Wave 6** *(blocked on Wave 3)*
**Wave 6** _(blocked on Wave 3)_
- [x] 05-08-PLAN.md — Settings + reliability: master toggle (D-09) + silent re-subscribe (D-10) + PermissionDeniedBanner + avatar→Settings sheet
@@ -223,17 +223,17 @@ Plans:
**Requirements**: none (all v1 REQ-IDs complete in Phases 15; this is a polish phase tracked against backlog items 999.2/3/6/7/8/9 and locked decisions D-01..D-13)
**Success Criteria** (what must be TRUE):
1. All-day events are visually distinct from timed events at a glance
2. The event form keeps a sane duration when the start moves, all-day edits don't grow the event, and a recurrence can be bounded (repeat-until / count)
3. A recurring series can be edited as a whole
4. A session that expires mid-use redirects cleanly to sign-in instead of hanging on a generic error
5. Unauthenticated cold load shows a neutral "signing you in…" splash — no calendar/"sign-in required" flash before Authelia
1. All-day events are visually distinct from timed events at a glance
2. The event form keeps a sane duration when the start moves, all-day edits don't grow the event, and a recurrence can be bounded (repeat-until / count)
3. A recurring series can be edited as a whole
4. A session that expires mid-use redirects cleanly to sign-in instead of hanging on a generic error
5. Unauthenticated cold load shows a neutral "signing you in…" splash — no calendar/"sign-in required" flash before Authelia
**Scope** (promoted from backlog, locked at planning): 999.2 (login flash), 999.3 (session-timeout redirect), 999.6 (all-day visual), 999.7 (form end-tracking + all-day-edit off-by-one), 999.8 (recurrence bound), 999.9 (recurring-series edit). 999.4 (reminders) and 999.5 (provider setup) deferred to milestone 1.1 (D-01/D-02).
**Plans**: 6 plans (2 waves)
Plans:
**Wave 1** *(parallel — exclusive file ownership)*
**Wave 1** _(parallel — exclusive file ownership)_
- [x] 06-01-PLAN.md — TDD: duration-preserving end-tracking math (computeNewTimedEnd/computeNewAllDayEnd) in eventDateTime.ts (D-04)
- [x] 06-02-PLAN.md — TDD: RRULE UNTIL/COUNT serialization + Zod acceptance + FREQ-persistence regression (vevent/outboxWorker/events route) (D-06/D-07)
@@ -241,7 +241,7 @@ Plans:
- [x] 06-04-PLAN.md — Spinner/pulse: global @keyframes pulse + remove redundant spin redefinition (D-13)
- [x] 06-05-PLAN.md — Auth gating slice: SessionExpiredError + AuthSplash + global QueryCache/MutationCache error handler; client.ts type mirrors (D-10/D-11, + D-06/D-08 type carriers)
**Wave 2** *(blocked on 06-01/02/03/05)*
**Wave 2** _(blocked on 06-01/02/03/05)_
- [x] 06-06-PLAN.md — EventForm integration slice: end-tracking wiring + recurrence-bound control + series-edit prompt + all-day pill (D-03/D-04/D-05/D-06/D-07/D-08/D-09/D-12)
@@ -253,14 +253,14 @@ Plans:
Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6
Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is complete. It is serialized here to reduce work-in-progress.
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Foundation + Broker Spike | 4/4 | Complete | 2026-06-04 |
| 2. Calendar Display | 5/5 | Complete | 2026-06-05 |
| 3. Event Write-Back + PWA Install | 12/12 | Complete | 2026-06-07 |
| 4. Shared Lists + Live Sync | 6/6 | Complete | 2026-06-09 |
| 5. Web Push Notifications | 8/8 | Complete | 2026-06-10 |
| 6. UX Polish | 6/6 | Complete | 2026-06-10 |
| Phase | Plans Complete | Status | Completed |
| --------------------------------- | -------------- | -------- | ---------- |
| 1. Foundation + Broker Spike | 4/4 | Complete | 2026-06-04 |
| 2. Calendar Display | 5/5 | Complete | 2026-06-05 |
| 3. Event Write-Back + PWA Install | 12/12 | Complete | 2026-06-07 |
| 4. Shared Lists + Live Sync | 6/6 | Complete | 2026-06-09 |
| 5. Web Push Notifications | 8/8 | Complete | 2026-06-10 |
| 6. UX Polish | 6/6 | Complete | 2026-06-10 |
## Backlog
@@ -276,7 +276,7 @@ Plans:
### Phase 999.4: Per-event reminder configuration (VALARM authoring + scheduler honors it) (BACKLOG)
**Goal:** [Captured for future planning] End-to-end per-event reminders — let the user choose *when* (or whether) to be reminded per event, and make the push scheduler honor that choice instead of a hardcoded lead.
**Goal:** [Captured for future planning] End-to-end per-event reminders — let the user choose _when_ (or whether) to be reminded per event, and make the push scheduler honor that choice instead of a hardcoded lead.
**Half A — author the VALARM (event form):** The event create/edit form has no UI to set a reminder ("remind me 10 min / 1 hour / 1 day before", or **no reminder**), so the written `.ics` carries no `VALARM` and no reminder can fire — in native clients or via web push. Add a reminder selector (including an explicit "none"), serialize chosen offsets as `VALARM` (TRIGGER) on write-back, and parse existing `VALARM`s on read so edits preserve them. Feeds the Phase 5 web-push requirement (push needs reminder data to notify about).
@@ -357,7 +357,7 @@ Plans:
- **An authenticated entry path for automated runs** so the assistant can reach the real PWA past Authelia — e.g. a reusable saved storage-state/cookie, a test-only bypass on a non-prod host, or driving the Authelia login once and reusing the session. (Note: this overlaps the existing `DEV_AUTH_BYPASS`, but that only works on the host-side dev stack, not the prod-mode PWA that has the real service worker. A mobile, authed, SW-enabled target is the gap.)
- Optionally: a documented way to point the harness at the Pangolin HTTPS URL with a persisted session, and/or remote-debug a real device.
**Boundary:** genuinely device-only behaviour (iOS-Safari standalone push, real APNs/FCM delivery, OS notification-channel importance) still needs a human — this item is about everything SHORT of that (responsive layout, tap flows, in-page notification UI states, auth redirects) which a mobile-emulated authed browser *could* cover but currently can't.
**Boundary:** genuinely device-only behaviour (iOS-Safari standalone push, real APNs/FCM delivery, OS notification-channel importance) still needs a human — this item is about everything SHORT of that (responsive layout, tap flows, in-page notification UI states, auth redirects) which a mobile-emulated authed browser _could_ cover but currently can't.
**Context:** Surfaced 2026-06-10 during Phase 5 UAT — repeated mobile-only bugs were caught only by the operator because the assistant had no mobile, authenticated browser to test in. **Related:** [[feedback-playwright-verify]] (use playwright-cli over manual verification — this extends it to mobile/authed). Tags: testing, playwright, mobile, pwa, oidc, dx.
**Requirements:** TBD