Commit Graph
23 Commits
Author SHA1 Message Date
Lucas Berger f6b47ebf1e fix(11-05): CR-01 surface reminderIsCustom to preserve custom VALARMs on edit
- expand.ts: add reminderIsCustom:boolean to CalendarOccurrence interface;
  derived from classifyValarms kind==='custom'; propagated to both
  non-recurring and recurring occurrence branches
- client.ts: mirror reminderIsCustom on CalendarOccurrence (atomic mirror)
- EventForm.tsx: extend deriveReminderValue to accept isCustom flag;
  returns '__custom__' when true, making the existing D-08 preserve branch
  live — editing a custom-alarm event now omits reminderLeadMinutes from
  the payload so outboxWorker extractValarms keeps the original VALARM
- Fix existing test fixtures (EventForm.test.tsx, EventDetailPopover.test.tsx)
  to include reminderIsCustom:false on all CalendarOccurrence literals

Fixes CAL-14 Pitfall 1: Apple Calendar absolute DATE-TIME / multi-VALARM
alarms no longer silently stripped on any edit round-trip from the PWA.
2026-06-14 08:11:46 -04:00
Lucas Berger 2c30afe8ff feat(11-04): add reminderLeadMinutes to CreateEventPayload + CalendarOccurrence
- CalendarOccurrence: required reminderLeadMinutes: number | null (atomic mirror of expand.ts, Plan 11-03)
- CreateEventPayload: optional reminderLeadMinutes?: number | null with absent/null/0/positive contract (D-08)
- Update CalendarOccurrence fixtures in EventForm.test.tsx + EventDetailPopover.test.tsx to include the new required field (reminderLeadMinutes: null)
- pwa tsc --noEmit exits 0
2026-06-13 22:33:06 -04:00
Lucas Berger bfe1eff5a3 feat(10-04): extend client.ts — MeUser.isAdmin+needsProviderSetup + admin/self-service fetchers
- Add isAdmin and needsProviderSetup to MeUser interface (D-03 UX gating)
- Add AdminMember, SaveCredentialPayload, AdminCalendar, SaveMyCredentialPayload types
- Add fetchAdminMembers, saveCredential, fetchAdminCalendars, setSharedCalendar fetchers
- Add saveMyCredential (self-service, no userId field — T-10-12/Pitfall 6)
- All fetchers use credentials:'include', redirect:'manual', handleAuthResponse
- Password never logged or stored beyond in-flight request body (T-10-15)
2026-06-13 15:02:06 -04:00
Lucas Berger 982438dc10 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.
2026-06-11 20:35:18 -04:00
Lucas Berger 03e953158a fix(13-02): eliminate all ESLint violations — pnpm lint exits 0
- eslint.config.js: disable React Compiler rules (v7 flat.recommended enables
  them; codebase does not use the Compiler); add e2e/ to disableTypeChecked
  block; promote exhaustive-deps to error
- API broker: remove redundant as-casts (outboxWorker, poller, reminderScheduler,
  expand, sync, vevent, spike); add targeted ical.js no-unsafe-assignment/argument
  disables with justifying comments inside try blocks
- API routes/sse.ts: fix no-misused-promises on async writeSSE callback with
  void+IIFE+catch pattern
- API routes/lists.ts: let → const for updateValues
- API tests: remove unused imports (beforeEach, eq, vi); rename unused vars
  with _ prefix; remove unused lastActiveId assignment
- PWA components: void navigate() and void queryClient.invalidateQueries() on
  all fire-and-forget call sites; fix CalendarShell explicit-type-casts;
  Couldn't → HTML entity
- PWA test files: as unknown as Response for partial mock objects; string | null
  type annotation on mockLastSyncedUid; remove async from test callbacks without
  await; act(() => {}) not await act(async () => {}) for sync ops
- sw.ts: restructure Notification.data?.url access as let+if so disable
  comments land on the exact violation lines; void self.skipWaiting()
