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
@@ -11,14 +11,14 @@ Deliver Web Push so both members receive timely, reliable notifications on the
installed PWA (iOS + Android) for three triggers:
1. **Event reminders** — ~15 min before a **shared Family-calendar** event starts (NOTIF-01)
2. **Event-change alerts** — when the *other* member adds/changes a relevant event (NOTIF-03)
3. **List-change alerts** — when the *other* member modifies a shared list (NOTIF-02)
2. **Event-change alerts** — when the _other_ member adds/changes a relevant event (NOTIF-03)
3. **List-change alerts** — when the _other_ member modifies a shared list (NOTIF-02)
Plus the iOS reliability machinery (subscription health-check + visible-notification
guarantee) that keeps subscriptions alive across inactivity (success criterion 4).
**Not in this phase:** quiet-hours/DND, per-event custom reminder offsets,
per-category opt-out, notifying on the member's *own* changes, reminders for
per-category opt-out, notifying on the member's _own_ changes, reminders for
personal calendars (see decisions + deferred).
</domain>
@@ -26,6 +26,7 @@ personal calendars (see decisions + deferred).
## Implementation Decisions
### Notification copy & anti-spam
- **D-01:** **Coalesce list-change pushes per list** within a short window (~3060s).
A grocery burst (many rapid edits) collapses into one push, not one-per-change.
Planner must define the debounce/window mechanism. Reorder (`position`) changes
@@ -34,7 +35,7 @@ personal calendars (see decisions + deferred).
changes) show **specifics** — title, time, action (e.g. `Lucas moved Dentist → Wed 3pm`,
`Soccer practice starts in 15 min`). **List pings stay generic** — they name the
actor, the list, and a change count, but **not item text** (e.g. `Wife made 3 changes
to Groceries`). Rationale: lists are the chattier, lower-stakes source; generic keeps
to Groceries`). Rationale: lists are the chattier, lower-stakes source; generic keeps
the lock screen cleaner.
- **D-03:** **Name the actor** in every change notification (`Wife checked off…`,
`Lucas added…`). Two-person household — attribution is clear and useful.
@@ -43,14 +44,15 @@ personal calendars (see decisions + deferred).
stay silent.** Avoids noise from trivial tweaks.
### Reminder scope & timing
- **D-05:** **Reminders fire for SHARED Family-calendar events only***by design*,
- **D-05:** **Reminders fire for SHARED Family-calendar events only**_by design_,
not as a limitation. Each member's **native device calendar app** (Apple Calendar /
Android, syncing their Fastmail personal calendar) already fires reminders for personal
events; FamilySync must **not duplicate** those. FamilySync owns reminders for the
**shared Family calendar** — the cross-ecosystem coordination gap the native clients
don't reliably cover. This narrows the literal reading of NOTIF-01 deliberately;
verification must treat "shared-calendar events" as the reminder surface.
- **Caveat for planner:** if a member *also* subscribes the shared calendar in their
- **Caveat for planner:** if a member _also_ subscribes the shared calendar in their
native calendar app they could get duplicate reminders — that's a household setup
choice, out of our control. Do not engineer against it.
- **Dependency:** the shared "Family" calendar is `is_shared=1`. Per Phase 2 D-16 the
@@ -63,6 +65,7 @@ personal calendars (see decisions + deferred).
timed events only. (They remain visible in the app.)
### Onboarding & opt-out
- **D-08:** **Contextual permission prompt right after PWA install** (or first installed
launch): a one-line explainer, then trigger `Notification.requestPermission()` /
`pushManager.subscribe()` on a **tap gesture**. iOS hard-requires installed-PWA + a user
@@ -78,6 +81,7 @@ personal calendars (see decisions + deferred).
itself** was revoked. This is the user-facing half of the mandatory iOS health-check.
### Carried forward — locked, NOT re-discussed
- **D-11:** **iOS reliability is mandatory from day one (STATE.md):** subscription
health-check + `event.waitUntil()` in the SW + **every push must display a visible
notification** (no silent pushes — iOS revokes after ~3). This is non-negotiable
@@ -91,6 +95,7 @@ personal calendars (see decisions + deferred).
deep-linking. Tap targets use real URLs.
### Claude's Discretion (researcher / planner decide)
- **No quiet-hours / DND in v1** — reminders and alerts always fire immediately.
(Deferred; revisit if it proves annoying in use.)
- **Tap-to-open deep-link targets** (obvious mapping, not separately discussed):
@@ -105,16 +110,18 @@ personal calendars (see decisions + deferred).
implementation.
- **Event-change detection source:** poller (`broker/poller.ts`, external changes) vs
outbox-confirm (`broker/outboxWorker.ts`, this-member writes) — pick the trigger point(s)
that fire for the *other* member without notifying the actor (D-03 implies suppress
that fire for the _other_ member without notifying the actor (D-03 implies suppress
self-notifications).
</decisions>
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Push / iOS / VAPID constraints
- `CLAUDE.md` — "React PWA Stack" iOS push requirements table (iOS 16.4 min, Home-Screen
install required, user-gesture subscribe, silent push unsupported → visible notification
mandatory, no BackgroundSync) AND the `web-push` (VAPID) stack entry. **Authoritative
@@ -123,12 +130,14 @@ personal calendars (see decisions + deferred).
health-check + `event.waitUntil()` mandatory from day one.
### Phase scope & requirements
- `.planning/ROADMAP.md` §"Phase 5: Web Push Notifications" — goal, 4 success criteria,
NOTIF-01/02/03, MVP mode, Depends on Phase 3 + 4.
- `.planning/REQUIREMENTS.md` — NOTIF-01 (event reminder), NOTIF-02 (list-change alert),
NOTIF-03 (event add/change alert).
### Prior locked decisions this phase builds on
- `.planning/phases/04-shared-lists-live-sync/04-CONTEXT.md` — D-04 (scoped SSE fan-out),
D-17 (react-router for deep-linking), D-18 (member-count-agnostic schema/auth/fan-out),
fan-out mechanism justification (in-memory emitter, no Redis).
@@ -139,6 +148,7 @@ personal calendars (see decisions + deferred).
(and thus reminders) exist.
### Integration code (read before implementing)
- `apps/api/src/lib/listEmitter.ts` — list-change publish points; push dispatch hooks here.
- `apps/api/src/broker/poller.ts`, `apps/api/src/broker/outboxWorker.ts` — event-change
detection sources.
@@ -146,12 +156,14 @@ personal calendars (see decisions + deferred).
contextual permission prompt (D-08).
- `apps/pwa/vite.config.*` — current vite-plugin-pwa `generateSW`/`autoUpdate` config (SW
strategy decision, D-discretion).
</canonical_refs>
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- **`apps/api/src/lib/listEmitter.ts` (`publishListEvent`)** — list-change events are already
emitted at the right points for Phase 4 SSE. Push dispatch for NOTIF-02 hooks the same call
sites; coalescing (D-01) wraps the dispatch.
@@ -163,6 +175,7 @@ personal calendars (see decisions + deferred).
permission prompt (D-08).
### Established Patterns
- **Broker-only Fastmail I/O (D-13):** notification code reads the MariaDB cache, never tsdav.
- **In-memory single-process fan-out (D-12):** no Redis/ioredis; push mirrors SSE topology.
- **vite-plugin-pwa `generateSW` + `autoUpdate`:** adding `push`/`notificationclick` handlers
@@ -171,14 +184,15 @@ personal calendars (see decisions + deferred).
who can see a list/event — never broadcast to all members. Suppress self-notifications.
### Integration Points
- **New reminder scheduler:** a server-side interval/cron scanning *shared-calendar timed
events* in the MariaDB cache, firing ~15 min pre-start (D-05/D-06/D-07). New infra — no
- **New reminder scheduler:** a server-side interval/cron scanning _shared-calendar timed
events_ in the MariaDB cache, firing ~15 min pre-start (D-05/D-06/D-07). New infra — no
analog exists yet.
- **New push-subscription store:** member-count-agnostic table for VAPID subscriptions
(per D-18); SW push handler; `web-push` server dispatch (`web-push` not yet installed).
- **Permission/subscription lifecycle** on the PWA: request → subscribe → persist → health-check
→ silent re-subscribe (D-08/D-10/D-11).
</code_context>
</code_context>
<specifics>
## Specific Ideas
@@ -188,7 +202,7 @@ personal calendars (see decisions + deferred).
`"{EventTitle} starts in 15 min"`.
- Reminder surface is the **shared Family calendar only** to avoid double-notifying against
native device calendar reminders — this is the load-bearing rationale behind D-05.
</specifics>
</specifics>
<deferred>
## Deferred Ideas
@@ -198,18 +212,19 @@ personal calendars (see decisions + deferred).
- **Per-category opt-out** (independent reminder / event-change / list-change toggles). v1.x.
- **Reminders for personal-calendar events** — intentionally excluded (native clients cover
these, D-05). Only revisit if the household stops relying on native reminders.
- **Notifying on the member's own changes** — out of scope; alerts are for the *other* member.
- **Notifying on the member's own changes** — out of scope; alerts are for the _other_ member.
### Reviewed Todos (not folded)
- **"Adopt drizzle generate+migrate workflow (retire db:push on MariaDB)"** — keyword match
on "push" was a false positive (DB migrations, not Web Push). BUT the underlying constraint
still applies: Phase 5 adds a push-subscription table; new tables MUST use
`drizzle-kit generate` + `migrate`, never `db:push` (unsafe on populated MariaDB). Noted as a
schema constraint for the planner, not folded as discussion scope.
- **"Kick off FamilySync with /gsd:new-project"** — stale kickoff todo; not relevant.
</deferred>
</deferred>
---
*Phase: 05-web-push-notifications*
*Context gathered: 2026-06-09*
_Phase: 05-web-push-notifications_
_Context gathered: 2026-06-09_