Commit Graph
702 Commits
Author SHA1 Message Date
Lucas Berger f1a2de2cdc docs: resolve D-16 — FamilySync shared calendar synced (id 10) + marked is_shared=1
Operator created the shared 'FamilySync' calendar on the primary Fastmail
account; poller synced it; ran UPDATE calendars SET is_shared=1 WHERE id=10.
Shared color lane now populated; Phase 5 reminders fire on its events.
2026-06-10 10:34:41 -04:00
Lucas BergerandClaude Opus 4.8 456121969f docs(06): create ux-polish phase plan (6 plans, 2 waves)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:25:41 -04:00
Lucas Berger d52acad54c docs(06): add validation strategy 2026-06-10 10:04:34 -04:00
Lucas Berger 3d0ec986a2 docs(06): research phase for ux-polish
Code-verified findings for all six D-01..D-13 fix areas: end-tracking
gap in EventForm, @keyframes pulse absent from tokens.css, hasRrule
missing from CalendarOccurrence type, and ical.js UNTIL/COUNT verified
against project node_modules. Includes validation architecture for TDD
and playwright-cli verification scopes.
2026-06-10 10:02:29 -04:00
Lucas Berger 4b77ec0254 docs(06): add brand assets & iconography contract to UI-SPEC
Appends a new "Brand Assets & Iconography" section covering the
glyph+wordmark mark concept, SVG source-of-truth files, raster export
pipeline, full favicon set (SVG + ICO + index.html gaps), maskable
safe-zone spec, in-app logo usage surfaces, and a complete asset
manifest table. All existing UI-SPEC sections preserved verbatim.
2026-06-10 09:45:00 -04:00
Lucas BergerandClaude Sonnet 4.6 9707fd0d85 docs(06): UI design contract for ux-polish phase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 09:38:54 -04:00
Lucas Berger cdbe94deb2 docs(state): record phase 6 context session 2026-06-10 09:33:43 -04:00
Lucas Berger afdc8d124d docs(06): capture phase context 2026-06-10 09:33:36 -04:00
Lucas Berger 497daf6add docs: add deferred REQ-IDs to REQUIREMENTS traceability table
CAL-09..CAL-12 (v1.x) and DISP-01/DISP-02 (v2) were in the body but
absent from the Traceability table. v1 coverage stays 20/20; deferred
IDs listed separately. Resolves the REQUIREMENTS traceability todo.
2026-06-10 09:26:45 -04:00
Lucas Berger 7369c9f1d1 docs(quick-260610-czd): plan/summary + STATE quick-task row, resolve deployment.md dev-cmd todo 2026-06-10 09:25:24 -04:00
Lucas Berger 39e2ee067e docs(quick-260610-czd-01): add host-side local dev run instructions to deployment.md
- Add 'Running locally (host-side, no Docker)' subsection after dev-auth bypass section
- Explain why plain pnpm dev fails: dev script has no dotenv, root .env sets DB_HOST=mariadb
- Document exact command: build first, then set -a; source .env; set +a && DEV_AUTH_BYPASS=true DB_HOST=localhost pnpm --filter @familysync/api dev
- Add Terminal 2 (PWA) command: pnpm --filter @familysync/pwa dev
- Explain why --env-file is intentionally absent from the dev script
2026-06-10 09:24:22 -04:00
Lucas Berger b745515753 docs(quick-260610-cr8): plan/summary/verification + STATE quick-task row 2026-06-10 09:20:22 -04:00
Lucas Berger 736adf7c58 docs(quick-260610-cr8): adopt drizzle generate+migrate, retire db:push
Plan/summary/verification artifacts; STATE quick-task row (Verified);
archive source todo to completed/.
2026-06-10 09:20:13 -04:00
Lucas Berger 1a95d81a3f docs(quick-260610-cr8-01): repoint deployment.md to generate+migrate, warn against push
- Step 3: replace drizzle-kit push command with drizzle-kit migrate
- Step 3: rewrite description from 'push schema once' to 'apply committed migrations'
- Step 3: add warning callout explaining MariaDB false-truncate foot-gun
- Step 3: add note on db:generate workflow for future schema changes
- Step 6: change push reference to drizzle-kit migrate (Step 3)
2026-06-10 09:16:30 -04:00
Lucas Berger f452400517 chore(quick-260610-cr8-01): remove db:push script from apps/api/package.json
- Delete the 'db:push': 'drizzle-kit push' entry from scripts
- Retain db:generate and db:migrate as the canonical workflow
- JSON remains valid; no other changes
2026-06-10 09:15:30 -04:00
Lucas Berger 68ff72d195 docs(phase-05): add security threat verification (26/26 closed) 2026-06-10 09:09:45 -04:00
Lucas Berger 3b54ea2f12 wip: phase 05 web-push paused — code complete + verified, awaiting iOS/Android device UAT 2026-06-09 22:51:04 -04:00
Lucas Berger 17dfaac5f2 docs(05): mark phase pending iOS/Android device UAT (verification human_needed) 2026-06-09 22:48:07 -04:00
Lucas Berger ecb576eb8a test(05): verification (12/12 code) + persist 5 human UAT items 2026-06-09 22:47:14 -04:00
Lucas Berger 02526d07eb docs(05): code review clean after --fix --all --auto (3 iterations) 2026-06-09 22:41:28 -04:00
Lucas Berger 17756fc523 fix(05-review): NEW-WR-01 emit delete changes when server returns zero events (whole-cache clear)
Pre-capture all currently-cached rows into pendingDeleteRows before the whole-cache
db.delete() when seenUids.length === 0. The existing >0 branch behavior is unchanged.
Adds a regression test verifying onChanges receives one delete change per cached row
on a full-calendar clear.
2026-06-09 22:39:11 -04:00
Lucas Berger c7ef5811d1 fix(05-review): NEW-CR-01 pre-resolve SW registration to eliminate await before pushManager.subscribe
Pre-fetch ServiceWorkerRegistration into component state via useEffect in both
PushPermissionPrompt and SettingsSheet. Gate the subscribe tap target as disabled
until both vapidKey AND swRegistration are ready. The tap handler now has zero
awaits between the user gesture and registration.pushManager.subscribe(), fully
satisfying the iOS user-gesture requirement.
2026-06-09 22:39:05 -04:00
Lucas Berger dc8516beb8 docs(05): add code review report 2026-06-09 22:33:09 -04:00
Lucas Berger 44fbb2bb3a fix(05-review): commit 0004 migration meta (journal + snapshot) 2026-06-09 22:33:09 -04:00
Lucas Berger 1044de57ae test(05-review): add CR-01/WR-01 reminder pruning tests and IN-01 actor-name tests 2026-06-09 22:30:03 -04:00
Lucas Berger 50da9b3bca fix(05-review): IN-01 resolve actor display name in eventChangeDispatcher for D-02/D-03 2026-06-09 22:25:32 -04:00
Lucas Berger 8cecbab7ab fix(05-review): CR-02 change endpoint/p256dh to varchar to prevent InnoDB prefix-index truncation 2026-06-09 22:24:23 -04:00
Lucas Berger 82eccc9017 fix(05-review): CR-04 pre-fetch VAPID key into state; no await before pushManager.subscribe 2026-06-09 22:24:00 -04:00
Lucas Berger e5f7b1ab7c fix(05-review): CR-03 notificationclick uses matchAll+focus+navigate for deep-link URLs 2026-06-09 22:22:27 -04:00
Lucas Berger 7702f7e19a fix(05-review): CR-01+WR-01 prune sentReminders after each scan and mark-sent after dispatch 2026-06-09 22:22:04 -04:00
Lucas Berger f058aefb88 fix(05-review): WR-04 collect delete changes after db.delete to avoid race in sync fan-out 2026-06-09 22:21:37 -04:00
Lucas Berger 814d29dbdd fix(05-review): WR-05 re-confirm push subscription server-side on health-check mount 2026-06-09 22:21:16 -04:00
Lucas Berger 76e0fb9588 fix(05-review): IN-03 use useId() instead of Math.random() for headingId in PushPermissionPrompt 2026-06-09 22:21:05 -04:00
Lucas Berger bf64a0a0e1 fix(05-review): IN-02 add empty-string fallbacks for VAPID env vars in docker-compose 2026-06-09 22:20:50 -04:00
Lucas Berger c69bd30aaa fix(05-review): WR-03 log error.message not raw error in push route catch blocks 2026-06-09 22:20:43 -04:00
Lucas Berger b666b1d114 fix(05-review): WR-02 remove unused 'and' import from listChangeDispatcher 2026-06-09 22:20:30 -04:00
Lucas Berger e496b5e00a docs(05-07): complete eventChangeDispatcher + syncCalendar diff plan 2026-06-09 22:06:14 -04:00
Lucas Berger 30e9de13f9 feat(05-07): implement eventChangeDispatcher + syncCalendar diff/title/onChanges
- Create eventChangeDispatcher.ts: dispatchEventChange + isMeaningfulChange
- D-04: description-only edits are silent; meaningful fields = title/dtstartUtc/dtstartDate/allDay/location
- D-03: actor excluded via ne() + application-level filter; all subs filtered by userId != actorUserId
- D-13: reads only push_subscriptions from MariaDB — no tsdav/Fastmail I/O
- syncCalendar: add optional onChanges callback; populate title from VEVENT SUMMARY on every upsert
- syncCalendar: pre-upsert SELECT to detect add vs update; track changedFields; prune emits deletes
- poller: pass onChanges with actor=cred.userId (external changes from other member)
- outboxWorker.triggerTargetedResync: pass onChanges with actor=userId (this-member writes)
- All 4 eventChangeDispatcher tests + 14 sync tests GREEN
2026-06-09 22:03:52 -04:00
Lucas Berger 4ef6333201 docs(05-08): complete opt-out + reliability surface plan 2026-06-09 21:53:33 -04:00
Lucas Berger 010a69c047 feat(05-08): PermissionDeniedBanner + App mount + CalendarShell onOpenSettings wiring
- Create PermissionDeniedBanner.tsx: role=alert banner shown only when
  permission=denied AND notificationsEnabled=1 (OS-revoked case, D-10)
  with AlertCircle icon, 'Notifications blocked' heading, inline 'How to enable'
  button that opens OS-specific instruction sheet (iOS 4-step / Android 4-step)
