Commit Graph
467 Commits
Author SHA1 Message Date
Lucas Berger f0af43c4e6 test(02-04): CalendarShell CAL-03 render smoke — Temporal hydration + Pitfall 6 guard
- CalendarShell.test.tsx: 6 tests covering render-without-throw, ScheduleXCalendar mount,
  hydrateEvents called with both timed + all-day occurrences, Temporal.PlainDate guard
  (Pitfall 4/all-day date shift), Temporal.ZonedDateTime for timed (Pitfall 4)
- test-setup.ts: window.matchMedia polyfill for jsdom (calendarStore initialises at module load)
- vitest.config.ts: adds setupFiles pointing to test-setup.ts
2026-06-05 10:42:30 -04:00
Lucas Berger b79f649ac5 feat(02-04): mount Schedule-X CalendarShell wired to TanStack Query + Zustand + hydrateEvents
- CalendarShell.tsx: all four views (day/week/month-grid/month-agenda), eventsService + eventModal plugins
- calendarId routing: 'shared' | String(ownerUserId) via hydrateEvents, matching buildCalendarConfig keys
- SX_FIRST_DAY_OF_WEEK=7 (Sunday, Temporal convention); initial range from Zustand default (A4 guard)
- onRangeUpdate updates Zustand range, triggering TanStack Query refetch on navigation
- App.tsx: replaces EventProof landing with CalendarShell; tokens only (no hardcoded hex/px)
2026-06-05 10:40:07 -04:00
Lucas Berger dabe2ccd57 docs(phase-02): record D-16 (shared calendar on primary FM account) + defer is_shared marking 2026-06-05 10:34:22 -04:00
Lucas Berger bfddcb1da2 docs(phase-02): update tracking after wave 2 2026-06-05 10:33:53 -04:00
Lucas Berger 010ef78230 docs(02-02): complete windowed events + expansion plan
- SUMMARY.md created with task outcomes, deviations, self-check
- Task 3 deferral documented (shared calendar marking pending operator setup)
2026-06-05 10:32:30 -04:00
Lucas Berger 9ee26c07a7 feat(02-02): evolve /api/events to windowed endpoint with color/owner join
- zValidator enforces YYYY-MM-DD regex on start/end (T-02b-01)
- 90-day window cap prevents DoS (T-02b-02)
- innerJoin calendarEvents→calendars→users for color + isShared + ownerUserId
- SQL pre-filter includes hasRrule=true rows regardless of dtstartUtc range
- expandOccurrences() called per row; shared calendar uses #F25C7A rose color
- events.test.ts: added @hono/oidc-auth mock; 4/4 assertions green
2026-06-05 10:30:36 -04:00
Lucas Berger 6736194a4a feat(02-02): add expandOccurrences() with VTIMEZONE + allDay split + EXDATE
- ICAL.TimezoneService.register() runs before RecurExpansion (DST correctness)
- All-day events serialized as YYYY-MM-DD strings (no time shift)
- EXDATE exclusions handled internally by ICAL.RecurExpansion
- Malformed rawVevent returns [] without throwing
- expand.test.ts DST, all-day, EXDATE assertions green (3/3)
2026-06-05 10:30:22 -04:00
Lucas Berger 5435df6bbe docs(02-03): complete plan 03 summary — token layer, calendar config, hydration, store 2026-06-05 09:49:39 -04:00
Lucas Berger f377d7c3f8 feat(02-03): hydrateEvents + calendarStore + windowed fetchEvents; RED stubs green
- Create hydrateEvents.ts: Temporal.PlainDate for allDay, ZonedDateTime for timed
  calendarId routes via isShared ? 'shared' : String(ownerUserId) — NOT String(calendarId)
  _familySync carries uid/color/isShared through to popover
- Update hydrateEvents.test.ts: add temporal-polyfill/global import; all 4 RED stubs now GREEN
- Create calendarStore.ts: Zustand store with selectedView (localStorage per breakpoint group),
  selectedDate, openEventId, calendarRange; D-05 view defaults; calendarRange ± buffer for
  initial TanStack Query key without depending on onRangeUpdate firing on mount
- Update client.ts: add CalendarOccurrence/OccurrencesResponse, windowed fetchEvents(start,end)
  with credentials:include; keep legacy CalendarEvent/EventsResponse + fetchEventsLegacy
  as deprecated for EventProof.tsx (removed in Plan 05)
