docs(backlog): promote session-timeout + event-reminder todos to backlog (999.3/999.4)

This commit is contained in:
Lucas Berger
2026-06-07 18:01:18 -04:00
parent 2e10752a59
commit 86069b89c1
3 changed files with 28 additions and 105 deletions
@@ -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.
@@ -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.