- Mount PermissionDeniedBanner and SettingsSheet in App.tsx; wire onOpenSettings
  state from avatar tap through CalendarShell → AppNav → PhoneNav/DesktopNav
- CalendarShell accepts optional onOpenSettings prop, threads to both AppNav usages
- playwright-cli verified: banner renders with exact UI-SPEC copy when
  permission=denied+was-enabled; banner absent when permission=granted;
  'How to enable' opens instruction sheet with correct Android steps;
  SettingsSheet opens from avatar click with toggle + permission-denied hint
2026-06-09 21:50:48 -04:00
Lucas Berger 1de4aa5a3e feat(05-08): SettingsSheet (master toggle D-09) + AppNav avatar promoted to button
- Create SettingsSheet.tsx: bottom sheet (role=dialog, z:301, Escape+backdrop-close)
  with FamilySync Notifications toggle (role=switch, aria-checked, 44px target)
  wired to usePushSubscription setEnabled + permission state
  and inline permission-denied hint (AlertCircle + 'How to enable') when denied
- Promote PhoneNav avatar div to button with onOpenSettings onClick + aria-label
- Add onOpenSettings prop to DesktopNav; add avatar button at sidebar bottom
- Thread onOpenSettings through AppNavProps
- Add @keyframes spin to tokens.css (fixes missing keyframe used by SettingsSheet + SyncStateToast)
2026-06-09 21:45:59 -04:00
Lucas Berger 458d6e4fef feat(05-08): extend usePushSubscription with isSubscribed, setEnabled, permission state (D-10)
- Add isSubscribed state (true when pushManager has active subscription)
- Add setEnabled(on) master toggle: off=unsubscribe, on+granted=silent subscribe, on+default/denied=no-op
- Health-check now respects readNotificationsDisabled() — skip re-subscribe if user explicitly disabled
- Export readNotificationsEnabled for PermissionDeniedBanner/SettingsSheet initial state
- Remove dead readNotificationsEnabled local-only usage (was unused in returned interface)
- persistNotificationsEnabled(false) now writes '0' instead of removing key for explicit off state
2026-06-09 21:43:31 -04:00
Lucas Berger 8e741cf528 docs(05-06): complete reminderScheduler plan 2026-06-09 21:40:22 -04:00
Lucas Berger b95f671485 feat(05-06): implement reminderScheduler — shared timed 15-min reminder scan
- Create apps/api/src/broker/reminderScheduler.ts:
  - runReminderCheck(now): queries isShared=true + allDay=false events
    with dtstartUtc in [now+14min, now+16min] via calendarEvents→calendars→
    pushSubscriptions cross-join (2 innerJoins; fans out to all subscribers)
  - In-memory sentReminders Set keyed uid:minuteBucket prevents double-fire
    at window boundary (D-06, T-05-18)
  - Per-event and per-subscription try/catch for error isolation (T-05-19)
  - Null title fallback (event.title ?? uid) — handles rows before Plan 05-07
  - Empty shared-calendar set produces zero sends and no crash (D-16)
  - startReminderScheduler(): node-cron 1-min schedule wrapping runReminderCheck