- Update EventProof.tsx: switch to fetchEventsLegacy to keep build clean until Plan 05
- tsc --noEmit clean; all 18 PWA tests pass
2026-06-05 09:47:07 -04:00
Lucas Berger 43554f491b feat(02-03): colorUtils + calendarConfig; turn RED calendarConfig stubs green
- Create colorUtils.ts: hexToContainer (15% alpha over white), hexToOnContainer
  (darken 40%), deriveScheduleXColors() returning { main, container, onContainer }
- Create colorUtils.test.ts: hex blend math assertions for #4A90D9 and #F25C7A
- Create calendarConfig.ts: WEEK_START_DAY=0, SX_FIRST_DAY_OF_WEEK=7 (0→7 translation)
  buildCalendarConfig() keyed by String(userId) + 'shared'; returns { firstDayOfWeek, calendars }
- calendarConfig.test.ts (Plan 01 RED stubs) now GREEN: all 4 assertions pass
2026-06-05 09:44:19 -04:00
Lucas Berger 0911a2330a feat(02-03): install Schedule-X stack + CSS token layer + main.tsx imports
- Install @schedule-x/{calendar,react,theme-default,event-modal,events-service}@4.x
- Install temporal-polyfill@0.3.2 and lucide-react@1.17.0
- Create src/styles/tokens.css: all color/spacing/typography/breakpoint tokens
  plus --sx-color-* Schedule-X overrides mapped to project tokens
- Create src/styles/tokens.ts: TypeScript mirror of token values for inline styles
- Create src/styles/index.css: imports tokens.css + minimal global reset
- Update main.tsx: Temporal polyfill first, then SX theme CSS, then styles/index.css
2026-06-05 09:42:34 -04:00
Lucas Berger 6dc5d77f60 docs(phase-02): update tracking after wave 1 2026-06-05 09:38:23 -04:00
Lucas Berger 2d75b2b73b docs(02-01): complete plan 01 — schema columns, test harness, dev-auth bypass summary 2026-06-05 09:35:53 -04:00
Lucas Berger 8bd44b33c7 feat(02-01): dev-auth bypass middleware with production hard guard
- Create apps/api/src/auth/devBypass.ts: devAuthBypass() middleware with
  NODE_ENV=production hard guard as first conditional (T-02-01 mitigation)
