168 lines
13 KiB
Markdown
168 lines
13 KiB
Markdown
---
|
||
gsd_state_version: 1.0
|
||
milestone: v1.0
|
||
milestone_name: milestone
|
||
status: executing
|
||
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: 39
|
||
percent: 29
|
||
---
|
||
|
||
# Project State
|
||
|
||
## Project Reference
|
||
|
||
See: .planning/PROJECT.md (updated 2026-06-07)
|
||
|
||
**Core value:** One color-coded family calendar (shared + personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store
|
||
**Current focus:** Phase 06 — ux-polish
|
||
|
||
## Current Position
|
||
|
||
Phase: 06 (ux-polish) — EXECUTING
|
||
Plan: 4 of 6
|
||
Status: Ready to execute
|
||
Last activity: 2026-06-10 -- Completed 06-02: RRULE UNTIL/COUNT serialization + FREQ persistence
|
||
|
||
Progress: [█████████░] 89%
|
||
|
||
## Performance Metrics
|
||
|
||
**Velocity:**
|
||
|
||
- Total plans completed: 17
|
||
- Average duration: -
|
||
- Total execution time: 0 hours
|
||
|
||
**By Phase:**
|
||
|
||
| Phase | Plans | Total | Avg/Plan |
|
||
|-------|-------|-------|----------|
|
||
| 02 | 5 | - | - |
|
||
| 03 | 12 | - | - |
|
||
|
||
**Recent Trend:**
|
||
|
||
- Last 5 plans: -
|
||
- Trend: -
|
||
|
||
*Updated after each plan completion*
|
||
| Phase 03 P01 | 45 | 5 tasks | 8 files |
|
||
| Phase 03 P03-02 | 4 | 2 tasks | 2 files |
|
||
| Phase 03 P03-03 | 7 | 3 tasks | 2 files |
|
||
| Phase 03 P03-07 | 5 | 2 tasks | 7 files |
|
||
| Phase 03 P03-04 | 15 | 2 tasks | 3 files |
|
||
| Phase 03 P03-05 | 6 | 3 tasks | 6 files |
|
||
| Phase 04 P01 | 65 | 4 tasks | 17 files |
|
||
| Phase 04 P03 | 12 | 2 tasks | 9 files |
|
||
| Phase 04 P04 | 11 | 2 tasks | 10 files |
|
||
| Phase 04 P05 | 10 | 2 tasks | 4 files |
|
||
| Phase 04 P06 | 11 | 2 tasks | 7 files |
|
||
| Phase 04 P07 | 6 | 2 tasks | 4 files |
|
||
| Phase 05 P01 | 20 | 4 tasks | 15 files |
|
||
| Phase 05 P02 | 5 | 1 tasks | 1 files |
|
||
| Phase 05 P03 | 5 | - tasks | - files |
|
||
| Phase 05 P04 | 11 | 3 tasks | 9 files |
|
||
| Phase 05 P05 | 8 | 2 tasks | 4 files |
|
||
| Phase 05 P06 | 6 | 1 tasks | 2 files |
|
||
| Phase 05 P08 | 9 | 3 tasks | 7 files |
|
||
| 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
|
||
|
||
### Decisions
|
||
|
||
Decisions are logged in PROJECT.md Key Decisions table.
|
||
Recent decisions affecting current work:
|
||
|
||
- CAL-08 RESOLVED → GO (Phase 1): per-member Fastmail app password reaches all of that account's calendars; no cross-account ACL needed. Unified view stands; no shared-only fallback. See CAL-08-DECISION.md.
|
||
- D-14 (2026-06-04): Phase 1 Gate 2 (live Authelia/Pangolin) deferred. SSE-over-Pangolin smoke = hard gate before Phase 4; live AUTH smoke incl. iOS standalone-PWA folded into Phase 3. Phases 2–3 build behind a dev-auth bypass. Tracked in 01-HUMAN-UAT.md + docs/deployment.md.
|
||
- D-15 (2026-06-04): Validate real topology via local Newt connector + test subdomain through Pangolin (Mode A), not an Unraid deploy; Unraid reserved for go-live.
|
||
- CalDAV locked: Fastmail does not expose calendars over JMAP. CalDAV via tsdav is the only protocol. No reconsideration.
|
||
- Identity: Use oidc_iss + oidc_sub as stable composite key. Never email.
|
||
- Real-time transport: Prefer SSE over WebSocket (proxy-resilient through Pangolin). SSE pass-through STILL UNVERIFIED — gate before Phase 4 (D-14).
|
||
- Recurring events: Create + display only in v1 (CALDAV:expand on server side). Single-occurrence edit deferred to v1.x.
|
||
- [Phase ?]: D-Task5-DDL: drizzle-kit push unsafe on MariaDB 11 (mysql dialect misreads metadata, schedules truncate). Additive DDL hand-applied; adopt drizzle-kit generate+migrate before next schema change.
|
||
- [Phase ?]: D-02-RRULE: use ICAL.Recur.fromString + ICAL.Property for RRULE serialization — addPropertyWithValue produces char-split output for RECUR types
|
||
- [Phase ?]: resolveUserId helper uses any type to avoid Hono context generic complexity — internal helper only
|
||
- [Phase ?]: Write endpoints enqueue-only (calendarOutbox) with 202 optimistic-accept; no Fastmail call in routes (D-12/D-05)
|
||
- [Phase ?]: GET /writable-calendars is authoritative D-03 writable-set endpoint: WHERE userId=currentUser.id OR isShared=1
|
||
- [Phase ?]: D-03-07-icons: PWA icons generated via pure Node.js (zlib/Buffer) when ImageMagick/sharp unavailable; placeholder art; operator can replace in public/ without code changes
|
||
- [Phase ?]: D-03-07-layout: InstallPrompt desktop layout wraps CalendarContent in flex-column div so banner appears above calendar grid
|
||
- [Phase ?]: D-03-04-hoisting: vi.hoisted() required for vi.mock() factory variables when test file has static import of module under test (TDZ issue)
|
||
- [Phase ?]: D-03-04-where: Drizzle types disallow chained .where().where(); use and() in single .where() call
|
||
- [Phase ?]: D-01 calendar default: last-used URL from localStorage (eventForm.lastCalendarUrl), first writable calendar as fallback
|
||
- [Phase ?]: D-02 calendar picker: hidden when writableCalendars.length === 1, shown when >1 — authoritative from GET /api/events/writable-calendars
|
||
- [Phase ?]: T-03-15 XSS: EventForm renders all values as plain-text JSX children; no dangerouslySetInnerHTML in code
|
||
- [Phase ?]: Phase 4 Plan 1
|
||
- [Phase ?]: D-04 GET scoped: two-select + Set union (owner + list_shares); ListDeleteDialog props-driven to preserve calendarStore dialog; zValidator returns 400 not 422 per existing convention
|
||
- [Phase 04-04]: listItemsRouter separate from listsRouter, mounted at /api/list-items for PATCH/DELETE item routes per RESEARCH architecture diagram
|
||
- [Phase 04-04]: Uncheck rank recomputed to active-bottom (generateKeyBetween(lastActiveRank, null)) in same DB write (Open Question 2 resolved)
|
||
- [Phase 04-04]: Delete-wins no-rollback: deleteMutation has no onError handler; item removal from cache is final (D-09)
|
||
- [Phase ?]: LIST-04: SSE connection lives in ListDetail (not hoisted to Lists route); Phase 5 push will own session lifecycle
|
||
- [Phase 04-07]: D-04-07-collation: Drizzle 0.45.x has no first-class collation option on varchar; used customType to emit varchar(255) COLLATE utf8mb4_bin for list_items.rank — keeps schema-as-code + generate+migrate workflow
|
||
- [Phase 04-07]: D-04-07-guard: isShared owner-only guard placed after access check, before updateValues construction; mirrors DELETE handler idiom (if !access.isOwner → 403)
|
||
- [Phase ?]: VAPID config is env-injected at runtime via docker-compose.yml environment block; no key baked into image (Phase 5 D-transposability)
|
||
- [Phase ?]: dispatchPush uses sub.id (not a separate dbRowId argument) — 2-arg signature matches existing test
|
||
- [Phase ?]: coalesceListPush dispatch signature is (listId, actorId, count) — test scaffold canonical; richer payload deferred to Plan 05-05 caller
|
||
- [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
|
||
|
||
- Phase 6 added (2026-06-07): UX Polish — all-day visual distinction, event-form date/recurrence behavior, recurring-series edit, auth-flow smoothing. Candidate scope pulls from backlog 999.2/999.3/999.6/999.7/999.8/999.9.
|
||
|
||
### Pending Todos
|
||
|
||
- ~~**Fix `docs/deployment.md` local-dev command**~~ DONE 2026-06-10 (quick 260610-czd) — added a "Running locally (host-side, no Docker)" subsection with the correct two-terminal command (`set -a; source .env; set +a && DEV_AUTH_BYPASS=true DB_HOST=localhost pnpm --filter @familysync/api dev` + `pnpm --filter @familysync/pwa dev`). `--env-file` deliberately NOT baked into the dev script (root `.env` sets `DB_HOST=mariadb`; auto-load would break host-side dev).
|
||
- ~~**REQUIREMENTS.md traceability gap**~~ DONE 2026-06-10 (gsd-fast) — added the 6 deferred REQ-IDs to the Traceability table: CAL-09…CAL-12 (v1.x, Deferred), DISP-01/DISP-02 (v2, Deferred). v1 coverage stays 20/20; deferred IDs tracked separately.
|
||
- **DST spring-forward spot-check (Phase 2)** — recurring/DST is implemented and code-verified (VTIMEZONE before expansion + local display TZ), and operator approved general times; navigating to March 2026 to eyeball the spring-forward transition is a recommended future spot-check.
|
||
- **Gitea CI — regression on PR to main + Docker build/publish** — self-hosted Gitea runner exists but no CI yet. Add `.gitea/workflows/`: full regression (lint/typecheck/unit + API integration tests against a MariaDB service container + PWA build) gating PRs to `main`, plus build/publish the Docker image to the Gitea registry. Decide image matrix (API-only vs API+PWA — only `apps/api/Dockerfile` exists today) and tag policy. See pending todo `2026-06-10-gitea-ci-regression-and-docker-publish.md`.
|
||
|
||
### Blockers/Concerns
|
||
|
||
- ~~Phase 1: Personal-calendar CalDAV ACL~~ RESOLVED → CAL-08 GO (per-member app password; no cross-account ACL).
|
||
- ~~Phase 4 ENTRY GATE: Pangolin SSE pass-through (issue #1034) unverified~~ CLEARED 2026-06-08 — SSE smoke PASS over familysync-dev.bergerhouse.net (~6 min, 35 heartbeats, buffering off, no cut). Live sync unblocked. Caveat: untested for a max total connection-duration cap; residual risk covered by Phase 4 design (D-10/D-11/D-12). See quick 260607-u8o + 03-GATE2-RESULTS.md Part C.
|
||
- Phase 3: iOS standalone-PWA + Authelia login is load-bearing for the wife and is the first real external auth test (carried Gate 2 item, D-14). Also: iOS install guide is load-bearing — she gets no push notifications if she does not install the PWA.
|
||
- Phase 2/3 dev: build behind a documented dev-auth bypass until Gate 2 deploy (D-14).
|
||
- Phase 5: iOS push subscriptions silently revoked after 3 silent pushes. Subscription health-check and event.waitUntil() are mandatory from day one.
|
||
|
||
### Quick Tasks Completed
|
||
|
||
| # | Description | Date | Commit | Status | Directory |
|
||
|---|-------------|------|--------|--------|-----------|
|
||
| 260606-tv8 | Fix missing sign-in redirect in the PWA (Phase 03 auth-entry gap from Gate 2): guarded /api/login → / + full-page redirect on unauthenticated fetchMe | 2026-06-07 | 7c6531f | | [260606-tv8-fix-missing-sign-in-redirect-in-the-pwa-](./quick/260606-tv8-fix-missing-sign-in-redirect-in-the-pwa-/) |
|
||
| 260607-l6l | Batch-fix Phase 03 write-path bugs: events.ts edit/delete missing calendars innerJoin (503, BLOCKING) + handler-coupled regression test; shared deriveDisplayName helper (me.ts + resolveUserId, corrects blank rows); GET /api/events userId/isShared ownership filter | 2026-06-07 | 2870413 | | [260607-l6l-fix-phase-03-write-path-correctness-bugs](./quick/260607-l6l-fix-phase-03-write-path-correctness-bugs/) |
|
||
| 260607-u8o | Record SSE-over-Pangolin smoke test PASS (Phase 4 entry gate, D-14 / issue #1034) — updated 01-HUMAN-UAT item 4 + 03-GATE2-RESULTS Part C to PASS with live evidence | 2026-06-08 | 26655cf | | [260607-u8o-record-sse-over-pangolin-smoke-test-pass](./quick/260607-u8o-record-sse-over-pangolin-smoke-test-pass/) |
|
||
| 260610-cr8 | Adopt drizzle generate+migrate workflow, retire db:push on MariaDB — removed db:push script + repointed deployment.md to migrate with anti-push warning; dry-verified no destructive diff | 2026-06-10 | 1a95d81 | Verified | [260610-cr8-adopt-drizzle-generate-migrate-workflow-](./quick/260610-cr8-adopt-drizzle-generate-migrate-workflow-/) |
|
||
| 260610-czd | Fix docs/deployment.md local-dev command — added "Running locally (host-side, no Docker)" subsection with correct env-sourced two-terminal run command (Phase 2 UAT gap) | 2026-06-10 | 39e2ee0 | | [260610-czd-fix-docs-deployment-md-local-dev-command](./quick/260610-czd-fix-docs-deployment-md-local-dev-command/) |
|
||
| 260610-hbu | Phase 5 reminder scheduler resilience (UAT Test 1 gap) — catch-up scan `(now, now+16min]` + per-uid exactly-once dedup so a missed/late cron tick no longer drops a reminder; lead-accurate body; also fixes pre-existing cross-tick double-fire. 10/10 reminder tests pass | 2026-06-10 | 19d92c6 | Verified | [260610-hbu-make-phase-5-reminder-scheduler-resilien](./quick/260610-hbu-make-phase-5-reminder-scheduler-resilien/) |
|
||
|
||
## Deferred Items
|
||
|
||
| Category | Item | Status | Deferred At |
|
||
|----------|------|--------|-------------|
|
||
| Calendar | Single-occurrence recurring edit (RECURRENCE-ID) | v1.x | Roadmap |
|
||
| Calendar | "This and following" recurring edit | v1.x | Roadmap |
|
||
| Calendar | Apple Calendar native subscribe URL docs | v1.x | Roadmap |
|
||
| Calendar | Secondary timezone display toggle | v1.x | Roadmap |
|
||
| Display | Wall-display / kiosk dashboard | v2 | PROJECT.md |
|
||
| ~~Calendar~~ | ~~Mark shared-family calendar `is_shared=1`~~ **RESOLVED 2026-06-10** — operator created the "FamilySync" calendar on the primary Fastmail account; poller synced it as calendars.id=10 (user 2); ran `UPDATE calendars SET is_shared=1 WHERE id=10`. Shared color lane now populated; Phase 5 reminders now fire on its events. Poller upsert does not touch is_shared, so the flag persists. | ~~Phase 2 (deferred, D-16)~~ DONE | 2026-06-05 → 2026-06-10 |
|
||
|
||
## Session Continuity
|
||
|
||
Last session: 2026-06-10T15:20:02.349Z
|
||
Stopped at: Completed 06-03: hasRrule server-side exposure
|
||
Resume file: None
|