diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 7964dcf..3f5d64c 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -199,3 +199,31 @@ Plans: Plans: - [ ] TBD (promote with /gsd-review-backlog when ready) + +### Phase 999.3: Redirect to sign-in on session timeout instead of hanging (BACKLOG) + +**Goal:** [Captured for future planning] When the OIDC session expires mid-use, the app hangs the action (no toast/progress) and then shows a generic "couldn't load events" — fooling the user into thinking it's broken rather than signed out. Detect session expiry (401 / opaqueredirect) from ANY query or mutation and drive a clear re-auth: top-level navigation to `/api/login`, ideally behind a brief "Your session expired — signing you back in…" interstitial, with no silent loss of an in-flight write. + +**Root cause** (diagnosed during Phase 03 Gate 2 live verification, 2026-06-07) — re-auth is only wired to the INITIAL `/api/me` failure and is one-shot (`familysync.loginRedirectAttempted` flag, cleared on a successful `/api/me`). `fetchEvents` uses `redirect:'follow'` so a timed-out session 302s cross-origin → the XHR rejects → `eventsQuery` just errors; write mutations hang/error with no re-auth. + +**Proposed fix:** Centralize expiry detection in `apps/pwa/src/api/client.ts` (typed `SessionExpiredError` on 401/opaqueredirect), apply `redirect:'manual'` consistently, and a single TanStack Query/Mutation error handler that re-arms `maybeRedirectToLogin()`. See `.planning/todos/pending/` archive for full detail. + +**Severity:** high — hits any long-lived tab (the wife leaving the PWA open). Tags: phase-03, auth, ux. +**Requirements:** TBD +**Plans:** 0 plans + +Plans: + +- [ ] TBD (promote with /gsd-review-backlog when ready) + +### Phase 999.4: Event-creation notification/reminder (VALARM) options (BACKLOG) + +**Goal:** [Captured for future planning] The event create/edit form has no UI to set a reminder ("remind me 10 min / 1 hour / 1 day before"), so the written `.ics` carries no `VALARM` and no reminder can fire — in native clients or via web push. Add a reminder selector, serialize chosen offsets as `VALARM` (TRIGGER) on write-back, parse existing `VALARM`s on read so edits preserve them. Feeds the Phase 5 web-push requirement (push needs reminder data to notify about). + +**Severity:** medium — feature gap surfaced during Phase 03 Gate 2 testing. Tags: phase-03, calendar, write-back, phase-05-dependency. +**Requirements:** TBD +**Plans:** 0 plans + +Plans: + +- [ ] TBD (promote with /gsd-review-backlog when ready) diff --git a/.planning/todos/pending/event-creation-reminder-options.md b/.planning/todos/pending/event-creation-reminder-options.md deleted file mode 100644 index 1e29cb3..0000000 --- a/.planning/todos/pending/event-creation-reminder-options.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Add notification/reminder (VALARM) options to event creation -date: 2026-06-07 -priority: medium -phase_hint: Phase 5 (push) or earlier in Phase 03 write-path polish ---- - -# Add notification/reminder options to event creation - -The event-create/edit form (`apps/pwa` EventForm → `POST /api/events`, `PATCH /:uid/edit`) -has **no UI for setting a reminder/alarm on an event**. A user creating an event cannot -choose "remind me 10 min / 1 hour / 1 day before". The written `.ics` therefore contains -no `VALARM` component, so neither Fastmail's native clients nor any downstream notification -path can fire an event reminder. - -This is distinct from (but feeds) the Active requirement -"Web Push notifications for event reminders and list changes" (Phase 5): even with web-push -infrastructure, there is nothing to notify *about* unless events carry reminder data. - -## Scope to decide when promoted - -- Add a reminder selector to EventForm (none / at time / 10m / 30m / 1h / 1d before; possibly - multiple). -- Serialize chosen offsets as `VALARM` (TRIGGER) blocks in the iCalendar payload written - back to Fastmail via the outbox. -- On read, parse existing `VALARM`s so edits preserve/show the current reminder. -- Decide division of labour vs Phase 5 web-push: VALARM gives native-client reminders - (Fastmail/Apple Calendar) for free; app-delivered web-push reminders are the separate - Phase 5 piece that would read these offsets. - -## Open question - -- iCalendar `VALARM` round-trips through `ical.js`; confirm tsdav PUT preserves it and that - Fastmail honours `DISPLAY`/`AUDIO` alarms set by a third-party CalDAV client. diff --git a/.planning/todos/pending/session-timeout-redirect-to-signin.md b/.planning/todos/pending/session-timeout-redirect-to-signin.md deleted file mode 100644 index aa008e6..0000000 --- a/.planning/todos/pending/session-timeout-redirect-to-signin.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: Redirect to sign-in on session timeout instead of hanging / "couldn't load events" -date: 2026-06-07 -priority: high -phase_hint: Phase 03 (auth-entry UX) / Gate 2 polish ---- - -# Redirect to sign-in on session timeout instead of hanging - -## Problem (observed in live Gate 2 testing) - -When the OIDC session expires **mid-use** (e.g. user leaves the tab open past -the session/refresh lifespan, then tries to delete an event), the app does not -recognize the signed-out state. The action (delete) appears to hang — no sync -toast, no progress — and shortly after the calendar shows a generic -**"couldn't load events"** error. The user is fooled into thinking the app is -working/broken rather than understanding they've been signed out and need to -re-authenticate. - -## Root cause - -Re-auth is only wired to the **initial** `/api/me` failure: -- `CalendarShell.tsx` calls `maybeRedirectToLogin()` only on `meQuery.isError`. -- `maybeRedirectToLogin()` (apps/pwa/src/lib/loginRedirect.ts) is **one-shot** - (guarded by the `familysync.loginRedirectAttempted` sessionStorage flag) and - `clearLoginRedirect()` runs on a successful `/api/me`. -- Other calls don't drive re-auth: - - `fetchEvents` uses `redirect: 'follow'`, so a timed-out session 302s to - Authelia (cross-origin) → the XHR rejects → `eventsQuery` just errors - ("couldn't load events"), no redirect. - - Write mutations (create/edit/**delete**) call the API directly; on an - expired session the request 302s/opaque-fails and the mutation hangs/errors - with no re-auth and no clear feedback. - -So a session that expires after the first successful load has no path back to -login except a manual full refresh. - -## Desired behavior - -Any API response that indicates "not authenticated anymore" (401, or an -`opaqueredirect`/`type: 'opaqueredirect'` from the OIDC guard's 302 to Authelia) -should put the app into a clear **signed-out** state and trigger a top-level -re-auth navigation to `/api/login` — from ANY query or mutation, not just the -initial `/api/me`. The user should never be left staring at a hung action or a -generic data-load error when the real cause is an expired session. - -## Scope to decide when promoted - -- Centralize auth-expiry detection in the API client (apps/pwa/src/api/client.ts): - a shared helper that classifies a response as "session expired" (401 or - opaqueredirect) and throws a typed `SessionExpiredError`. -- Apply `redirect: 'manual'` consistently (fetchEvents/mutations) so the guard's - 302 is detectable instead of hanging on the cross-origin follow. -- A single TanStack Query handler (e.g. QueryCache/MutationCache `onError`, or - a small auth-state listener) that, on `SessionExpiredError`, calls a - re-armed `maybeRedirectToLogin()` — the one-shot loop guard must be reset so - a genuine later expiry can redirect again (clear the flag on detected expiry, - not only on successful `/api/me`). -- Decide UX: immediate redirect to `/api/login` vs a brief "Your session - expired — signing you back in…" toast/interstitial before redirecting (the - non-technical Apple member should not see a raw error). -- Make sure an in-flight write that fails on expiry is not silently lost — after - re-auth, either resurface the unsaved action or clearly tell the user it - wasn't saved (ties to T-03-18 no-silent-loss). - -## Notes - -- Relates to the earlier one-shot login redirect work (quick 260606-tv8) and the - OIDC state-cookie-churn fix (commit bb61d21 — gate events query on auth). -- Verify with a real expiry (or shorten `OIDC_AUTH_EXPIRES` / Authelia - `refresh_token_lifespan` in a test) rather than only reasoning about it.