- Wire startReminderScheduler() into index.ts isMainModule() guard after
  startOutboxWorker() and VAPID setVapidDetails (NOTIF-01)
- Tests: 3/3 GREEN (all-day excluded, non-shared excluded, dedup)
2026-06-09 21:38:32 -04:00
Lucas Berger 9b04528fd6 docs(05-05): complete list-change push plan 2026-06-09 21:29:09 -04:00
Lucas Berger d2ce4e08c7 feat(05-05): hook notifyListChange into list/item mutations (reorder excluded)
- POST /:id/items (item added) → notifyListChange
- PATCH /list-items/:itemId checked/text → notifyListChange; position-only → silent (D-01)
- DELETE /list-items/:itemId → notifyListChange
- PATCH /:id (list rename/share toggle) → notifyListChange
- DELETE /:id (list delete) → notifyListChange
- POST / (list create) → no notification (empty list, D-01 spirit)
- lists.test.ts: 2 new tests prove reorder-silent (position) and check-notifies (NOTIF-02)
- All 59 lists.test.ts assertions GREEN
2026-06-09 21:26:29 -04:00
Lucas Berger 69231043e4 feat(05-05): implement listChangeDispatcher — access-scoped, self-suppressed, coalesced push (NOTIF-02)
- notifyListChange(listId, actorId, windowMs?) wraps coalesceListPush with a
  dispatch closure that resolves actor name + list name from DB, builds
  audience as owner ∪ list_shares MINUS actorId (D-03), and calls
  dispatchPush per accessible subscriber subscription
