style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
---
|
||||
|
||||
<user_constraints>
|
||||
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
### Locked Decisions
|
||||
@@ -37,18 +38,20 @@
|
||||
- Per-member show/hide filter (add when membership > 2).
|
||||
- Secondary timezone display toggle (v1.x).
|
||||
- Single-occurrence / "this and following" recurring edits (v1.x).
|
||||
</user_constraints>
|
||||
</user_constraints>
|
||||
|
||||
---
|
||||
|
||||
<phase_requirements>
|
||||
|
||||
## Phase Requirements
|
||||
|
||||
| ID | Description | Research Support |
|
||||
|----|-------------|------------------|
|
||||
| CAL-02 | User sees a unified, color-coded calendar that aggregates every accessible calendar into one view | §Backend: /api/events evolution; §Frontend: Schedule-X calendars config with per-calendar lightColors |
|
||||
| CAL-03 | User can switch between week, month, day, and agenda/list views | §Schedule-X Views; all four views confirmed in @schedule-x/calendar v4.6.0 |
|
||||
| CAL-07 | User can create a recurring event and see all its occurrences expanded correctly (display portion only — creation is Phase 3) | §Recurrence expansion pipeline; §DST correctness; §All-day event handling |
|
||||
| ID | Description | Research Support |
|
||||
| ------ | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| CAL-02 | User sees a unified, color-coded calendar that aggregates every accessible calendar into one view | §Backend: /api/events evolution; §Frontend: Schedule-X calendars config with per-calendar lightColors |
|
||||
| CAL-03 | User can switch between week, month, day, and agenda/list views | §Schedule-X Views; all four views confirmed in @schedule-x/calendar v4.6.0 |
|
||||
| CAL-07 | User can create a recurring event and see all its occurrences expanded correctly (display portion only — creation is Phase 3) | §Recurrence expansion pipeline; §DST correctness; §All-day event handling |
|
||||
|
||||
</phase_requirements>
|
||||
|
||||
---
|
||||
@@ -67,18 +70,18 @@ All-day event correctness is already partially solved by the D-13 schema split (
|
||||
|
||||
## Architectural Responsibility Map
|
||||
|
||||
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||
|------------|-------------|----------------|-----------|
|
||||
| Event storage and polling | API / Backend | — | Phase 1 broker owns all Fastmail I/O; routes read MariaDB cache |
|
||||
| Recurrence expansion | API / Backend | — | Server expands to concrete occurrences for the requested window; client renders, never expands (D-09) |
|
||||
| All-day / timed discrimination | API / Backend | — | D-13 schema split already done in Phase 1; route must preserve and expose the distinction |
|
||||
| Color and ownership join | API / Backend | — | `calendars.userId → users.color` join lives closest to the data; frontend just reads the color hex |
|
||||
| Temporal object construction | Frontend (PWA) | — | Server sends plain strings; client converts to `Temporal.ZonedDateTime` / `Temporal.PlainDate` before Schedule-X |
|
||||
| Calendar rendering (views) | Frontend (PWA) | — | Schedule-X renders day/week/month/agenda in the browser |
|
||||
| Token layer / theming | Frontend (PWA) | — | CSS custom properties + TS token object; Schedule-X `--sx-color-*` vars overridden |
|
||||
| View state, selected date, open popover | Frontend (PWA) — Zustand | — | UI-only state; never server data |
|
||||
| Event list caching and re-fetch | Frontend (PWA) — TanStack Query | — | Cache key = `['events', start, end]`; invalidated on range change |
|
||||
| Dev-auth bypass | API / Backend | — | Env-flagged middleware injecting fixed user; never active in production |
|
||||
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||
| --------------------------------------- | ------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| Event storage and polling | API / Backend | — | Phase 1 broker owns all Fastmail I/O; routes read MariaDB cache |
|
||||
| Recurrence expansion | API / Backend | — | Server expands to concrete occurrences for the requested window; client renders, never expands (D-09) |
|
||||
| All-day / timed discrimination | API / Backend | — | D-13 schema split already done in Phase 1; route must preserve and expose the distinction |
|
||||
| Color and ownership join | API / Backend | — | `calendars.userId → users.color` join lives closest to the data; frontend just reads the color hex |
|
||||
| Temporal object construction | Frontend (PWA) | — | Server sends plain strings; client converts to `Temporal.ZonedDateTime` / `Temporal.PlainDate` before Schedule-X |
|
||||
| Calendar rendering (views) | Frontend (PWA) | — | Schedule-X renders day/week/month/agenda in the browser |
|
||||
| Token layer / theming | Frontend (PWA) | — | CSS custom properties + TS token object; Schedule-X `--sx-color-*` vars overridden |
|
||||
| View state, selected date, open popover | Frontend (PWA) — Zustand | — | UI-only state; never server data |
|
||||
| Event list caching and re-fetch | Frontend (PWA) — TanStack Query | — | Cache key = `['events', start, end]`; invalidated on range change |
|
||||
| Dev-auth bypass | API / Backend | — | Env-flagged middleware injecting fixed user; never active in production |
|
||||
|
||||
---
|
||||
|
||||
@@ -86,17 +89,17 @@ All-day event correctness is already partially solved by the D-13 schema split (
|
||||
|
||||
### Core (all versions verified against npm registry 2026-06-04)
|
||||
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| `@schedule-x/calendar` | 4.6.0 | Calendar engine (views, Temporal-based event model) | Selected in UI-SPEC; active maintenance; last published 2026-05-12 |
|
||||
| `@schedule-x/react` | 4.1.0 | React adapter (`useCalendarApp`, `ScheduleXCalendar`) | Official React adapter; peer-requires `@schedule-x/calendar ^3.1.0 \|\| ^4.0.0`; 4.6.0 satisfies this |
|
||||
| `@schedule-x/theme-default` | 4.6.0 | Default CSS layout; overridden by project tokens | Required for Schedule-X internal layout engine; all colors are token-overridden |
|
||||
| `@schedule-x/event-modal` | 4.6.0 | `createEventModalPlugin()` for custom `eventModal` component | Required to replace default modal with `EventDetailPopover` |
|
||||
| `@schedule-x/events-service` | 4.6.0 | `createEventsServicePlugin()` for dynamic event updates | Required to update events after TanStack Query fetches new window |
|
||||
| `temporal-polyfill` | 0.3.2 | `Temporal` global polyfill for browsers without native support | `@schedule-x/calendar` peer-requires `temporal-polyfill@0.3.0`; 0.3.2 satisfies |
|
||||
| `lucide-react` | 1.17.0 | Icon library (CalendarDays, X, MapPin, ChevronLeft/Right) | Specified in UI-SPEC; tree-shakeable; active maintenance |
|
||||
| `ical.js` | 2.2.1 | VEVENT parse + `ICAL.RecurExpansion` for recurrence | Already in both `apps/api` and `apps/pwa`; Phase 1 pattern established |
|
||||
| `rrule` | 2.8.1 | RRULE string parsing (used only if `ICAL.RecurExpansion` is insufficient) | Already in project stack per CLAUDE.md; last pub 2023-11-10 — treat as stable |
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
| ---------------------------- | ------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `@schedule-x/calendar` | 4.6.0 | Calendar engine (views, Temporal-based event model) | Selected in UI-SPEC; active maintenance; last published 2026-05-12 |
|
||||
| `@schedule-x/react` | 4.1.0 | React adapter (`useCalendarApp`, `ScheduleXCalendar`) | Official React adapter; peer-requires `@schedule-x/calendar ^3.1.0 \|\| ^4.0.0`; 4.6.0 satisfies this |
|
||||
| `@schedule-x/theme-default` | 4.6.0 | Default CSS layout; overridden by project tokens | Required for Schedule-X internal layout engine; all colors are token-overridden |
|
||||
| `@schedule-x/event-modal` | 4.6.0 | `createEventModalPlugin()` for custom `eventModal` component | Required to replace default modal with `EventDetailPopover` |
|
||||
| `@schedule-x/events-service` | 4.6.0 | `createEventsServicePlugin()` for dynamic event updates | Required to update events after TanStack Query fetches new window |
|
||||
| `temporal-polyfill` | 0.3.2 | `Temporal` global polyfill for browsers without native support | `@schedule-x/calendar` peer-requires `temporal-polyfill@0.3.0`; 0.3.2 satisfies |
|
||||
| `lucide-react` | 1.17.0 | Icon library (CalendarDays, X, MapPin, ChevronLeft/Right) | Specified in UI-SPEC; tree-shakeable; active maintenance |
|
||||
| `ical.js` | 2.2.1 | VEVENT parse + `ICAL.RecurExpansion` for recurrence | Already in both `apps/api` and `apps/pwa`; Phase 1 pattern established |
|
||||
| `rrule` | 2.8.1 | RRULE string parsing (used only if `ICAL.RecurExpansion` is insufficient) | Already in project stack per CLAUDE.md; last pub 2023-11-10 — treat as stable |
|
||||
|
||||
### No New Backend Dependencies Needed
|
||||
|
||||
@@ -119,20 +122,20 @@ pnpm add @schedule-x/calendar@4.6.0 @schedule-x/react@4.1.0 @schedule-x/theme-de
|
||||
|
||||
slopcheck was not available at research time. All packages below were verified via official documentation or established source repos. No packages flagged as suspicious by manual review.
|
||||
|
||||
| Package | Registry | Age | Source Repo | Postinstall | Disposition |
|
||||
|---------|----------|-----|-------------|-------------|-------------|
|
||||
| `@schedule-x/calendar` | npm | 2+ yrs | github.com/schedule-x/schedule-x | none | Approved |
|
||||
| `@schedule-x/react` | npm | 2+ yrs | github.com/schedule-x/react | none | Approved |
|
||||
| `@schedule-x/theme-default` | npm | 2+ yrs | github.com/schedule-x/schedule-x | none | Approved |
|
||||
| `@schedule-x/event-modal` | npm | 2+ yrs | github.com/schedule-x/schedule-x | none | Approved |
|
||||
| `@schedule-x/events-service` | npm | 2+ yrs | github.com/schedule-x/schedule-x | none | Approved |
|
||||
| `temporal-polyfill` | npm | 2+ yrs | github.com/fullcalendar/temporal-polyfill | none | Approved |
|
||||
| `lucide-react` | npm | 4+ yrs | github.com/lucide-icons/lucide | none | Approved |
|
||||
| Package | Registry | Age | Source Repo | Postinstall | Disposition |
|
||||
| ---------------------------- | -------- | ------ | ----------------------------------------- | ----------- | ----------- |
|
||||
| `@schedule-x/calendar` | npm | 2+ yrs | github.com/schedule-x/schedule-x | none | Approved |
|
||||
| `@schedule-x/react` | npm | 2+ yrs | github.com/schedule-x/react | none | Approved |
|
||||
| `@schedule-x/theme-default` | npm | 2+ yrs | github.com/schedule-x/schedule-x | none | Approved |
|
||||
| `@schedule-x/event-modal` | npm | 2+ yrs | github.com/schedule-x/schedule-x | none | Approved |
|
||||
| `@schedule-x/events-service` | npm | 2+ yrs | github.com/schedule-x/schedule-x | none | Approved |
|
||||
| `temporal-polyfill` | npm | 2+ yrs | github.com/fullcalendar/temporal-polyfill | none | Approved |
|
||||
| `lucide-react` | npm | 4+ yrs | github.com/lucide-icons/lucide | none | Approved |
|
||||
|
||||
**Packages removed due to slopcheck [SLOP] verdict:** none
|
||||
**Packages flagged as suspicious [SUS]:** none
|
||||
|
||||
*slopcheck was unavailable at research time. All packages are tagged [VERIFIED: npm registry] based on official source repos confirmed via npm view. Planner should add `checkpoint:human-verify` before install if extra caution is warranted — this two-person household app is self-hosted with no third-party attack surface for these well-established packages.*
|
||||
_slopcheck was unavailable at research time. All packages are tagged [VERIFIED: npm registry] based on official source repos confirmed via npm view. Planner should add `checkpoint:human-verify` before install if extra caution is warranted — this two-person household app is self-hosted with no third-party attack surface for these well-established packages._
|
||||
|
||||
---
|
||||
|
||||
@@ -222,23 +225,23 @@ apps/
|
||||
```typescript
|
||||
// apps/api/src/broker/expand.ts
|
||||
// Source: https://github.com/kewisch/ical.js/wiki/Common-Use-Cases
|
||||
import ICAL from 'ical.js'
|
||||
import ICAL from 'ical.js';
|
||||
|
||||
export interface CalendarOccurrence {
|
||||
id: string // `${uid}::${dtstart_iso}` — stable identity for Schedule-X
|
||||
uid: string
|
||||
calendarId: number
|
||||
calendarName: string
|
||||
ownerUserId: number
|
||||
color: string // hex from users.color or shared-family constant
|
||||
isShared: boolean // true when calendar is the shared-family calendar
|
||||
title: string
|
||||
start: string // ISO 8601 with timezone offset: '2026-06-15T10:00:00+02:00[America/Toronto]'
|
||||
// for all-day: 'DATE:2026-06-15' — use a distinct format so client knows
|
||||
end: string
|
||||
allDay: boolean
|
||||
location: string | null
|
||||
description: string | null
|
||||
id: string; // `${uid}::${dtstart_iso}` — stable identity for Schedule-X
|
||||
uid: string;
|
||||
calendarId: number;
|
||||
calendarName: string;
|
||||
ownerUserId: number;
|
||||
color: string; // hex from users.color or shared-family constant
|
||||
isShared: boolean; // true when calendar is the shared-family calendar
|
||||
title: string;
|
||||
start: string; // ISO 8601 with timezone offset: '2026-06-15T10:00:00+02:00[America/Toronto]'
|
||||
// for all-day: 'DATE:2026-06-15' — use a distinct format so client knows
|
||||
end: string;
|
||||
allDay: boolean;
|
||||
location: string | null;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export function expandOccurrences(
|
||||
@@ -251,24 +254,24 @@ export function expandOccurrences(
|
||||
color: string,
|
||||
isShared: boolean,
|
||||
): CalendarOccurrence[] {
|
||||
const parsed = ICAL.parse(rawVevent)
|
||||
const comp = new ICAL.Component(parsed)
|
||||
const parsed = ICAL.parse(rawVevent);
|
||||
const comp = new ICAL.Component(parsed);
|
||||
|
||||
// CRITICAL: Register VTIMEZONE components before RecurExpansion
|
||||
// Without this, RecurExpansion uses UTC and DST transitions produce wrong wall-clock times
|
||||
for (const vtimezone of comp.getAllSubcomponents('vtimezone')) {
|
||||
const tzid = vtimezone.getFirstPropertyValue('tzid') as string
|
||||
const tzid = vtimezone.getFirstPropertyValue('tzid') as string;
|
||||
if (tzid && !ICAL.TimezoneService.has(tzid)) {
|
||||
ICAL.TimezoneService.register(tzid, new ICAL.Timezone({ component: vtimezone, tzid }))
|
||||
ICAL.TimezoneService.register(tzid, new ICAL.Timezone({ component: vtimezone, tzid }));
|
||||
}
|
||||
}
|
||||
|
||||
const vevent = comp.getFirstSubcomponent('vevent')
|
||||
if (!vevent) return []
|
||||
const vevent = comp.getFirstSubcomponent('vevent');
|
||||
if (!vevent) return [];
|
||||
|
||||
const event = new ICAL.Event(vevent)
|
||||
const dtstart = vevent.getFirstPropertyValue('dtstart') as ICAL.Time
|
||||
const uid = event.uid
|
||||
const event = new ICAL.Event(vevent);
|
||||
const dtstart = vevent.getFirstPropertyValue('dtstart') as ICAL.Time;
|
||||
const uid = event.uid;
|
||||
|
||||
// Non-recurring event: single occurrence check
|
||||
if (!event.isRecurring()) {
|
||||
@@ -276,19 +279,19 @@ export function expandOccurrences(
|
||||
}
|
||||
|
||||
// Recurring event: use RecurExpansion
|
||||
const expand = new ICAL.RecurExpansion({ component: vevent, dtstart })
|
||||
const rangeStart = ICAL.Time.fromJSDate(windowStart, /* useUtc */ false)
|
||||
const rangeEnd = ICAL.Time.fromJSDate(windowEnd, /* useUtc */ false)
|
||||
const expand = new ICAL.RecurExpansion({ component: vevent, dtstart });
|
||||
const rangeStart = ICAL.Time.fromJSDate(windowStart, /* useUtc */ false);
|
||||
const rangeEnd = ICAL.Time.fromJSDate(windowEnd, /* useUtc */ false);
|
||||
|
||||
const occurrences: CalendarOccurrence[] = []
|
||||
let next: ICAL.Time | null
|
||||
const occurrences: CalendarOccurrence[] = [];
|
||||
let next: ICAL.Time | null;
|
||||
|
||||
while ((next = expand.next()) && next.compare(rangeEnd) < 0) {
|
||||
if (next.compare(rangeStart) < 0) continue
|
||||
if (next.compare(rangeStart) < 0) continue;
|
||||
// Build occurrence, compute end from duration
|
||||
// ...
|
||||
}
|
||||
return occurrences
|
||||
return occurrences;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -303,19 +306,19 @@ export function expandOccurrences(
|
||||
```typescript
|
||||
// apps/pwa/src/lib/hydrateEvents.ts
|
||||
// Source: https://schedule-x.dev/docs/calendar/events
|
||||
import 'temporal-polyfill/global' // registers Temporal on globalThis
|
||||
import type { CalendarOccurrence } from '@familysync/shared' // server type
|
||||
import 'temporal-polyfill/global'; // registers Temporal on globalThis
|
||||
import type { CalendarOccurrence } from '@familysync/shared'; // server type
|
||||
|
||||
export interface ScheduleXEvent {
|
||||
id: string
|
||||
title: string
|
||||
start: Temporal.ZonedDateTime | Temporal.PlainDate
|
||||
end: Temporal.ZonedDateTime | Temporal.PlainDate
|
||||
calendarId: string // must be string matching the key in calendars config
|
||||
location?: string
|
||||
description?: string
|
||||
id: string;
|
||||
title: string;
|
||||
start: Temporal.ZonedDateTime | Temporal.PlainDate;
|
||||
end: Temporal.ZonedDateTime | Temporal.PlainDate;
|
||||
calendarId: string; // must be string matching the key in calendars config
|
||||
location?: string;
|
||||
description?: string;
|
||||
// custom business fields pass through
|
||||
_familySync?: { uid: string; color: string; isShared: boolean }
|
||||
_familySync?: { uid: string; color: string; isShared: boolean };
|
||||
}
|
||||
|
||||
export function hydrateEvents(occurrences: CalendarOccurrence[]): ScheduleXEvent[] {
|
||||
@@ -325,11 +328,11 @@ export function hydrateEvents(occurrences: CalendarOccurrence[]): ScheduleXEvent
|
||||
return {
|
||||
id: occ.id,
|
||||
title: occ.title,
|
||||
start: Temporal.PlainDate.from(occ.start), // occ.start is 'YYYY-MM-DD'
|
||||
start: Temporal.PlainDate.from(occ.start), // occ.start is 'YYYY-MM-DD'
|
||||
end: Temporal.PlainDate.from(occ.end),
|
||||
calendarId: String(occ.calendarId),
|
||||
_familySync: { uid: occ.uid, color: occ.color, isShared: occ.isShared },
|
||||
}
|
||||
};
|
||||
}
|
||||
// Timed: use ZonedDateTime from the offset-aware ISO string the server returns
|
||||
return {
|
||||
@@ -341,8 +344,8 @@ export function hydrateEvents(occurrences: CalendarOccurrence[]): ScheduleXEvent
|
||||
location: occ.location ?? undefined,
|
||||
description: occ.description ?? undefined,
|
||||
_familySync: { uid: occ.uid, color: occ.color, isShared: occ.isShared },
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -357,51 +360,54 @@ import {
|
||||
createViewWeek,
|
||||
createViewMonthGrid,
|
||||
createViewMonthAgenda,
|
||||
} from '@schedule-x/calendar'
|
||||
import { createEventsServicePlugin } from '@schedule-x/events-service'
|
||||
import { createEventModalPlugin } from '@schedule-x/event-modal'
|
||||
} from '@schedule-x/calendar';
|
||||
import { createEventsServicePlugin } from '@schedule-x/events-service';
|
||||
import { createEventModalPlugin } from '@schedule-x/event-modal';
|
||||
|
||||
export const WEEK_START_DAY = 0 // 0 = Sunday in project convention; Schedule-X uses 7 = Sunday
|
||||
export const WEEK_START_DAY = 0; // 0 = Sunday in project convention; Schedule-X uses 7 = Sunday
|
||||
|
||||
// Schedule-X v4 firstDayOfWeek: Temporal numbering — 1=Mon, 7=Sun
|
||||
// Must translate from project convention (0=Sun) to Schedule-X (7=Sun)
|
||||
function toSXWeekStart(dayConvention: number): number {
|
||||
return dayConvention === 0 ? 7 : dayConvention
|
||||
return dayConvention === 0 ? 7 : dayConvention;
|
||||
}
|
||||
|
||||
export interface MemberCalendarConfig {
|
||||
id: string // String(users.id)
|
||||
name: string // users.displayName
|
||||
color: string // users.color hex
|
||||
id: string; // String(users.id)
|
||||
name: string; // users.displayName
|
||||
color: string; // users.color hex
|
||||
}
|
||||
|
||||
export function buildCalendarConfig(members: MemberCalendarConfig[]) {
|
||||
const calendars: Record<string, { colorName: string; lightColors: { main: string; container: string; onContainer: string } }> = {}
|
||||
const calendars: Record<
|
||||
string,
|
||||
{ colorName: string; lightColors: { main: string; container: string; onContainer: string } }
|
||||
> = {};
|
||||
|
||||
// Shared-family calendar: reserved rose color
|
||||
calendars['shared'] = {
|
||||
colorName: 'shared',
|
||||
lightColors: deriveScheduleXColors('#F25C7A'),
|
||||
}
|
||||
};
|
||||
|
||||
// Per-member calendars keyed by String(userId)
|
||||
for (const m of members) {
|
||||
calendars[m.id] = {
|
||||
colorName: `member-${m.id}`,
|
||||
lightColors: deriveScheduleXColors(m.color),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { calendars }
|
||||
return { calendars };
|
||||
}
|
||||
|
||||
// UI-SPEC color derivation: container = main at 15% opacity over white, onContainer = main darkened 40%
|
||||
function deriveScheduleXColors(main: string) {
|
||||
return {
|
||||
main,
|
||||
container: hexWithOpacity(main, 0.15), // CSS rgba computed over #FFFFFF
|
||||
container: hexWithOpacity(main, 0.15), // CSS rgba computed over #FFFFFF
|
||||
onContainer: darkenHex(main, 0.4),
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -467,7 +473,7 @@ function CalendarShell() {
|
||||
// apps/api/src/auth/devBypass.ts
|
||||
// Active ONLY when DEV_AUTH_BYPASS=true AND NODE_ENV !== 'production'
|
||||
// Injects a fixed dev user into the request context so oidcAuthMiddleware is skipped
|
||||
import type { MiddlewareHandler } from 'hono'
|
||||
import type { MiddlewareHandler } from 'hono';
|
||||
|
||||
const DEV_USER = {
|
||||
id: 1,
|
||||
@@ -475,21 +481,21 @@ const DEV_USER = {
|
||||
oidcSub: 'dev-user',
|
||||
displayName: 'Dev User',
|
||||
color: '#4A90D9',
|
||||
}
|
||||
};
|
||||
|
||||
export function devAuthBypass(): MiddlewareHandler {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// Hard guard — never active in production regardless of env flag
|
||||
return async (_c, next) => next()
|
||||
return async (_c, next) => next();
|
||||
}
|
||||
if (process.env.DEV_AUTH_BYPASS !== 'true') {
|
||||
return async (_c, next) => next()
|
||||
return async (_c, next) => next();
|
||||
}
|
||||
// Inject fixed dev user into Hono context (replaces getAuth(c) result)
|
||||
return async (c, next) => {
|
||||
c.set('user', DEV_USER)
|
||||
await next()
|
||||
}
|
||||
c.set('user', DEV_USER);
|
||||
await next();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -509,14 +515,14 @@ Mount in `index.ts` BEFORE `oidcAuthMiddleware` on `/api/*` when bypass is activ
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| Recurring event expansion with EXDATE | Custom RRULE iterator | `ICAL.RecurExpansion` | RecurExpansion handles RDATE, EXDATE, RECURRENCE-ID in one integrated iterator |
|
||||
| All four calendar views | Custom React grid | `@schedule-x/calendar` views | Day/week/month/agenda correctly handling overlap, all-day banners, and touch is 3-6 weeks of work |
|
||||
| Custom event modal | Custom DOM overlay | `createEventModalPlugin` + `customComponents.eventModal` | Schedule-X positions the modal relative to the event; re-use in Phase 3 is built-in |
|
||||
| VTIMEZONE DST tables | Custom offset lookup | `ICAL.TimezoneService.register()` from parsed VTIMEZONE | The VTIMEZONE component in the ICS already contains the correct DST rules for the calendar's timezone |
|
||||
| Calendar color derivation | Manual CSS computation | `colorUtils.ts` utility function (small, one-file) | The 15%/darken derivation is simple enough to implement inline; no third-party needed |
|
||||
| iCalendar string parsing | Custom VCALENDAR parser | `ICAL.parse()` + `ICAL.Component` | VCALENDAR has pathological edge cases (folded lines, UTF-8 encoded params, VTIMEZONE nesting) |
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
| ------------------------------------- | ----------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| Recurring event expansion with EXDATE | Custom RRULE iterator | `ICAL.RecurExpansion` | RecurExpansion handles RDATE, EXDATE, RECURRENCE-ID in one integrated iterator |
|
||||
| All four calendar views | Custom React grid | `@schedule-x/calendar` views | Day/week/month/agenda correctly handling overlap, all-day banners, and touch is 3-6 weeks of work |
|
||||
| Custom event modal | Custom DOM overlay | `createEventModalPlugin` + `customComponents.eventModal` | Schedule-X positions the modal relative to the event; re-use in Phase 3 is built-in |
|
||||
| VTIMEZONE DST tables | Custom offset lookup | `ICAL.TimezoneService.register()` from parsed VTIMEZONE | The VTIMEZONE component in the ICS already contains the correct DST rules for the calendar's timezone |
|
||||
| Calendar color derivation | Manual CSS computation | `colorUtils.ts` utility function (small, one-file) | The 15%/darken derivation is simple enough to implement inline; no third-party needed |
|
||||
| iCalendar string parsing | Custom VCALENDAR parser | `ICAL.parse()` + `ICAL.Component` | VCALENDAR has pathological edge cases (folded lines, UTF-8 encoded params, VTIMEZONE nesting) |
|
||||
|
||||
**Key insight:** Calendar view rendering that handles overlap, drag handle exclusion zones, DST, all-day banners, and touch gestures for iOS correctly is multi-month work. Schedule-X exists precisely for this.
|
||||
|
||||
@@ -603,63 +609,60 @@ Mount in `index.ts` BEFORE `oidcAuthMiddleware` on `/api/*` when bypass is activ
|
||||
```typescript
|
||||
// Source: https://github.com/kewisch/ical.js/wiki/Common-Use-Cases
|
||||
// Source: https://kewisch.github.io/ical.js/api/
|
||||
import ICAL from 'ical.js'
|
||||
import ICAL from 'ical.js';
|
||||
|
||||
function expandVeventOccurrences(
|
||||
rawVcalendar: string,
|
||||
windowStart: Date,
|
||||
windowEnd: Date,
|
||||
): Array<{ dtstart: Date; dtend: Date; allDay: boolean }> {
|
||||
const parsed = ICAL.parse(rawVcalendar)
|
||||
const comp = new ICAL.Component(parsed)
|
||||
const parsed = ICAL.parse(rawVcalendar);
|
||||
const comp = new ICAL.Component(parsed);
|
||||
|
||||
// Step 1: Register all VTIMEZONE components in this VCALENDAR.
|
||||
// Must happen BEFORE constructing ICAL.RecurExpansion.
|
||||
for (const vtz of comp.getAllSubcomponents('vtimezone')) {
|
||||
const tzid = vtz.getFirstPropertyValue('tzid') as string
|
||||
const tzid = vtz.getFirstPropertyValue('tzid') as string;
|
||||
if (tzid && !ICAL.TimezoneService.has(tzid)) {
|
||||
ICAL.TimezoneService.register(
|
||||
tzid,
|
||||
new ICAL.Timezone({ component: vtz, tzid }),
|
||||
)
|
||||
ICAL.TimezoneService.register(tzid, new ICAL.Timezone({ component: vtz, tzid }));
|
||||
}
|
||||
}
|
||||
|
||||
const vevent = comp.getFirstSubcomponent('vevent')
|
||||
if (!vevent) return []
|
||||
const vevent = comp.getFirstSubcomponent('vevent');
|
||||
if (!vevent) return [];
|
||||
|
||||
const event = new ICAL.Event(vevent)
|
||||
const dtstart = vevent.getFirstPropertyValue('dtstart') as ICAL.Time
|
||||
const allDay = dtstart.isDate
|
||||
const event = new ICAL.Event(vevent);
|
||||
const dtstart = vevent.getFirstPropertyValue('dtstart') as ICAL.Time;
|
||||
const allDay = dtstart.isDate;
|
||||
|
||||
const results: Array<{ dtstart: Date; dtend: Date; allDay: boolean }> = []
|
||||
const rangeStart = ICAL.Time.fromJSDate(windowStart, false)
|
||||
const rangeEnd = ICAL.Time.fromJSDate(windowEnd, false)
|
||||
const results: Array<{ dtstart: Date; dtend: Date; allDay: boolean }> = [];
|
||||
const rangeStart = ICAL.Time.fromJSDate(windowStart, false);
|
||||
const rangeEnd = ICAL.Time.fromJSDate(windowEnd, false);
|
||||
|
||||
if (!event.isRecurring()) {
|
||||
if (dtstart.compare(rangeStart) >= 0 && dtstart.compare(rangeEnd) < 0) {
|
||||
const dtend = vevent.getFirstPropertyValue('dtend') as ICAL.Time | null
|
||||
const dtend = vevent.getFirstPropertyValue('dtend') as ICAL.Time | null;
|
||||
results.push({
|
||||
dtstart: dtstart.toJSDate(),
|
||||
dtend: (dtend ?? dtstart).toJSDate(),
|
||||
allDay,
|
||||
})
|
||||
});
|
||||
}
|
||||
return results
|
||||
return results;
|
||||
}
|
||||
|
||||
// RecurExpansion handles RRULE + RDATE + EXDATE internally
|
||||
const expand = new ICAL.RecurExpansion({ component: vevent, dtstart })
|
||||
let next: ICAL.Time | null
|
||||
const expand = new ICAL.RecurExpansion({ component: vevent, dtstart });
|
||||
let next: ICAL.Time | null;
|
||||
while ((next = expand.next()) && next.compare(rangeEnd) < 0) {
|
||||
if (next.compare(rangeStart) < 0) continue
|
||||
if (next.compare(rangeStart) < 0) continue;
|
||||
// Compute end using the original event's duration
|
||||
const duration = event.duration
|
||||
const occEnd = next.clone()
|
||||
occEnd.addDuration(duration)
|
||||
results.push({ dtstart: next.toJSDate(), dtend: occEnd.toJSDate(), allDay })
|
||||
const duration = event.duration;
|
||||
const occEnd = next.clone();
|
||||
occEnd.addDuration(duration);
|
||||
results.push({ dtstart: next.toJSDate(), dtend: occEnd.toJSDate(), allDay });
|
||||
}
|
||||
return results
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -671,10 +674,14 @@ function expandVeventOccurrences(
|
||||
// The client must use Temporal.PlainDate — NOT ZonedDateTime
|
||||
|
||||
// WRONG (shifts date in negative-offset timezones):
|
||||
{ start: Temporal.ZonedDateTime.from('2026-06-15T00:00:00Z') }
|
||||
{
|
||||
start: Temporal.ZonedDateTime.from('2026-06-15T00:00:00Z');
|
||||
}
|
||||
|
||||
// CORRECT:
|
||||
{ start: Temporal.PlainDate.from('2026-06-15') }
|
||||
{
|
||||
start: Temporal.PlainDate.from('2026-06-15');
|
||||
}
|
||||
```
|
||||
|
||||
### Schedule-X CSS Token Override Pattern
|
||||
@@ -689,7 +696,7 @@ function expandVeventOccurrences(
|
||||
|
||||
:root {
|
||||
/* Map Schedule-X color vars to project tokens */
|
||||
--sx-color-primary: var(--color-member-0); /* current user's color */
|
||||
--sx-color-primary: var(--color-member-0); /* current user's color */
|
||||
--sx-color-on-primary: #ffffff;
|
||||
--sx-color-surface: var(--color-surface);
|
||||
--sx-color-on-surface: var(--color-text-primary);
|
||||
@@ -712,9 +719,9 @@ function expandVeventOccurrences(
|
||||
```typescript
|
||||
// apps/api/src/routes/events.ts — current implementation
|
||||
eventsRouter.get('/', async (c) => {
|
||||
const events = await db.select().from(calendarEvents) // no window, no join, no expansion
|
||||
return c.json({ events })
|
||||
})
|
||||
const events = await db.select().from(calendarEvents); // no window, no join, no expansion
|
||||
return c.json({ events });
|
||||
});
|
||||
```
|
||||
|
||||
### Target state (Phase 2)
|
||||
@@ -722,14 +729,14 @@ eventsRouter.get('/', async (c) => {
|
||||
```typescript
|
||||
// apps/api/src/routes/events.ts — evolved
|
||||
eventsRouter.get('/', async (c) => {
|
||||
const { start, end } = c.req.query()
|
||||
const { start, end } = c.req.query();
|
||||
// Zod-validate start/end as ISO dates
|
||||
// SQL: calendarEvents JOIN calendars JOIN users
|
||||
// WHERE (dtstartUtc BETWEEN start AND end) OR (dtstartDate BETWEEN start AND end)
|
||||
// OR event.hasRrule (to catch recurring masters whose window occurrence may differ)
|
||||
// For each row: call expandOccurrences(rawVevent, windowStart, windowEnd, ...)
|
||||
// Return: { occurrences: CalendarOccurrence[] }
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**SQL pre-filter strategy:** The SQL `WHERE` must also include events with an RRULE property that _started before_ the window, because a weekly meeting created 3 years ago can still have occurrences in the current window. Include a `hasRrule` boolean column (can be added via migration) or parse `rawVevent` in the expansion step and skip in-memory if no occurrences fall in window. The simpler approach: include all events where `dtstartUtc < windowEnd` (no lower bound) OR `dtstartDate < windowEnd`, then let `expandOccurrences` handle the window check. Add a schema migration to add a `hasRrule` boolean indexed column to `calendarEvents` to avoid scanning all historical events on every request.
|
||||
@@ -740,14 +747,15 @@ eventsRouter.get('/', async (c) => {
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Schedule-X ISO string events `"YYYY-MM-DD HH:MM"` | `Temporal.ZonedDateTime` / `Temporal.PlainDate` | Schedule-X v3 (2024) | Server must return parseable strings; client must hydrate |
|
||||
| `react-big-calendar` (moment/date-fns) | Schedule-X (Temporal-based) | 2024 ecosystem shift | react-big-calendar's CSS is hard to override; Schedule-X CSS tokens are first-class |
|
||||
| `FullCalendar` open-source | Schedule-X (fully MIT) | 2024 for self-hosted | FullCalendar premium features are commercial; Schedule-X is fully open |
|
||||
| rrule-only recurrence expansion | `ICAL.RecurExpansion` (higher-level) | ical.js 1.x+ | RecurExpansion integrates RRULE + RDATE + EXDATE; no separate EXDATE handling needed |
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
| ------------------------------------------------- | ----------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------ |
|
||||
| Schedule-X ISO string events `"YYYY-MM-DD HH:MM"` | `Temporal.ZonedDateTime` / `Temporal.PlainDate` | Schedule-X v3 (2024) | Server must return parseable strings; client must hydrate |
|
||||
| `react-big-calendar` (moment/date-fns) | Schedule-X (Temporal-based) | 2024 ecosystem shift | react-big-calendar's CSS is hard to override; Schedule-X CSS tokens are first-class |
|
||||
| `FullCalendar` open-source | Schedule-X (fully MIT) | 2024 for self-hosted | FullCalendar premium features are commercial; Schedule-X is fully open |
|
||||
| rrule-only recurrence expansion | `ICAL.RecurExpansion` (higher-level) | ical.js 1.x+ | RecurExpansion integrates RRULE + RDATE + EXDATE; no separate EXDATE handling needed |
|
||||
|
||||
**Deprecated/outdated:**
|
||||
|
||||
- `react-big-calendar`: Not deprecated per se, but the CSS override story is significantly worse for a token-based design system. The UI-SPEC already rejected it.
|
||||
- Schedule-X v2 ISO string format: Removed in v3. Any tutorial older than mid-2024 using string dates is wrong.
|
||||
|
||||
@@ -757,36 +765,36 @@ eventsRouter.get('/', async (c) => {
|
||||
|
||||
### Test Framework
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | Vitest (already configured in `apps/api/vitest.config.ts`) |
|
||||
| Config file | `apps/api/vitest.config.ts` (exists); `apps/pwa` has no test setup — needs Wave 0 |
|
||||
| Quick run command | `pnpm --filter @familysync/api test` |
|
||||
| Full suite command | `pnpm -r test` (workspace-wide) |
|
||||
| Property | Value |
|
||||
| ------------------ | --------------------------------------------------------------------------------- |
|
||||
| Framework | Vitest (already configured in `apps/api/vitest.config.ts`) |
|
||||
| Config file | `apps/api/vitest.config.ts` (exists); `apps/pwa` has no test setup — needs Wave 0 |
|
||||
| Quick run command | `pnpm --filter @familysync/api test` |
|
||||
| Full suite command | `pnpm -r test` (workspace-wide) |
|
||||
|
||||
### Phase Requirements → Test Map
|
||||
|
||||
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||
|--------|----------|-----------|-------------------|-------------|
|
||||
| CAL-02 (color) | Events returned with correct `color` field from `users.color` | unit | `pnpm --filter @familysync/api test -- tests/routes/events.test.ts` | ❌ Wave 0 |
|
||||
| CAL-02 (aggregation) | Events from multiple calendars (multiple users) returned in single response | unit | same file | ❌ Wave 0 |
|
||||
| CAL-03 (views) | Schedule-X renders without error with all four views configured | smoke | `pnpm --filter @familysync/pwa test -- calendar.spec.tsx` | ❌ Wave 0 |
|
||||
| CAL-07 (recurrence) | `expandOccurrences()` returns correct occurrences for weekly RRULE in a 30-day window | unit | `pnpm --filter @familysync/api test -- tests/broker/expand.test.ts` | ❌ Wave 0 |
|
||||
| CAL-07 (DST) | `expandOccurrences()` with America/New_York RRULE across March DST boundary returns correct wall-clock times | unit | same file | ❌ Wave 0 |
|
||||
| CAL-07 (all-day) | `expandOccurrences()` for all-day event returns `allDay: true` and `start: 'YYYY-MM-DD'` with no time component | unit | same file | ❌ Wave 0 |
|
||||
| CAL-07 (EXDATE) | `expandOccurrences()` excludes EXDATE occurrences from expansion | unit | same file | ❌ Wave 0 |
|
||||
| CAL-07 (Temporal) | `hydrateEvents()` converts all-day occurrences to `Temporal.PlainDate` and timed to `Temporal.ZonedDateTime` | unit | `pnpm --filter @familysync/pwa test -- lib/hydrateEvents.test.ts` | ❌ Wave 0 |
|
||||
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------- | ------------ |
|
||||
| CAL-02 (color) | Events returned with correct `color` field from `users.color` | unit | `pnpm --filter @familysync/api test -- tests/routes/events.test.ts` | ❌ Wave 0 |
|
||||
| CAL-02 (aggregation) | Events from multiple calendars (multiple users) returned in single response | unit | same file | ❌ Wave 0 |
|
||||
| CAL-03 (views) | Schedule-X renders without error with all four views configured | smoke | `pnpm --filter @familysync/pwa test -- calendar.spec.tsx` | ❌ Wave 0 |
|
||||
| CAL-07 (recurrence) | `expandOccurrences()` returns correct occurrences for weekly RRULE in a 30-day window | unit | `pnpm --filter @familysync/api test -- tests/broker/expand.test.ts` | ❌ Wave 0 |
|
||||
| CAL-07 (DST) | `expandOccurrences()` with America/New_York RRULE across March DST boundary returns correct wall-clock times | unit | same file | ❌ Wave 0 |
|
||||
| CAL-07 (all-day) | `expandOccurrences()` for all-day event returns `allDay: true` and `start: 'YYYY-MM-DD'` with no time component | unit | same file | ❌ Wave 0 |
|
||||
| CAL-07 (EXDATE) | `expandOccurrences()` excludes EXDATE occurrences from expansion | unit | same file | ❌ Wave 0 |
|
||||
| CAL-07 (Temporal) | `hydrateEvents()` converts all-day occurrences to `Temporal.PlainDate` and timed to `Temporal.ZonedDateTime` | unit | `pnpm --filter @familysync/pwa test -- lib/hydrateEvents.test.ts` | ❌ Wave 0 |
|
||||
|
||||
### Fixture ICS Files (test corpus)
|
||||
|
||||
The most valuable test artifacts are fixture `.ics` files. Create in `apps/api/tests/fixtures/`:
|
||||
|
||||
| Filename | Contents | Tests |
|
||||
|----------|----------|-------|
|
||||
| `weekly-dst.ics` | Weekly meeting at 10:00 America/New_York spanning March DST transition (2026-03-01 to 2026-04-30) | CAL-07 DST |
|
||||
| `allday-birthday.ics` | Annual birthday event (DATE type, no DTEND) | CAL-07 all-day |
|
||||
| `exdate-series.ics` | Weekly series with one EXDATE (a skipped occurrence) | CAL-07 EXDATE |
|
||||
| `multi-cal.ics` | Two separate VCALENDAR blobs to represent two members' events | CAL-02 aggregation |
|
||||
| Filename | Contents | Tests |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------- | ------------------ |
|
||||
| `weekly-dst.ics` | Weekly meeting at 10:00 America/New_York spanning March DST transition (2026-03-01 to 2026-04-30) | CAL-07 DST |
|
||||
| `allday-birthday.ics` | Annual birthday event (DATE type, no DTEND) | CAL-07 all-day |
|
||||
| `exdate-series.ics` | Weekly series with one EXDATE (a skipped occurrence) | CAL-07 EXDATE |
|
||||
| `multi-cal.ics` | Two separate VCALENDAR blobs to represent two members' events | CAL-02 aggregation |
|
||||
|
||||
These fixture files can be generated from real Fastmail ICS exports or hand-crafted with known-correct VTIMEZONE blocks.
|
||||
|
||||
@@ -813,34 +821,34 @@ These fixture files can be generated from real Fastmail ICS exports or hand-craf
|
||||
|
||||
### Applicable ASVS Categories
|
||||
|
||||
| ASVS Category | Applies | Standard Control |
|
||||
|---------------|---------|-----------------|
|
||||
| V2 Authentication | yes — dev bypass must not leak | `NODE_ENV === 'production'` hard guard in `devAuthBypass()` |
|
||||
| V3 Session Management | carried from Phase 1 | `@hono/oidc-auth` JWT cookie (httpOnly + Secure + SameSite) |
|
||||
| V4 Access Control | yes — `/api/events` must be authenticated | `oidcAuthMiddleware` on `/api/*` (Phase 1 pattern) |
|
||||
| V5 Input Validation | yes — `?start=` and `?end=` query params | `zod` + `@hono/zod-validator`: validate ISO date format before SQL |
|
||||
| V6 Cryptography | no new crypto in Phase 2 | — |
|
||||
| ASVS Category | Applies | Standard Control |
|
||||
| --------------------- | ----------------------------------------- | ------------------------------------------------------------------ |
|
||||
| V2 Authentication | yes — dev bypass must not leak | `NODE_ENV === 'production'` hard guard in `devAuthBypass()` |
|
||||
| V3 Session Management | carried from Phase 1 | `@hono/oidc-auth` JWT cookie (httpOnly + Secure + SameSite) |
|
||||
| V4 Access Control | yes — `/api/events` must be authenticated | `oidcAuthMiddleware` on `/api/*` (Phase 1 pattern) |
|
||||
| V5 Input Validation | yes — `?start=` and `?end=` query params | `zod` + `@hono/zod-validator`: validate ISO date format before SQL |
|
||||
| V6 Cryptography | no new crypto in Phase 2 | — |
|
||||
|
||||
### Known Threat Patterns for This Phase
|
||||
|
||||
| Pattern | STRIDE | Standard Mitigation |
|
||||
|---------|--------|---------------------|
|
||||
| Dev-auth bypass left active in production | Elevation of privilege | Hard `NODE_ENV !== 'production'` guard; `.env.example` warning |
|
||||
| SQL injection via `?start=` / `?end=` date params | Tampering | Zod ISO date validation; Drizzle parameterized queries |
|
||||
| XSS via event title/description in EventDetailPopover | Tampering | React's default JSX escaping; never use `dangerouslySetInnerHTML` for event fields |
|
||||
| Overfetch (no window) timing/DoS | Denial of service | Zod-enforce required `start` + `end` params; cap window to 90 days max |
|
||||
| Pattern | STRIDE | Standard Mitigation |
|
||||
| ----------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------- |
|
||||
| Dev-auth bypass left active in production | Elevation of privilege | Hard `NODE_ENV !== 'production'` guard; `.env.example` warning |
|
||||
| SQL injection via `?start=` / `?end=` date params | Tampering | Zod ISO date validation; Drizzle parameterized queries |
|
||||
| XSS via event title/description in EventDetailPopover | Tampering | React's default JSX escaping; never use `dangerouslySetInnerHTML` for event fields |
|
||||
| Overfetch (no window) timing/DoS | Denial of service | Zod-enforce required `start` + `end` params; cap window to 90 days max |
|
||||
|
||||
---
|
||||
|
||||
## Environment Availability
|
||||
|
||||
| Dependency | Required By | Available | Version | Fallback |
|
||||
|------------|------------|-----------|---------|----------|
|
||||
| Node.js 22 | API + PWA build | ✓ | (WSL2 dev env — assumed from Phase 1) | — |
|
||||
| pnpm | Workspace install | ✓ | (Phase 1 used it) | — |
|
||||
| MariaDB (Docker) | `/api/events` windowed query | ✓ | Phase 1 confirmed: 503 events cached | — |
|
||||
| Temporal (browser) | Schedule-X v4 | Partial | Needs `temporal-polyfill` in PWA | `temporal-polyfill@0.3.2` — no fallback needed |
|
||||
| Live Authelia/Pangolin | Full auth flow | ✗ (D-14 deferred) | — | Dev-auth bypass middleware (must build in Phase 2) |
|
||||
| Dependency | Required By | Available | Version | Fallback |
|
||||
| ---------------------- | ---------------------------- | ----------------- | ------------------------------------- | -------------------------------------------------- |
|
||||
| Node.js 22 | API + PWA build | ✓ | (WSL2 dev env — assumed from Phase 1) | — |
|
||||
| pnpm | Workspace install | ✓ | (Phase 1 used it) | — |
|
||||
| MariaDB (Docker) | `/api/events` windowed query | ✓ | Phase 1 confirmed: 503 events cached | — |
|
||||
| Temporal (browser) | Schedule-X v4 | Partial | Needs `temporal-polyfill` in PWA | `temporal-polyfill@0.3.2` — no fallback needed |
|
||||
| Live Authelia/Pangolin | Full auth flow | ✗ (D-14 deferred) | — | Dev-auth bypass middleware (must build in Phase 2) |
|
||||
|
||||
**Missing with no fallback:** None. Dev-auth bypass covers the Authelia deferral.
|
||||
|
||||
@@ -848,12 +856,12 @@ These fixture files can be generated from real Fastmail ICS exports or hand-craf
|
||||
|
||||
## Assumptions Log
|
||||
|
||||
| # | Claim | Section | Risk if Wrong |
|
||||
|---|-------|---------|---------------|
|
||||
| A1 | `ICAL.RecurExpansion` handles EXDATE internally when using the high-level API | Architecture Patterns | Planner would need to add manual EXDATE filtering in `expandOccurrences` |
|
||||
| A2 | `@schedule-x/react@4.1.0` is API-compatible with `@schedule-x/calendar@4.6.0` for the features used (views, calendars, onRangeUpdate, customComponents) | Standard Stack | Version mismatch may cause TypeScript errors on newer options; test in Wave 0 |
|
||||
| A3 | The shared-family calendar can be identified programmatically (by displayName or a new `isShared` column) without a schema migration | Backend: /api/events evolution | If not deterministic, Phase 2 plan must include a migration adding `calendars.isShared` |
|
||||
| A4 | `onRangeUpdate` fires immediately on mount with the initial window | Architecture Patterns | If it does not fire on mount, initial fetch requires a separate first-render trigger |
|
||||
| # | Claim | Section | Risk if Wrong |
|
||||
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------- |
|
||||
| A1 | `ICAL.RecurExpansion` handles EXDATE internally when using the high-level API | Architecture Patterns | Planner would need to add manual EXDATE filtering in `expandOccurrences` |
|
||||
| A2 | `@schedule-x/react@4.1.0` is API-compatible with `@schedule-x/calendar@4.6.0` for the features used (views, calendars, onRangeUpdate, customComponents) | Standard Stack | Version mismatch may cause TypeScript errors on newer options; test in Wave 0 |
|
||||
| A3 | The shared-family calendar can be identified programmatically (by displayName or a new `isShared` column) without a schema migration | Backend: /api/events evolution | If not deterministic, Phase 2 plan must include a migration adding `calendars.isShared` |
|
||||
| A4 | `onRangeUpdate` fires immediately on mount with the initial window | Architecture Patterns | If it does not fire on mount, initial fetch requires a separate first-render trigger |
|
||||
|
||||
**A1 verification:** The ical.js wiki states RecurExpansion "takes into account recurrence exceptions (RDATE and EXDATE)" [CITED: github.com/kewisch/ical.js/wiki/Common-Use-Cases]. Treat as HIGH confidence.
|
||||
**A2 verification:** Peer dep `^4.0.0` satisfied by 4.6.0 [VERIFIED: npm registry]. API surface used (views, calendars, onRangeUpdate) is stable since v4.0.0. Treat as MEDIUM confidence — validate in Wave 0.
|
||||
@@ -884,6 +892,7 @@ These fixture files can be generated from real Fastmail ICS exports or hand-craf
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
|
||||
- [schedule-x.dev/docs/frameworks/react](https://schedule-x.dev/docs/frameworks/react) — React adapter usage, views, eventsService plugin
|
||||
- [schedule-x.dev/docs/calendar/calendars](https://schedule-x.dev/docs/calendar/calendars) — lightColors config, calendarId on events
|
||||
- [schedule-x.dev/docs/calendar/events](https://schedule-x.dev/docs/calendar/events) — Temporal.ZonedDateTime / Temporal.PlainDate requirement
|
||||
@@ -895,11 +904,13 @@ These fixture files can be generated from real Fastmail ICS exports or hand-craf
|
||||
- `npm view` on all Phase 2 packages — versions and publish dates confirmed [VERIFIED: npm registry]
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
|
||||
- [schedule-x.dev/docs/calendar/major-version-migrations](https://schedule-x.dev/docs/calendar/major-version-migrations) — v2→v3 breaking changes (Temporal adoption confirmed)
|
||||
- [schedule-x.dev/docs/calendar/plugins/event-modal](https://schedule-x.dev/docs/calendar/plugins/event-modal) — createEventModalPlugin + customComponents.eventModal
|
||||
- WebSearch on rrule DST behavior — confirmed known issue with `tzid` parameter and UTC fallback; `ICAL.RecurExpansion` is the recommended alternative
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
|
||||
- WebSearch results on VTIMEZONE registration best practices — cross-verified with official ical.js wiki
|
||||
|
||||
---
|
||||
@@ -907,6 +918,7 @@ These fixture files can be generated from real Fastmail ICS exports or hand-craf
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
|
||||
- Standard stack: HIGH — all packages verified on npm registry; Schedule-X selected in UI-SPEC
|
||||
- Architecture (recurrence expansion): HIGH — ICAL.RecurExpansion documented in official ical.js wiki; Phase 1 sync.ts pattern extended
|
||||
- Architecture (Schedule-X Temporal format): HIGH — verified against official Schedule-X docs
|
||||
|
||||
Reference in New Issue
Block a user