2026-06-11 20:23:38 -04:00
Lucas Berger 8b79d499f6 fix(06): IN-01 correct stale CalendarOccurrence.id comment to ev-<uid>-<epochMs> form 2026-06-10 16:53:52 -04:00
Lucas Berger d7d4023cf9 feat(06-05): centralize session-expiry detection and extend client types
- Add SessionExpiredError class with Object.setPrototypeOf for correct instanceof
- Add handleAuthResponse helper: throws SessionExpiredError on 401/opaqueredirect, generic Error on other non-ok
- Add redirect:'manual' + handleAuthResponse to all six fetch wrappers (fetchEvents, createEvent, updateEvent, deleteEvent, fetchSyncStatus, fetchWritableCalendars)
- Unify fetchMe: now throws SessionExpiredError instead of generic Error
- Add recurrenceUntil? and recurrenceCount? to CreateEventPayload (D-06)
- Add hasRrule: boolean to CalendarOccurrence client mirror (D-08, Pitfall 4)
2026-06-10 11:29:37 -04:00
Lucas Berger e5072ff663 test(06-05): add failing SessionExpiredError detection tests 2026-06-10 11:26:59 -04:00
Lucas Berger 6da9c2ae7b feat(04-04): add ListDetail with active/completed split + ItemRow + AddItemInput (LIST-02)
- listsClient.ts: add fetchListItems, addItem, patchListItem, deleteItem + ListItemsResponse type
- ListDetail.tsx: replace placeholder with real implementation — useQuery(['list', listId])
  with 30s polling fallback (D-12); active/completed split (D-05); optimistic mutations (D-07);
  delete-wins no-rollback (D-09); per-field check PATCH (D-08)
- ItemRow.tsx: 44px touch target, checkbox (20px visual/44px touch, accent fill when checked),
  plain-text item text (T-04-06 XSS guard), GripVertical handle slot for Plan 05,
  hover Trash2 delete + swipe-left zone, transform 150ms ease-out animation slot (D-14)
- AddItemInput.tsx: sticky bottom input + Add button, disabled when empty, Enter key support
- ListDetail.test.tsx: 7 real tests replacing todo stubs — optimistic add/check/uncheck/delete,
  rollback on error, D-05 completed-sink split, D-09 delete-wins no-rollback
- Playwright browser check: add milk → sinks to Completed on check → vanishes on delete PASS
2026-06-09 13:01:49 -04:00
Lucas Berger 95dbc663c1 feat(04-03): wire ListsIndex + ListCard + CreateListSheet + ListDeleteDialog (LIST-01)
- listsClient.ts: add createList/patchList/deleteList + List/ListItem types with activeCount/doneCount
- ListsEmptyState.tsx: extracted standalone component (ClipboardList icon, UI-SPEC copy)
- ListCard.tsx: name/count badge/Shared pill/ChevronRight; hover-reveal delete button; navigates /lists/:id
- CreateListSheet.tsx: bottom-sheet/modal; Shared default (D-01); optimistic useMutation; auto-focus; Escape to close
- ListDeleteDialog.tsx: mirrors DeleteConfirmationDialog pattern; props-driven (no calendarStore); XSS guard on name
- ListsIndex.tsx: replaced placeholder with real data via useQuery+useMutation; mounts CreateListSheet+ListDeleteDialog
- DeleteConfirmationDialog.tsx NOT modified (stable, D-06 pattern preserved)
- PWA typecheck passes; DeleteConfirmationDialog.test.tsx 10 passed
- Playwright E2E: create Groceries+Gift Ideas (Shared pills); delete dialog → confirm → card disappears
2026-06-09 12:45:19 -04:00
Lucas Berger c0088edf44 feat(04-01): wire BrowserRouter + BottomTabBar + empty Lists surface
- App.tsx: BrowserRouter with /calendar, /lists, /lists/:listId routes; / redirects to /calendar
- BottomTabBar.tsx: fixed-bottom 56px tab bar with Calendar + Lists NavLinks, active accent
- AppNav.tsx: add Calendar/Lists NavLinks to desktop sidebar (≥768px)
- ListsIndex.tsx: full-height surface with isLoading/isError/empty state branches; "+ New List" FAB placeholder
- ListDetail.tsx: placeholder stub for /lists/:listId (Plan 04-04 fills in)
- listsStore.ts: Zustand UI-only store (activeTab, createListSheetOpen)
- listsClient.ts: fetchLists + List/ListItem types (initial; Plans 04-02/03 expand)
- Fix Wave-0 RED stubs: add vitest imports so stubs execute (todo) not error on import
- Fix CalendarShell.test.tsx: wrap renderWithClient in MemoryRouter (AppNav uses NavLink)
2026-06-09 12:03:52 -04:00
Lucas Berger 02aa407764 fix(03): WR-01 preserve existing RRULE on edit instead of resetting to none 2026-06-09 10:39:06 -04:00
Lucas Berger 1adb460412 fix(pwa): fetchMe uses redirect:manual so unauthenticated /api/me can't hang
With the default redirect:follow, the browser follows the OIDC guard's 302 to
Authelia (cross-origin, credentialed) and the fetch HANGS — meQuery stays
'loading' so the SPA spins forever and the isError-driven login redirect never
fires. redirect:manual surfaces the 302 as an opaqueredirect (status 0) that we
detect as auth-required and throw, letting CalendarShell navigate to /api/login.
+4 fetchMe tests.
2026-06-06 21:57:42 -04:00
Lucas Berger 6dc9ccd2e9 feat(260606-tv8-01): add one-shot login-redirect helper + tests; fix client.ts comment
- Add loginRedirect.ts: maybeRedirectToLogin (sessionStorage one-shot guard) and
  clearLoginRedirect; guards window/sessionStorage for SSR/test safety