- D-02 generic copy: '{Actor} made {N} changes to {ListName}' — no item text
- D-03 self-suppression: actorId filtered from audience before subscription load
- T-05-14: audience strictly scoped to list access (owner + list_shares only)
- T-05-15: no item text in notification body
- Empty audience and missing subscriptions are silent no-ops
- Tests: 5/5 GREEN (burst→1 push, self-suppress, access scope, empty audience)
2026-06-09 21:24:48 -04:00
Lucas Berger 97f7026095 test(05-05): add failing tests for listChangeDispatcher — RED gate
- burst coalescing: N calls → 1 dispatchPush to non-actor with count=N
- D-03 self-suppression: actor's own subscription never dispatched
- T-05-14 access scoping: unrelated user (no owner/share) excluded
- empty audience (no other members): no dispatch, no crash
- empty audience (other member has no subscription): no dispatch, no crash
2026-06-09 21:20:34 -04:00
Lucas Berger 60c247d8ed docs(05-04): complete push vertical slice plan 2026-06-09 21:16:23 -04:00
Lucas Berger d816f79271 fix(05-04): wrap setVapidDetails in try/catch to prevent startup crash on bad VAPID key
- Bad/truncated VAPID_PRIVATE_KEY logs a warning instead of crashing the server
- API still starts and serves all other routes; push dispatch fails with a log message
2026-06-09 21:13:58 -04:00