- Exports DEV_USER const (id:1, color:COLOR_PALETTE[0]) for test reference
- Mount devAuthBypass() before oidcAuthMiddleware on /api/* in index.ts
- Add devBypass.test.ts: all three behavioral cases pass (production guard,
  unset-flag passthrough, active-injection)
- Add DEV_AUTH_BYPASS to .env.example with production warning comment
- Extend docs/deployment.md with dev-auth bypass section and production prohibition
2026-06-05 09:32:00 -04:00
Lucas Berger 75252eb08c feat(02-01): schema columns, PWA vitest harness, ICS fixtures, RED test stubs
- Add calendarEvents.hasRrule boolean + idx_calendar_events_has_rrule index (Phase 2 pre-filter)
- Add calendars.isShared boolean for shared-family calendar identification
- Create apps/pwa/vitest.config.ts with jsdom environment
- Add vitest, @testing-library/react, jsdom, @testing-library/jest-dom to PWA devDependencies
- Add "test": "vitest run" script to apps/pwa/package.json
- Create three ICS fixtures: weekly-dst.ics (DST spanning), allday-birthday.ics, exdate-series.ics
- Create RED test stub expand.test.ts with concrete DST wall-clock assertions (10:00 local both sides of March 2026 boundary)
- Create RED test stub events.test.ts with 400 validation and color/isShared field contracts
- Create RED test stub hydrateEvents.test.ts with Temporal type and calendarId routing contracts (shared→"shared", personal→String(ownerUserId))
- Create RED test stub calendarConfig.test.ts with firstDayOfWeek 0→7 translation contract
2026-06-05 09:29:45 -04:00
Lucas Berger 62ebb1f9d4 docs(phase-02): begin execution — consume handoff, advance state 2026-06-05 09:24:49 -04:00
Lucas Berger 81d5c61aa5 wip: calendar-display paused — planned, not executed (0/5 plans) 2026-06-04 15:29:39 -04:00
Lucas Berger 8bd52c6923 docs: add backlog item 999.1 — treat Fastmail as a calendar provider, support adding more 2026-06-04 15:12:20 -04:00
Lucas Berger fc4cc2ccf8 docs(02): create phase plan 2026-06-04 15:09:55 -04:00
Lucas Berger b842955612 docs(02): create calendar-display phase plan (5 plans, 4 waves) 2026-06-04 14:56:57 -04:00
Lucas Berger 5e14413024 docs(02): add pattern map 2026-06-04 14:47:28 -04:00
Lucas Berger a707f8d9ca docs(02): add research and validation strategy 2026-06-04 14:44:14 -04:00
Lucas Berger e9fcd7a372 docs(02): research calendar display phase 2026-06-04 14:43:23 -04:00
Lucas Berger 5b98fc097d docs(state): record phase 2 UI-SPEC session 2026-06-04 14:24:20 -04:00
Lucas Berger f1604b5057 docs(02): UI design contract 2026-06-04 14:24:20 -04:00
Lucas Berger 79a18b9ea2 docs(02): add UI design contract for calendar display phase
Specifies the token-layer architecture, Schedule-X as the rendering
library (all four views, CSS-var theming, per-calendar color), per-member
event colors, shared-family rose (#F25C7A), spacing/typography/breakpoints,
component inventory, and copywriting contract for Phase 2.
2026-06-04 14:21:57 -04:00
Lucas Berger 7d65ef0fa9 docs(state): record phase 2 context session 2026-06-04 14:14:08 -04:00
Lucas Berger dbec9b510c docs(02): capture phase context 2026-06-04 14:14:07 -04:00
Lucas Berger 6e74b3fc05 docs: capture D-14/D-15 (Gate 2 deferral + local-Newt test rig) + deployment runbook
- docs/deployment.md: operator runbook — Mode A (local Newt test rig) vs Mode B (Unraid
  prod), Authelia client block, env reference, Pangolin SSE idle-timeout note, Gate 2 checklist.
- ROADMAP: Phase 1 verification-status note (Gate 1 done, Gate 2 deferred); Phase 3 gains live
  AUTH + iOS standalone-PWA criterion; Phase 4 gains hard SSE-smoke entry gate (#1034).
- PROJECT.md: D-14 (Gate 2 deferral split) + D-15 (local Newt rig); CAL-08 marked validated.
- STATE: decisions/blockers updated to reflect deferral and dev-auth-bypass approach.
2026-06-04 11:55:31 -04:00
Lucas Berger 5bed2228b7 test(01): verification (human_needed, 11/13) + persist Gate 2 human-UAT
Code verified against codebase, 24 tests pass, CAL-01 + CAL-08 proven live.
AUTH-01/02/03 live login + SSE-over-Pangolin smoke test require operator infra —
tracked as 01-HUMAN-UAT.md (status: partial).
2026-06-04 11:44:52 -04:00
Lucas Berger fe8d5c8e4b docs(phase-01): mark 01-04 code complete after wave 3 2026-06-04 11:40:00 -04:00
Lucas Berger 0b074cd568 docs(01-04): record CAL-08 GO decision — broker read path proven live
Live spike against me@lucasberger.ca: one app password enumerated both account
calendars (PROPFIND), and a REPORT fetched + cached 503 real events into MariaDB
with correct D-13 timed/all-day handling (CAL-01). Per-member app-password model
(D-09) validated; no cross-account ACL needed; no fallback required. Gate 2 (live
Authelia/Pangolin login + SSE smoke test) remains pending operator action.
2026-06-04 11:39:44 -04:00
Lucas Berger d6f5e7d7a5 fix: ignore all .env.* secret variants, not just bare .env
Bare `.env` only matched the exact filename, leaving .env.spike / .env.local
and other secret variants untracked-but-committable. Ignore .env.* and re-include
.env.example so the template stays tracked.
2026-06-04 11:35:32 -04:00
Lucas Berger 22a0f05170 docs(01-04): complete integration+gate plan summary; add CAL-08 decision template
- 01-04-SUMMARY.md: code-complete; Task 1 committed (48f90ce); Tasks 2+3 are live-gate checkpoints with exact human-action steps
- CAL-08-DECISION.md: template for human to fill in after running apps/api/src/broker/spike.ts against real Fastmail credentials
2026-06-04 11:18:04 -04:00
Lucas Berger 48f90ceca9 feat(01-04): wire broker + routes into bootstrap, add SSE endpoint, EventProof
- Mount /api/events, /api/sse in index.ts behind oidcAuthMiddleware; /callback + /health before guard
- Call startBrokerPoller() on boot (5-min ctag-poll background schedule)
- Add sseRouter with GET /heartbeat (streamSSE, 10s interval) for Pangolin SSE smoke test (D-08, T-04-01)
- Add CAL-08 spike script (broker/spike.ts): createFastmailClient → fetchCalendars → print calendar URLs
- Add fetchEvents() to pwa/api/client.ts with typed CalendarEvent/EventsResponse shapes
- Add EventProof.tsx: React Query ['events'], renders first event title+date or empty-state (CAL-01 broker proof)
- Update App.tsx to render MemberBadge + EventProof on landing page
- Add ical.js@2.2.1 to PWA dependencies for VEVENT summary parsing in EventProof
- All 24 API unit tests green; tsc --noEmit clean in both apps/api and apps/pwa
2026-06-04 11:16:10 -04:00
Lucas Berger d2d8333bf8 docs(phase-01): update tracking after wave 2 (01-02 OIDC, 01-03 broker) 2026-06-04 11:10:44 -04:00
Lucas Berger 191cea70cc docs(01-03): complete CalDAV broker slice plan summary
- covers all 3 tasks: AES-256-GCM crypto, broker client/sync/events, ctag poller
- documents D-13 dtstart split decision and ctag null-defence pattern
- notes live Fastmail integration + personal-calendar ACL deferred to plan 04
2026-06-04 11:10:02 -04:00
Lucas Berger 23b8e5326d feat(01-03): implement ctag poller with node-cron 5-min schedule
- startBrokerPoller schedules runPoll via node-cron */5 * * * *
- runPoll loads all member_credentials, decrypts each app password
- ctag/syncToken comparison skips syncCalendar when ctag unchanged
- errors per-credential caught and logged without crashing the poller
- runPoll exported for unit-testing with vi.mock injected dependencies
2026-06-04 11:08:46 -04:00
Lucas Berger 1b3ea4e99b test(01-03): add failing tests for broker poller ctag change detection
- 5 tests: skip on unchanged ctag, sync on changed ctag, first-sync (null ctag), decrypt failure handled, all credentials processed
- Mocks db.select, createFastmailClient, syncCalendar, decryptPassword at module level
- RED gate: all fail (src/broker/poller.ts does not exist yet)
2026-06-04 10:34:12 -04:00
Lucas Berger dd02207318 feat(01-03): implement broker client, syncCalendar, and events route (CAL-01)
- client.ts: createFastmailClient(email, appPassword) → tsdav DAVClient via Basic auth
- sync.ts: syncCalendar upserts calendar row, selects ID, fetches+parses VEVENTs with ical.js
  - all-day DATE → dtstartDate (Date obj at T00:00:00Z), dtstartUtc=null, allDay=true
  - timed → dtstartUtc (JS Date), dtstartDate=null, allDay=false (D-13/Pitfall #3)
  - onDuplicateKeyUpdate on calendarId+uid composite key (idempotent)
- routes/events.ts: GET /api/events reads cache only — no tsdav import (T-03-02)
- tsc --noEmit clean; all 6 sync tests pass
2026-06-04 10:33:28 -04:00
Lucas Berger 90b99296e8 test(01-03): refine sync RED tests with richer db mock (select + insert)
- Use separate mock functions for insert/select chain to enable per-test assertions
- Reset mock implementations in beforeEach after vi.clearAllMocks
- Mock returns {id:42} from select (calendar ID for event upsert)
2026-06-04 10:30:53 -04:00
Lucas Berger ae21541e97 test(01-03): add failing tests for syncCalendar (D-13 all-day DATE handling)
- 6 tests: all-day DATE vs timed TIMESTAMP, allDay flag, upsert on dup UID, rawVevent blob, ctag update
- Mocks db singleton at module level with vi.mock hoisting pattern
- Uses SAMPLE_VEVENT_TIMED/ALLDAY fixtures from tests/helpers/db.ts
- RED gate: all fail (src/broker/sync.ts does not exist yet)
2026-06-04 10:29:29 -04:00
Lucas Berger d6d91201b1 feat(01-03): implement AES-256-GCM app-password encryption (T-03-01)
- encryptPassword: randomBytes(12) IV, aes-256-gcm, returns JSON {iv,authTag,ciphertext}
- decryptPassword: verifies GCM auth tag; throws on tamper
- Key from APP_PASSWORD_ENCRYPTION_KEY env (64-char hex); validated on each call
- No logging of plaintext or key
2026-06-04 10:27:40 -04:00
Lucas Berger 04d7c23e94 test(01-03): add failing tests for AES-256-GCM crypto helpers
- 5 tests covering roundtrip, IV uniqueness, tamper detection, payload shape
- Dynamic import with env key set in beforeAll for module-level KEY eval
- RED gate: all fail (src/broker/crypto.ts does not exist yet)
2026-06-04 10:26:59 -04:00
Lucas Berger 3bfffd60ae docs(01-02): complete OIDC auth slice plan summary 2026-06-04 10:24:18 -04:00
Lucas Berger 668ed9be0d feat(01-02): wire OIDC middleware, /api/me route, and authenticated PWA shell
- src/auth/middleware.ts: re-exports oidcAuthMiddleware, processOAuthCallback,
  getAuth from @hono/oidc-auth; documents required env vars and AUTH-02
  refresh-token rotation (no iframe, D-12)
- src/routes/me.ts: GET / calls getAuth → upsertUser(iss, sub, email) →
  returns { user: { id, displayName, color } }; identity keyed on iss+sub
- src/index.ts: /callback registered before oidcAuthMiddleware; /api/*
  guarded; /health remains unauthenticated; /api/me mounted
- apps/pwa/src/api/client.ts: typed fetchMe() with credentials: 'include'
- apps/pwa/src/App.tsx: useQuery(['me'], fetchMe); renders member name and
  color swatch; retains /health stack indicator from Plan 01
- tsc --noEmit: clean; all tests pass
2026-06-04 10:22:46 -04:00
Lucas Berger baabfce9e2 feat(01-02): implement upsertUser with stable color assignment (AUTH-03)
- Export COLOR_PALETTE (6 accessible hex hues, round-robin assignment)
- upsertUser(oidcIss, oidcSub, displayName?) keyed on iss+sub never email
- First login: COUNT existing users → assign COLOR_PALETTE[count % len]
- Re-upsert: returns existing row unchanged (idempotent, no duplicate insert)
- Uses $returningId() + re-select pattern (mysql2 no RETURNING clause)
- All 6 tests pass (GREEN)
2026-06-04 10:21:04 -04:00
Lucas Berger 61c258c021 test(01-02): add failing tests for upsertUser color assignment + identity
- Replace it.todo stubs with real failing tests (RED gate)
- Tests cover palette[0] first user, palette[1] second user, idempotent
  re-upsert, iss+sub identity key, full row shape
- Tests fail: src/auth/user.ts does not exist yet
2026-06-04 10:20:27 -04:00
Lucas Berger ee4c71fa46 docs(01-01): clear Task 3 checkpoint, mark plan complete
Stack verified live (/health green, 4 tables pushed). Record Docker deviation in
SUMMARY, mark 01-01 complete in ROADMAP, clean up paused-setup handoff files.
2026-06-04 10:17:21 -04:00
Lucas Berger 00efbab107 fix(01-01): build Docker image from repo-root pnpm workspace context
The walking-skeleton Dockerfile built from a ./apps/api context and could not
work in a pnpm workspace: the lockfile lives at the repo root, pnpm 11 refused
esbuild's build script without the root pnpm-workspace.yaml, the dev stage never
compiled src->dist, and the production stage had invalid COPY syntax referencing
a path outside its context.

Switch to the correct monorepo pattern: build from the repo-root context, copy
the workspace manifest + lockfile + both package.jsons, and install with
--frozen-lockfile --filter @familysync/api...  Reorder stages so production is
default; dev reuses builder output. Fix the dev volume mount path.

Surfaced while clearing the Task 3 checkpoint (stack bring-up): drizzle-kit push
applied the 4 tables and /health returned {ok:true,db:up} end-to-end.
2026-06-04 10:17:06 -04:00