Commit Graph
978 Commits
Author SHA1 Message Date
Lucas Berger bef4a83fe0 fix(02): CSS-safe occurrence ids + error boundary to surface render errors
Schedule-X rejects ids containing ':' '[' ']' (the old ${uid}::${iso} form) — mint ev-<uid>-<epochMs> instead. Add an ErrorBoundary so a render throw shows the error instead of a blank page.
2026-06-05 14:30:32 -04:00
Lucas Berger 93c368402c fix(02): update expand tests to assert IANA-annotated format + add cross-contract test
- Assert timed start/end strings include '[America/New_York]' bracket (not offset-only)
- Assert DST boundary offsets: -05:00[America/New_York] pre-transition, -04:00[America/New_York] post
- Add cross-contract regression test: feeds expandOccurrences output directly into
  Temporal.ZonedDateTime.from() to prove the expand→hydrate contract holds end-to-end
- Import 'temporal-polyfill/global' at top of test file for the Temporal global
- Rename describe block from 'RED stubs (Wave 0)' to reflect GREEN state
2026-06-05 14:04:00 -04:00
Lucas Berger 35f725d450 fix(02): emit IANA-annotated timed strings from serializeTime
Temporal.ZonedDateTime.from() rejects offset-only ISO strings such as
'2026-06-18T08:00:00-04:00'; it requires an IANA bracket, e.g.
'2026-06-18T08:00:00-04:00[America/New_York]'. serializeTime() was
emitting offset-only for named zones and bare 'Z' for UTC — both
unparseable by the frontend, blanking the calendar view.

Changes:
- Named IANA zone: emit '...±HH:MM[tzid]' using t.zone.tzid
- UTC zone: strip trailing 'Z' from toString(), emit '+00:00[UTC]'
- Floating zone (no registered VTIMEZONE): fall back to '+00:00[UTC]'
- Update CalendarOccurrence docstrings to reflect the IANA-annotated contract
- Add temporal-polyfill@0.3.2 as dev dep in api for cross-contract test
2026-06-05 14:02:36 -04:00
Lucas Berger df5d36308a fix(02): add regression tests for /api/me under dev-auth bypass
- Asserts GET /api/me returns 200 with DEV_USER (id=1, color=#4A90D9)
  when DEV_AUTH_BYPASS=true and NODE_ENV!=production
- Asserts oidcAuthMiddleware is NOT wired when bypass is active
- Asserts oidcAuthMiddleware IS wired when bypass is absent
- Asserts 401 from getAuth(null) fallback path with no OIDC session
2026-06-05 13:48:05 -04:00
Lucas Berger 4b34b16f02 fix(02): dev-auth bypass no longer blocked by oidcAuthMiddleware
- index.ts: compute devBypassActive at startup; skip app.use(oidcAuthMiddleware)
  entirely when active so the OIDC guard never runs in local dev
- routes/me.ts: read c.get('user') first; return dev identity directly when
  devAuthBypass injected it, bypassing getAuth() and the DB upsert
- auth/devBypass.ts: add ContextVariableMap augmentation for 'user' key;
  correct stale comment that claimed getAuth/401 path was still active
2026-06-05 13:48:00 -04:00
Lucas Berger 11595e7924 docs(02-05): complete plan 05 autonomous tasks — SUMMARY.md 2026-06-05 10:59:31 -04:00
Lucas Berger 216ddcedf4 feat(02-05): ColorLegend, AppNav, ViewToolbar, SkeletonCalendar, EmptyState; retire EventProof
- ColorLegend: per-member color swatches (12px circle, label) + always-visible Family row
- AppNav: phone 48px top bar (avatar with aria-label/title) + tablet/desktop 240px sidebar with ColorLegend
- ViewToolbar: Today/prev/next + Day/Week/Month/Agenda view switcher; 44px min-height; active state uses surface tint not accent
- SkeletonCalendar: shimmer month (6x7 grid) and agenda (4 date-group blocks) variants; aria-busy=true
- EmptyState: CalendarDays icon + 'Nothing here' heading + body copy per UI-SPEC
- CalendarShell: full phone/desktop layout with AppNav + ViewToolbar + ColorLegend chrome
- CalendarShell: state branches — loading→SkeletonCalendar, empty→EmptyState, error→'Couldn't load events' + Retry button (refetchQueries)
- EventProof.tsx deleted; legacy types removed from client.ts
- CalendarShell.test.tsx: updated to waitFor ScheduleXCalendar after data loads
- All 36 tests pass, tsc clean, vite build clean (490kB)
2026-06-05 10:58:06 -04:00
Lucas Berger 3eebfbff42 feat(02-05): EventDetailPopover read-only popover, XSS-safe, wired into CalendarShell
- EventDetailPopover: reads openEventId from Zustand, resolves occurrence from TanStack Query cache
- Dual-mode: standalone (Zustand-driven) + customComponents.eventModal (Schedule-X)
- Plain-text JSX children for all event fields (T-02e-01 XSS guard)
- Focus trap, Escape to close, backdrop-click to close, aria-label=Close (44px target)
- Phone: bottom sheet layout; tablet/desktop: centered popover (max-width 360px)
- Phase-3 footer action area reserved with comment
- CalendarShell: passes customComponents.eventModal=EventDetailPopover to ScheduleXCalendar
- test-setup.ts: import @testing-library/jest-dom for toHaveTextContent matcher
- All 36 tests pass, tsc clean
2026-06-05 10:51:57 -04:00
Lucas Berger 433fb9f900 test(02-05): add failing tests for EventDetailPopover (TDD RED)
- renders title/location/description/calendar-name as plain text
- Escape key and backdrop click close the popover via setOpenEventId(null)
- close button aria-label=Close assertion
- XSS guard: HTML-looking title and description render as escaped text not DOM elements
- renders null when openEventId is null
2026-06-05 10:47:41 -04:00
Lucas Berger 00a4e66a8d docs(phase-02): update tracking after wave 3 2026-06-05 10:44:57 -04:00
Lucas Berger 7b32bc1456 docs(02-04): complete CalendarShell plan — SUMMARY.md 2026-06-05 10:44:04 -04:00
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