docs: expand backlog 999.4 — per-event reminder config + scheduler honors VALARM
Folded the new capture into 999.4 instead of a fragmented duplicate: - Half A (existing): author per-event VALARM in the event form, incl. 'no reminder'. - Half B (new, 2026-06-10): reminderScheduler honors each event's VALARM TRIGGER instead of the hardcoded 15-min lead, and fires nothing when there's no alarm — preserving the catch-up + per-uid exactly-once dedup guarantees.
This commit is contained in:
+27
-82
@@ -274,43 +274,17 @@ Plans:
|
|||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
### Phase 999.2: Slick unauthenticated-entry — no calendar/"Sign-in required" flash before Authelia redirect (BACKLOG)
|
### Phase 999.4: Per-event reminder configuration (VALARM authoring + scheduler honors it) (BACKLOG)
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] On a cold unauthenticated load the PWA briefly paints the calendar shell + skeleton, then flashes a "Sign-in required" error, then redirects to Authelia — not slick (violates the "low-friction for the non-technical Apple member" hard constraint). Make unauthenticated entry render a single neutral "Signing you in…" splash and go straight to Authelia, with no app content or error text painted first.
|
**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.
|
||||||
|
|
||||||
**Root cause** (diagnosed during Phase 03 Gate 2 live verification, 2026-06-07) — `apps/pwa/src/components/CalendarShell.tsx`: the component renders optimistically before auth is known. While `meQuery` (GET `/api/me`) is pending, `isInitialLoading` renders the calendar shell + `SkeletonCalendar`. When `meQuery` resolves as an `opaqueredirect` (unauthenticated — `fetchMe` uses `redirect:'manual'` in `apps/pwa/src/api/client.ts`), it errors and in the same tick (1) the early return `if (meQuery.isError) return <div role="alert">Sign-in required</div>` (~line 187) paints, and (2) a `useEffect` calls `window.location.href='/api/login'`. Because the navigation is async, React paints "Sign-in required" for ~one frame before leaving for Authelia. Net: calendar flash → "Sign-in required" flash → Authelia.
|
**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).
|
||||||
|
|
||||||
**Proposed fix:** Gate the app render on auth state — (a) don't render CalendarContent/skeleton until `meQuery.isSuccess`; (b) while unauthenticated and redirecting, render a neutral full-screen "Signing you in…" splash instead of the "Sign-in required" alert; (c) reserve the "Sign-in required" dead-end only for the one-shot-guard fall-through (already bounced through `/api/login` and still failing). Optionally hoist the auth check above the heavy calendar mount.
|
**Half B — scheduler honors the provider's value (NEW, surfaced 2026-06-10):** Today `apps/api/src/broker/reminderScheduler.ts` runs a **hardcoded 15-minute** scan for shared timed events (`index.ts:139` "starting in ~15 min"; reminderScheduler header "15-min reminder scan") and never reads the event's actual alarm. So every reminder fires 15 min before regardless of what the event (or the calendar provider) specifies, and an event with **no** alarm still gets a 15-min push. Change the scheduler to read each event's `VALARM` `TRIGGER` (the value written in Half A / set in Fastmail or another native client) and fire at that lead — and fire **nothing** when the event has no alarm. The current fixed 15-min window/dedup logic (catch-up scan, per-uid exactly-once — see quick 260610-hbu) must be generalized to a variable per-event lead.
|
||||||
|
|
||||||
**Severity:** low / cosmetic, but hits every unauthenticated cold load and the wife's first impression. Tags: phase-03, ux-polish, auth.
|
**Boundary:** preserve the reminder scheduler's resilience guarantees (catch-up on a missed tick, per-uid exactly-once dedup). This makes the lead per-event/variable rather than constant; it is not a rewrite of the scan/dedup design.
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
**Severity:** medium — feature gap surfaced during Phase 03 Gate 2 testing; Half B surfaced 2026-06-10. Tags: phase-03, phase-05, calendar, write-back, reminders, valarm, push, scheduler, phase-05-dependency.
|
||||||
|
|
||||||
- [ ] 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
|
**Requirements:** TBD
|
||||||
**Plans:** 0 plans
|
**Plans:** 0 plans
|
||||||
|
|
||||||
@@ -340,56 +314,6 @@ Plans:
|
|||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
### Phase 999.6: All-day events should stand out visually (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] All-day events currently look identical to timed events except for the absence of a time label, so they don't read as "all-day" at a glance. Give them a distinct visual treatment (e.g. a full-width pill/bar in the all-day row, a different shape/border, or a subtle background band) so they're immediately distinguishable from timed events.
|
|
||||||
|
|
||||||
**Context** (surfaced 2026-06-07, Gate 2 live testing). Tags: phase-03, ui, calendar, ux-polish.
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.7: Event form — auto-advance end when start moves; keep duration sane (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] In the create/edit form, moving the start date/time into the future does NOT move the end with it, so the event keeps a stale end and can become an absurdly long event. Auto-advance the end to preserve the current duration (or snap to a sensible default, e.g. +1h timed / same-day all-day) whenever the start changes, and guard against accidentally-multi-day durations.
|
|
||||||
|
|
||||||
**Context** (surfaced 2026-06-07): a "recurring event" was created with start 2026-06-11 and end 2026-08-13 — a ~2-month-long event — because the end did not track the start. Combined with a weekly RRULE this rendered as overlapping bars spanning the calendar (looked duplicated). Tags: phase-03, ui, event-form, ux.
|
|
||||||
|
|
||||||
**Also fix here — all-day EDIT off-by-one (latent bug found 2026-06-07):** the write path treats the form's all-day end date as INCLUSIVE and writes an exclusive `DTEND` (+1, WR-04 in `vevent.ts`). But the edit form populates its end-date field from the raw occurrence end, which is the EXCLUSIVE `DTEND` (start+1 for a 1-day event). So editing an all-day event and saving advances `DTEND` by another day — the event grows by one day per edit. The form must convert the exclusive occurrence end back to the inclusive date (−1 day) when populating all-day edits, symmetric with the create write and the display fix (commit d4d5327, hydrateEvents).
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.8: Recurrence bound (repeat-until / count) + recurring create polish (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] The recurrence control is just a frequency preset (none/daily/weekly/…) with no way to bound the series, so users reach for the event end-date to mean "repeat until" — producing one giant multi-month event instead of N short occurrences. Add a "repeat until <date>" (or "for N occurrences") control that writes RRULE UNTIL/COUNT, keeping each occurrence's duration tied to start→end (not the recurrence span). Also verify the frequency dropdown writes the selected FREQ (a daily selection appeared to persist as weekly — confirm/fix).
|
|
||||||
|
|
||||||
**Context** (surfaced 2026-06-07): stored event had `RRULE:FREQ=WEEKLY` with a 2-month DTSTART→DTEND duration; user expected daily and a bounded series. Recurring is "create + display only" in v1 (D-decision); this is the create-side UX gap. Tags: phase-03, recurrence, event-form, caldav, ux.
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.9: Edit a recurring series (whole-series edit) (BACKLOG)
|
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] There is currently no way to edit a recurring series — opening an occurrence offers no "edit this series" path. Add whole-series editing (modify the master VEVENT: title/time/RRULE/etc.). NOTE: single-occurrence editing (RECURRENCE-ID) and "this and following" are already deferred to v1.x (see Deferred Items); this item is specifically the series-level edit, the most common case, and may be worth pulling earlier than the per-occurrence variants.
|
|
||||||
|
|
||||||
**Context** (surfaced 2026-06-07, Gate 2 live testing). Tags: phase-03, recurrence, event-form, caldav.
|
|
||||||
**Requirements:** TBD
|
|
||||||
**Plans:** 0 plans
|
|
||||||
|
|
||||||
Plans:
|
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
|
||||||
|
|
||||||
### Phase 999.10: Admin Settings / Administration section — manage app passwords + designate the shared calendar via UI (BACKLOG)
|
### Phase 999.10: Admin Settings / Administration section — manage app passwords + designate the shared calendar via UI (BACKLOG)
|
||||||
|
|
||||||
**Goal:** [Captured for future planning] Add an in-app **Settings/Administration** section, gated to an administrator role, for configuration that today requires manual backend/DB steps:
|
**Goal:** [Captured for future planning] Add an in-app **Settings/Administration** section, gated to an administrator role, for configuration that today requires manual backend/DB steps:
|
||||||
@@ -460,3 +384,24 @@ Plans:
|
|||||||
Plans:
|
Plans:
|
||||||
|
|
||||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|
||||||
|
### Phase 999.14: Gitea CI — full regression on PR to main + build/publish Docker image (BACKLOG)
|
||||||
|
|
||||||
|
**Goal:** [Captured for future planning] The repo is committed against a self-hosted Gitea instance with a registered Actions runner, but there is no CI yet (no `.gitea/workflows/` or `.github/workflows/`). Two things should run automatically: (1) **full regression** on every PR targeting `main` — gating the merge; (2) **build the app's Docker image and publish it** to the Gitea container registry.
|
||||||
|
|
||||||
|
**Options / decisions to make when picking this up:**
|
||||||
|
- **Test scope:** "full regression" = lint + typecheck + unit + the API integration tests. Integration tests need a real MariaDB (see [[api-integration-test-db]]) — the workflow must spin up a MariaDB service container, bind it, and set `DB_HOST=127.0.0.1` + `.env` creds. The PWA build/test also runs.
|
||||||
|
- **Monorepo:** pnpm workspace (`apps/api`, `apps/pwa`, shared). Cache the pnpm store.
|
||||||
|
- **Docker images:** only `apps/api/Dockerfile` exists today — there is no PWA Dockerfile yet. Decide one image (API) vs. also building/serving the PWA. Tag scheme + when to publish (only on merge to `main`? on tags? per-PR?).
|
||||||
|
- **Registry auth:** push to the Gitea registry using the runner's Gitea-provided token or a dedicated package-write token.
|
||||||
|
- Gitea Actions are GitHub-Actions-compatible syntax but run on the self-hosted runner — confirm runner labels and available images, and that Actions is enabled, before authoring.
|
||||||
|
|
||||||
|
**Likely shape:** a `.gitea/workflows/ci.yml` — `on: pull_request` (to `main`) → install (pnpm), lint, typecheck, unit, API integration vs. a `mariadb` service container, PWA build; `on: push` to `main`/tag → `docker build apps/api/Dockerfile`, login, push tagged image.
|
||||||
|
|
||||||
|
**Context:** Promoted from STATE.md pending todo (`.planning/todos/pending/2026-06-10-gitea-ci-regression-and-docker-publish.md`), surfaced 2026-06-10. Tags: tooling, ci, gitea, docker, mariadb, monorepo.
|
||||||
|
**Requirements:** TBD
|
||||||
|
**Plans:** 0 plans
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||||
|
|||||||
Reference in New Issue
Block a user