- Add loginRedirect.test.ts: covers first-call redirect, one-shot no-op, clear+retry
- Update client.ts: remove false claim that fetch follows Authelia 302 automatically;
  note that XHR/fetch CORS-blocks cross-origin redirects, top-level nav required
2026-06-06 21:37:14 -04:00
Lucas Berger 8aeacc8607 feat(03-06): add deleteEvent, fetchSyncStatus client calls and delete/sync Zustand keys
- deleteEvent(uid): DELETE /api/events/:uid with credentials:include, throws on !ok
- fetchSyncStatus(uid): GET /api/events/sync-status?uid= returning SyncStatus
- Export SyncStatus and SyncStatusValue types
- Zustand: deleteDialogOpen/deleteDialogUid/lastSyncedUid keys + setDeleteDialog/setLastSyncedUid setters
2026-06-05 18:35:51 -04:00
Lucas Berger 8357cf998e test(03-06): add failing tests for deleteEvent, fetchSyncStatus, and delete/sync Zustand keys 2026-06-05 18:34:55 -04:00
Lucas Berger 6ffcdcbd6b feat(03-05): add write client calls and eventForm Zustand keys
- createEvent(payload): POST /api/events/create, credentials:include, returns {uid}
- updateEvent(uid, payload): PATCH /api/events/:uid/edit
- fetchWritableCalendars(): GET /api/events/writable-calendars, returns calendars array (D-03 server-authoritative)
- Exported interfaces: CreateEventPayload, CreateEventResponse, WritableCalendar, RecurrencePreset
- calendarStore: eventFormOpen (bool), eventFormMode ('create'|'edit'), eventFormUid (string|null)
- setEventForm(open, mode?, uid?) setter with correct defaults
2026-06-05 18:26:00 -04:00
Lucas Berger 6400ce693c test(03-05): add failing tests for write client calls and eventForm store keys
- createEvent: POST /api/events/create with credentials:include, returns uid
- updateEvent: PATCH /api/events/:uid/edit
- fetchWritableCalendars: GET /api/events/writable-calendars, returns calendars array
- calendarStore: eventFormOpen, eventFormMode, eventFormUid defaults and setEventForm setter
2026-06-05 18:25:00 -04:00
Lucas Berger 194f6a82a8 fix(02): show owner name / Family in event popover footer
Backend:
- expand.ts: add ownerName: string | null to CalendarOccurrence
  interface and expandOccurrences() signature; thread it onto every
  emitted occurrence.
- events.ts: SELECT users.displayName as ownerName in the join; pass
  it to expandOccurrences().

Frontend:
- client.ts: add ownerName: string | null to CalendarOccurrence.
- EventDetailPopover.tsx: render isShared ? 'Family' :
  (ownerName ?? calendarName) in the footer instead of calendarName.

Tests:
- expand.test.ts: pass ownerName to all expandOccurrences() calls;
  assert ownerName is carried onto occurrences in the DST test.
- events.test.ts: add ownerName to mock rows; assert ownerName present
  on occurrences; add ownerName assertion to timed-recurring test.
- EventDetailPopover.test.tsx: add ownerName to fixtures; split
  "calendar name in footer" into three targeted tests covering
  personal-with-owner, shared→Family, and null-owner fallback.
