Milestone v1.0: FamilySync MVP #1

Merged
luckberg merged 376 commits from gsd/v1.0-milestone into main 2026-06-10 17:39:19 -04:00
3 changed files with 135 additions and 11 deletions
Showing only changes of commit 0c2c26c375 - Show all commits
+3 -3
View File
@@ -237,7 +237,7 @@ Plans:
- [x] 06-01-PLAN.md — TDD: duration-preserving end-tracking math (computeNewTimedEnd/computeNewAllDayEnd) in eventDateTime.ts (D-04)
- [x] 06-02-PLAN.md — TDD: RRULE UNTIL/COUNT serialization + Zod acceptance + FREQ-persistence regression (vevent/outboxWorker/events route) (D-06/D-07)
- [ ] 06-03-PLAN.md — TDD: hasRrule on CalendarOccurrence + bounded-expansion lock (expand.ts) (D-06/D-08)
- [x] 06-03-PLAN.md — TDD: hasRrule on CalendarOccurrence + bounded-expansion lock (expand.ts) (D-06/D-08)
- [ ] 06-04-PLAN.md — Spinner/pulse: global @keyframes pulse + remove redundant spin redefinition (D-13)
- [ ] 06-05-PLAN.md — Auth gating slice: SessionExpiredError + AuthSplash + global QueryCache/MutationCache error handler; client.ts type mirrors (D-10/D-11, + D-06/D-08 type carriers)
@@ -260,7 +260,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple
| 3. Event Write-Back + PWA Install | 12/12 | Complete | 2026-06-07 |
| 4. Shared Lists + Live Sync | 6/6 | Complete | 2026-06-09 |
| 5. Web Push Notifications | 8/8 | Complete | 2026-06-10 |
| 6. UX Polish | 2/6 | In Progress| |
| 6. UX Polish | 3/6 | In Progress| |
## Backlog
@@ -268,7 +268,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple
**Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern.
**Requirements:** TBD
**Plans:** 2/6 plans executed
**Plans:** 3/6 plans executed
Plans:
+10 -8
View File
@@ -3,14 +3,14 @@ gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: executing
stopped_at: Phase 06 UI-SPEC approved
last_updated: "2026-06-10T15:03:08.173Z"
last_activity: 2026-06-10 -- Phase 06 execution started
stopped_at: "Completed 06-03: hasRrule server-side exposure"
last_updated: "2026-06-10T15:20:02.358Z"
last_activity: "2026-06-10 -- Completed 06-02: RRULE UNTIL/COUNT serialization + FREQ persistence"
progress:
total_phases: 17
completed_phases: 5
total_plans: 42
completed_plans: 38
completed_plans: 39
percent: 29
---
@@ -26,7 +26,7 @@ See: .planning/PROJECT.md (updated 2026-06-07)
## Current Position
Phase: 06 (ux-polish) — EXECUTING
Plan: 3 of 6
Plan: 4 of 6
Status: Ready to execute
Last activity: 2026-06-10 -- Completed 06-02: RRULE UNTIL/COUNT serialization + FREQ persistence
@@ -75,6 +75,7 @@ Progress: [█████████░] 89%
| Phase 05 P07 | 8 | 1 tasks | 4 files |
| Phase 06-ux-polish P01 | 2 | 2 tasks | 2 files |
| Phase 06-ux-polish P02 | 8 | 2 tasks | 4 files |
| Phase 06-ux-polish P03 | 11 | 2 tasks | 3 files |
## Accumulated Context
@@ -116,6 +117,7 @@ Recent decisions affecting current work:
- [Phase ?]: notifyListChange fires for all list/item mutations except reorder (position) and list-create per D-01
- [Phase ?]: D-05-06-crossjoin: Drizzle cross-join in reminderScheduler pairs shared events with all pushSubscriptions; grouping by uid post-join ensures full fan-out per deduped event (reminderScheduler.ts)
- [Phase ?]: D-03 actor exclusion: ne() at DB level + filter() in application code (defence-in-depth for eventChangeDispatcher tests)
- [Phase ?]: D-08: hasRrule derived from event.isRecurring() in expand.ts — no DB query change needed; captured once before branch
### Roadmap Evolution
@@ -159,6 +161,6 @@ Recent decisions affecting current work:
## Session Continuity
Last session: 2026-06-10T15:03:08.167Z
Stopped at: Phase 06 UI-SPEC approved
Resume file: .planning/phases/06-ux-polish/06-UI-SPEC.md
Last session: 2026-06-10T15:20:02.349Z
Stopped at: Completed 06-03: hasRrule server-side exposure
Resume file: None
@@ -0,0 +1,122 @@
---
phase: "06-ux-polish"
plan: "03"
subsystem: "api/broker"
tags: ["tdd", "expand", "hasRrule", "ical", "recurrence", "d-08", "d-06"]
dependency_graph:
requires:
- "apps/api/src/broker/expand.ts (CalendarOccurrence interface)"
- "apps/api/tests/fixtures/*.ics (existing fixtures)"
provides:
- "CalendarOccurrence.hasRrule: boolean (server source-of-truth)"
- "weekly-count3.ics test fixture (bounded RRULE, COUNT=3)"
- "expand.test.ts hasRrule + bounded-RRULE assertions"
affects:
- "apps/api/src/broker/expand.ts (CalendarOccurrence consumers — routes/events.ts)"
- "apps/pwa/src/api/client.ts (mirror field added in Plan 05)"
tech_stack:
added: []
patterns:
- "TDD RED→GREEN: test-only commit followed by implementation commit"
- "Capture event.isRecurring() once before branch, pass to both push sites"
- "epochMilliseconds (not epochSeconds) for Temporal duration arithmetic with temporal-polyfill"
key_files:
created:
- "apps/api/tests/fixtures/weekly-count3.ics"
modified:
- "apps/api/src/broker/expand.ts"
- "apps/api/tests/broker/expand.test.ts"
decisions:
- "D-08: hasRrule derived from event.isRecurring() — no DB query change needed (already available on the parsed ICAL.Event)"
- "Captured isRecurring once before the non-recurring/recurring branch (single capture pattern from PATTERNS.md)"
- "epochMilliseconds used for Temporal duration math — temporal-polyfill returns number not BigInt for this property"
- "weekly-count3.ics uses UTC DTSTART/DTEND (no VTIMEZONE needed) for simplicity in the bounded test fixture"
metrics:
duration: "11m"
completed: "2026-06-10"
tasks_completed: 2
files_modified: 3
---
# Phase 06 Plan 03: hasRrule Server-Side Exposure Summary
Added `hasRrule: boolean` to the `CalendarOccurrence` interface in `expand.ts` and populated it via `event.isRecurring()` — the server-side signal that gates the whole-series-edit confirmation prompt (D-08).
## Tasks Completed
| # | Task | Type | Commit | Outcome |
|---|------|------|--------|---------|
| 1 | RED — failing tests for hasRrule + bounded expansion | TDD test | 593302e | 3 hasRrule failures + duration test confirmed red |
| 2 | GREEN — add hasRrule to interface and populate it | TDD impl | 44d336c | 10/10 expand tests pass |
## What Was Built
### `apps/api/src/broker/expand.ts`
Added `hasRrule: boolean` field to `CalendarOccurrence` interface with JSDoc. Captured `const isRecurring = event.isRecurring()` once before the non-recurring/recurring branch. Both `occurrences.push({...})` sites now include `hasRrule: isRecurring` — false in the non-recurring branch, true in the recurring branch.
### `apps/api/tests/broker/expand.test.ts`
Added two new `describe` blocks:
**`hasRrule field — D-08`** (2 tests):
- `weekly-dst.ics` (recurring): all occurrences have `hasRrule === true`
- `single-duration.ics` (non-recurring): the single occurrence has `hasRrule === false`
**`Bounded RRULE (COUNT=3) — D-06 invariant`** (3 tests):
- `COUNT=3` within a 6-month window returns exactly 3 occurrences
- Each bounded occurrence duration = 1 hour from DTSTART→DTEND (not recurrence span)
- Bounded occurrences have `hasRrule === true`
### `apps/api/tests/fixtures/weekly-count3.ics`
New fixture: `FREQ=WEEKLY;COUNT=3`, `DTSTART:20260601T090000Z`, `DTEND:20260601T100000Z` (1-hour UTC events). Used for the bounded expansion invariant test.
## TDD Gate Compliance
- RED commit (`test(06-03): ...`) at `593302e` — tests written first, confirmed failing due to absent `hasRrule` field
- GREEN commit (`feat(06-03): ...`) at `44d336c` — implementation added, all 10 tests pass
## Deviations from Plan
**1. [Rule 1 - Bug] Duration test using `epochMilliseconds` instead of `epochSeconds`**
- **Found during:** Task 1 test writing
- **Issue:** `Temporal.ZonedDateTime.epochSeconds` returns `NaN` in the `temporal-polyfill` package used in the test suite; `epochMilliseconds` returns a regular `number`
- **Fix:** Duration assertion uses `endZdt.epochMilliseconds - startZdt.epochMilliseconds` and compares to `3_600_000` (1 hour in ms)
- **Files modified:** `apps/api/tests/broker/expand.test.ts`
- **Commit:** 593302e (incorporated into RED commit before final form)
No other deviations. Plan executed as written.
## Verification
```
npx vitest run tests/broker/expand.test.ts
Test Files 1 passed (1)
Tests 10 passed (10)
```
```
grep -n "hasRrule" apps/api/src/broker/expand.ts
68: hasRrule: boolean
224: // Capture once — used in both branches to populate hasRrule.
261: hasRrule: isRecurring, // always false in the non-recurring branch
308: hasRrule: isRecurring, // always true in the recurring branch
```
`client.ts` untouched — mirror field is Plan 05's responsibility.
## Known Stubs
None. `hasRrule` is fully populated from `event.isRecurring()` — no placeholder values.
## Threat Flags
No new threat surface. `hasRrule` is a boolean derived from data already returned to the authenticated caller. T-06-03 accepted in plan threat model.
## Self-Check: PASSED
- `apps/api/src/broker/expand.ts` exists and contains `hasRrule`
- `apps/api/tests/fixtures/weekly-count3.ics` exists
- `apps/api/tests/broker/expand.test.ts` contains `hasRrule` assertions
- Commits 593302e and 44d336c exist in git log