2026-06-05 15:14:43 -04:00
Lucas Berger 216ddcedf4 feat(02-05): ColorLegend, AppNav, ViewToolbar, SkeletonCalendar, EmptyState; retire EventProof
- ColorLegend: per-member color swatches (12px circle, label) + always-visible Family row
- AppNav: phone 48px top bar (avatar with aria-label/title) + tablet/desktop 240px sidebar with ColorLegend
- ViewToolbar: Today/prev/next + Day/Week/Month/Agenda view switcher; 44px min-height; active state uses surface tint not accent
- SkeletonCalendar: shimmer month (6x7 grid) and agenda (4 date-group blocks) variants; aria-busy=true
- EmptyState: CalendarDays icon + 'Nothing here' heading + body copy per UI-SPEC
- CalendarShell: full phone/desktop layout with AppNav + ViewToolbar + ColorLegend chrome
- CalendarShell: state branches — loading→SkeletonCalendar, empty→EmptyState, error→'Couldn't load events' + Retry button (refetchQueries)
- EventProof.tsx deleted; legacy types removed from client.ts
- CalendarShell.test.tsx: updated to waitFor ScheduleXCalendar after data loads
- All 36 tests pass, tsc clean, vite build clean (490kB)
2026-06-05 10:58:06 -04:00
Lucas Berger f377d7c3f8 feat(02-03): hydrateEvents + calendarStore + windowed fetchEvents; RED stubs green
- Create hydrateEvents.ts: Temporal.PlainDate for allDay, ZonedDateTime for timed
  calendarId routes via isShared ? 'shared' : String(ownerUserId) — NOT String(calendarId)
  _familySync carries uid/color/isShared through to popover
- Update hydrateEvents.test.ts: add temporal-polyfill/global import; all 4 RED stubs now GREEN
- Create calendarStore.ts: Zustand store with selectedView (localStorage per breakpoint group),
  selectedDate, openEventId, calendarRange; D-05 view defaults; calendarRange ± buffer for
  initial TanStack Query key without depending on onRangeUpdate firing on mount
- Update client.ts: add CalendarOccurrence/OccurrencesResponse, windowed fetchEvents(start,end)
  with credentials:include; keep legacy CalendarEvent/EventsResponse + fetchEventsLegacy
  as deprecated for EventProof.tsx (removed in Plan 05)
- Update EventProof.tsx: switch to fetchEventsLegacy to keep build clean until Plan 05
- tsc --noEmit clean; all 18 PWA tests pass
2026-06-05 09:47:07 -04:00
Lucas Berger 48f90ceca9 feat(01-04): wire broker + routes into bootstrap, add SSE endpoint, EventProof
- Mount /api/events, /api/sse in index.ts behind oidcAuthMiddleware; /callback + /health before guard
- Call startBrokerPoller() on boot (5-min ctag-poll background schedule)
- Add sseRouter with GET /heartbeat (streamSSE, 10s interval) for Pangolin SSE smoke test (D-08, T-04-01)
- Add CAL-08 spike script (broker/spike.ts): createFastmailClient → fetchCalendars → print calendar URLs
- Add fetchEvents() to pwa/api/client.ts with typed CalendarEvent/EventsResponse shapes
- Add EventProof.tsx: React Query ['events'], renders first event title+date or empty-state (CAL-01 broker proof)
- Update App.tsx to render MemberBadge + EventProof on landing page
- Add ical.js@2.2.1 to PWA dependencies for VEVENT summary parsing in EventProof
- All 24 API unit tests green; tsc --noEmit clean in both apps/api and apps/pwa
2026-06-04 11:16:10 -04:00
Lucas Berger 668ed9be0d feat(01-02): wire OIDC middleware, /api/me route, and authenticated PWA shell
- src/auth/middleware.ts: re-exports oidcAuthMiddleware, processOAuthCallback,
  getAuth from @hono/oidc-auth; documents required env vars and AUTH-02
  refresh-token rotation (no iframe, D-12)
- src/routes/me.ts: GET / calls getAuth → upsertUser(iss, sub, email) →
  returns { user: { id, displayName, color } }; identity keyed on iss+sub
- src/index.ts: /callback registered before oidcAuthMiddleware; /api/*
  guarded; /health remains unauthenticated; /api/me mounted
- apps/pwa/src/api/client.ts: typed fetchMe() with credentials: 'include'
- apps/pwa/src/App.tsx: useQuery(['me'], fetchMe); renders member name and
  color swatch; retains /health stack indicator from Plan 01
- tsc --noEmit: clean; all tests pass
2026-06-04 10:22:46 -04:00