docs(02): create calendar-display phase plan (5 plans, 4 waves)
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
---
|
||||
phase: 02-calendar-display
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- apps/api/src/db/schema.ts
|
||||
- apps/api/src/auth/devBypass.ts
|
||||
- apps/api/src/index.ts
|
||||
- apps/api/tests/fixtures/weekly-dst.ics
|
||||
- apps/api/tests/fixtures/allday-birthday.ics
|
||||
- apps/api/tests/fixtures/exdate-series.ics
|
||||
- apps/api/tests/broker/expand.test.ts
|
||||
- apps/api/tests/routes/events.test.ts
|
||||
- apps/api/tests/auth/devBypass.test.ts
|
||||
- apps/pwa/vitest.config.ts
|
||||
- apps/pwa/package.json
|
||||
- apps/pwa/src/lib/hydrateEvents.test.ts
|
||||
- apps/pwa/src/lib/calendarConfig.test.ts
|
||||
- .env.example
|
||||
- docs/deployment.md
|
||||
autonomous: true
|
||||
requirements: [CAL-02, CAL-03, CAL-07]
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Drizzle schema has an indexed hasRrule boolean on calendar_events and an isShared boolean on calendars, both pushed to the live MariaDB"
|
||||
- "PWA test runner (vitest + jsdom + @testing-library/react) executes and a smoke test passes"
|
||||
- "Dev-auth bypass middleware injects a fixed dev user only when DEV_AUTH_BYPASS=true AND NODE_ENV!=production"
|
||||
- "Failing-but-present test stubs exist for expand, events route, hydrateEvents, and calendarConfig (Wave 0 RED state)"
|
||||
artifacts:
|
||||
- path: "apps/api/src/db/schema.ts"
|
||||
provides: "hasRrule + isShared columns + idx_calendar_events_has_rrule index"
|
||||
contains: "has_rrule"
|
||||
- path: "apps/api/src/auth/devBypass.ts"
|
||||
provides: "devAuthBypass() middleware with hard production guard"
|
||||
exports: ["devAuthBypass"]
|
||||
- path: "apps/pwa/vitest.config.ts"
|
||||
provides: "jsdom-environment vitest config for PWA"
|
||||
contains: "jsdom"
|
||||
- path: "apps/api/tests/fixtures/weekly-dst.ics"
|
||||
provides: "DST-spanning weekly RRULE fixture for CAL-07 tests"
|
||||
min_lines: 10
|
||||
key_links:
|
||||
- from: "apps/api/src/index.ts"
|
||||
to: "apps/api/src/auth/devBypass.ts"
|
||||
via: "app.use('/api/*', devAuthBypass()) before oidcAuthMiddleware"
|
||||
pattern: "devAuthBypass"
|
||||
- from: "apps/pwa/package.json"
|
||||
to: "vitest"
|
||||
via: "test script + devDependencies"
|
||||
pattern: "\"test\".*vitest"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Lay the verifiable foundation for the Phase 2 calendar slice: add the two schema columns the
|
||||
display pipeline depends on (`calendar_events.hasRrule`, `calendars.isShared`), push them to the
|
||||
live MariaDB, stand up the PWA test runner, write the dev-auth bypass so the UI can be built
|
||||
without live Authelia (D-14), and create the failing test stubs + ICS fixtures that all later
|
||||
waves turn green.
|
||||
|
||||
Purpose: Every later plan (expansion engine, windowed route, hydration, calendar render) needs
|
||||
these columns, the test harness, and the dev-auth bypass to exist first. This is the only
|
||||
horizontal-foundation plan in the phase — kept minimal so the next plan delivers a real slice.
|
||||
Output: Migrated schema (pushed), PWA vitest harness, dev-auth bypass middleware, ICS fixtures,
|
||||
RED test stubs.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/02-calendar-display/02-RESEARCH.md
|
||||
@.planning/phases/02-calendar-display/02-PATTERNS.md
|
||||
@.planning/phases/02-calendar-display/02-CONTEXT.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Add schema columns + PWA test harness + ICS fixtures + RED test stubs</name>
|
||||
<files>apps/api/src/db/schema.ts, apps/pwa/vitest.config.ts, apps/pwa/package.json, apps/api/tests/fixtures/weekly-dst.ics, apps/api/tests/fixtures/allday-birthday.ics, apps/api/tests/fixtures/exdate-series.ics, apps/api/tests/broker/expand.test.ts, apps/api/tests/routes/events.test.ts, apps/pwa/src/lib/hydrateEvents.test.ts, apps/pwa/src/lib/calendarConfig.test.ts</files>
|
||||
<read_first>
|
||||
- apps/api/src/db/schema.ts (current calendarEvents + calendars table definitions; column + index style to copy)
|
||||
- apps/api/vitest.config.ts (analog for PWA config — change environment node→jsdom)
|
||||
- apps/pwa/package.json (current scripts + devDependencies block)
|
||||
- apps/api/tests/broker/poller.test.ts (test file structure: describe/it/expect, vi.mock hoisting)
|
||||
- apps/api/tests/health.test.ts (Hono app.request() route-test pattern)
|
||||
- .planning/phases/02-calendar-display/02-RESEARCH.md §"Validation Architecture" (fixture corpus + Wave 0 gaps table)
|
||||
- .planning/phases/02-calendar-display/02-PATTERNS.md §"apps/pwa/vitest.config.ts" and §"apps/api/tests/broker/expand.test.ts"
|
||||
</read_first>
|
||||
<behavior>
|
||||
- expand.test.ts: weekly America/New_York RRULE expanded across the March 2026 DST boundary returns occurrences whose wall-clock time stays 10:00 local on both sides of the transition (currently RED — expandOccurrences does not exist yet)
|
||||
- expand.test.ts: all-day birthday fixture returns allDay:true with start as 'YYYY-MM-DD' and no time component
|
||||
- expand.test.ts: exdate-series fixture omits the single EXDATE-excluded occurrence
|
||||
- events.test.ts: GET /api/events?start=&end= returns occurrences each carrying a color field and isShared flag (RED — route not evolved yet)
|
||||
- hydrateEvents.test.ts: all-day occurrence → Temporal.PlainDate; timed occurrence → Temporal.ZonedDateTime
|
||||
- calendarConfig.test.ts: WEEK_START_DAY=0 translates to Schedule-X firstDayOfWeek 7
|
||||
</behavior>
|
||||
<action>
|
||||
Add to `calendarEvents` in schema.ts: `hasRrule: boolean('has_rrule').default(false).notNull()`, and a new index `index('idx_calendar_events_has_rrule').on(t.hasRrule)` in the table's index array (copy the exact style of `idx_calendar_events_dtstart_utc`). Add to `calendars`: `isShared: boolean('is_shared').default(false).notNull()`. `boolean` and `index` are already imported.
|
||||
|
||||
Create `apps/pwa/vitest.config.ts` mirroring `apps/api/vitest.config.ts` but with `environment: 'jsdom'` and `globals: true`. In `apps/pwa/package.json` add `"test": "vitest run"` to scripts and add devDependencies `vitest`, `@testing-library/react`, `@testing-library/jest-dom`, `jsdom` (use versions compatible with the workspace's existing vitest major; match the version in apps/api). Install via `pnpm install` at repo root.
|
||||
|
||||
Create three ICS fixtures under `apps/api/tests/fixtures/`: `weekly-dst.ics` (VEVENT with `DTSTART;TZID=America/New_York:20260301T100000`, `RRULE:FREQ=WEEKLY`, and a full `VTIMEZONE` block for America/New_York with both STANDARD and DAYLIGHT subcomponents so DST rules are present), `allday-birthday.ics` (VEVENT with `DTSTART;VALUE=DATE:20260615`, yearly RRULE, no DTEND), `exdate-series.ics` (weekly VEVENT with one `EXDATE` line removing a single occurrence). These must be valid VCALENDAR strings parseable by ICAL.parse.
|
||||
|
||||
Create the four RED test stubs. Each test imports the not-yet-existing module (`../../src/broker/expand.js`, etc.) so the file fails to resolve — that is the intended RED state. Per the Nyquist rule, mark each `<automated>` for the modules they cover as satisfied here. Use the describe/it patterns from poller.test.ts and health.test.ts. Load fixtures with `readFileSync` relative to the test file. Do NOT implement expand.ts, the route changes, hydrateEvents.ts, or calendarConfig.ts in this task — only the stubs that later plans turn green.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && grep -q "has_rrule" src/db/schema.ts && grep -q "is_shared" src/db/schema.ts && grep -q "idx_calendar_events_has_rrule" src/db/schema.ts && echo SCHEMA_OK</automated>
|
||||
<automated>cd /home/luc/Projects/familysync && grep -q jsdom apps/pwa/vitest.config.ts && grep -q '"test": "vitest run"' apps/pwa/package.json && echo PWA_HARNESS_OK</automated>
|
||||
<automated>cd apps/api && node -e "const I=require('ical.js');for(const f of ['weekly-dst','allday-birthday','exdate-series']){I.parse(require('fs').readFileSync('tests/fixtures/'+f+'.ics','utf8'))};console.log('FIXTURES_PARSE_OK')"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- apps/api/src/db/schema.ts contains `hasRrule: boolean('has_rrule')` and `idx_calendar_events_has_rrule`
|
||||
- apps/api/src/db/schema.ts contains `isShared: boolean('is_shared')` on the calendars table
|
||||
- apps/pwa/vitest.config.ts contains `environment: 'jsdom'`
|
||||
- apps/pwa/package.json scripts contains `"test": "vitest run"` and devDependencies include vitest, @testing-library/react, jsdom
|
||||
- All three fixture .ics files parse via ICAL.parse without throwing
|
||||
- weekly-dst.ics contains a VTIMEZONE block with both STANDARD and DAYLIGHT subcomponents
|
||||
- expand.test.ts, events.test.ts, hydrateEvents.test.ts, calendarConfig.test.ts exist and reference their target modules (RED is expected — modules not yet built)
|
||||
</acceptance_criteria>
|
||||
<done>Schema columns + index added; PWA vitest/jsdom harness installed and runnable; three ICS fixtures parse; four RED test stubs exist referencing not-yet-built modules.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Dev-auth bypass middleware + production guard + env docs</name>
|
||||
<files>apps/api/src/auth/devBypass.ts, apps/api/src/index.ts, apps/api/tests/auth/devBypass.test.ts, .env.example, docs/deployment.md</files>
|
||||
<read_first>
|
||||
- apps/api/src/auth/middleware.ts (re-export pattern; getAuth/oidcAuthMiddleware surface)
|
||||
- apps/api/src/index.ts (current middleware mount order: callback → /health → oidcAuthMiddleware on /api/* → routes)
|
||||
- apps/api/src/routes/me.ts (how getAuth(c) is consumed downstream — the injected user must satisfy it)
|
||||
- apps/api/src/auth/user.ts (DEV_USER shape: id, displayName, color; COLOR_PALETTE[0])
|
||||
- .planning/phases/02-calendar-display/02-RESEARCH.md §"Pattern 5: Dev-Auth Bypass Middleware" and §"Pitfall 7"
|
||||
- docs/deployment.md (existing dev-auth-bypass / Gate 2 context to extend)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- devBypass.test.ts: with NODE_ENV='production' the middleware is a pure passthrough and never sets a user, even if DEV_AUTH_BYPASS='true'
|
||||
- devBypass.test.ts: with NODE_ENV='test' and DEV_AUTH_BYPASS unset, middleware is passthrough (no user injected)
|
||||
- devBypass.test.ts: with NODE_ENV!='production' and DEV_AUTH_BYPASS='true', a fixed dev user is injected into the Hono context
|
||||
</behavior>
|
||||
<action>
|
||||
Create `apps/api/src/auth/devBypass.ts` exporting `devAuthBypass(): MiddlewareHandler`. FIRST check `process.env.NODE_ENV === 'production'` and return a no-op passthrough (`async (_c, next) => next()`) before reading any other env var — this hard guard is mandatory (Pitfall 7). Then if `process.env.DEV_AUTH_BYPASS !== 'true'`, also return passthrough. Otherwise return a handler that calls `c.set('user', DEV_USER)` then `await next()`. DEV_USER = a fixed object `{ id, oidcIss: 'dev', oidcSub: 'dev-user', displayName: 'Dev User', color }` where color is `COLOR_PALETTE[0]` ('#4A90D9'). Match the context key (`'user'`) and shape that `getAuth(c)` consumers in me.ts expect — read me.ts to confirm whether downstream reads `getAuth(c)` or `c.get('user')`; if me.ts uses `getAuth(c)` from @hono/oidc-auth, set both the auth claim and `c.set('user', DEV_USER)` so the events route (which will read the resolved user) works. Document the exact mechanism in a top-of-file comment.
|
||||
|
||||
In `apps/api/src/index.ts`, mount `app.use('/api/*', devAuthBypass())` on the line immediately BEFORE the existing `app.use('/api/*', oidcAuthMiddleware())`. The bypass is a no-op when inactive, so production behavior is unchanged.
|
||||
|
||||
Add `DEV_AUTH_BYPASS` to `.env.example` with a comment: `# DEV ONLY — injects a fixed dev user, skips Authelia. Hard-disabled when NODE_ENV=production. NEVER set in prod.` Extend `docs/deployment.md` dev-auth-bypass section to note the NODE_ENV production hard guard and that the production Docker Compose must not set DEV_AUTH_BYPASS.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && pnpm test -- tests/auth/devBypass.test.ts</automated>
|
||||
<automated>cd apps/api && grep -q "NODE_ENV === 'production'" src/auth/devBypass.ts && grep -q "devAuthBypass()" src/index.ts && echo BYPASS_WIRED</automated>
|
||||
<automated>cd /home/luc/Projects/familysync && grep -q "DEV_AUTH_BYPASS" .env.example && echo ENV_DOCUMENTED</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- apps/api/src/auth/devBypass.ts exports devAuthBypass and the FIRST conditional checks NODE_ENV === 'production'
|
||||
- apps/api/src/index.ts calls devAuthBypass() on /api/* immediately before oidcAuthMiddleware()
|
||||
- tests/auth/devBypass.test.ts passes: production guard, unset-flag passthrough, and active-injection cases all green
|
||||
- .env.example documents DEV_AUTH_BYPASS with the production warning comment
|
||||
- docs/deployment.md notes the production hard guard and prod-compose prohibition
|
||||
</acceptance_criteria>
|
||||
<done>devAuthBypass middleware exists with production hard guard, is mounted before oidcAuthMiddleware, all three behavior tests pass, env + deployment docs updated.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" gate="blocking">
|
||||
<name>Task 3: [BLOCKING] Push Drizzle schema to live MariaDB</name>
|
||||
<files>apps/api (drizzle-kit push — no source file)</files>
|
||||
<read_first>
|
||||
- apps/api/src/db/schema.ts (the columns added in Task 1 that must reach the live DB)
|
||||
- apps/api/drizzle.config.ts (push target / credentials config)
|
||||
- .planning/phases/02-calendar-display/02-RESEARCH.md §"Environment Availability" (MariaDB Docker confirmed up)
|
||||
</read_first>
|
||||
<action>
|
||||
Run `npx drizzle-kit push` in `apps/api` to apply the `has_rrule`, `idx_calendar_events_has_rrule`, and `calendars.is_shared` schema changes to the live MariaDB. This is MANDATORY and BLOCKING: type checks and builds pass without it (types come from the Drizzle config, not the live DB), so skipping it produces a false-positive verification state where the windowed query in Plan 02 fails at runtime with "unknown column has_rrule". The MariaDB container must be running (Phase 1 confirmed it up with 503 cached events). If drizzle-kit push emits an interactive confirmation prompt that cannot be auto-confirmed, stop and surface it — do not guess answers to destructive prompts.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && node -e "const m=require('mysql2/promise');(async()=>{const c=await m.createConnection(process.env.DATABASE_URL);const [r]=await c.query('SHOW COLUMNS FROM calendar_events LIKE \'has_rrule\'');const [s]=await c.query('SHOW COLUMNS FROM calendars LIKE \'is_shared\'');if(r.length&&s.length){console.log('PUSH_OK')}else{process.exit(1)};await c.end()})()"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `SHOW COLUMNS FROM calendar_events LIKE 'has_rrule'` returns one row against the live MariaDB
|
||||
- `SHOW COLUMNS FROM calendars LIKE 'is_shared'` returns one row against the live MariaDB
|
||||
- drizzle-kit push completed without destructive data loss on the existing 503-event cache
|
||||
</acceptance_criteria>
|
||||
<done>has_rrule (+ index) and calendars.is_shared exist in the live MariaDB schema; existing event cache intact.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| browser → /api/* | OIDC-gated; dev-auth bypass replaces the gate in dev only |
|
||||
| CI/prod env → app config | DEV_AUTH_BYPASS env var could leak into production |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-02-01 | Elevation of privilege | devAuthBypass() | mitigate | Hard `NODE_ENV === 'production'` guard as the FIRST conditional, before reading DEV_AUTH_BYPASS; .env.example warning; prod compose must not set the flag (Pitfall 7) |
|
||||
| T-02-02 | Tampering | drizzle-kit push | accept | Local dev DB; push reviewed; no untrusted input. Operator runs push against own MariaDB |
|
||||
| T-02-SC | Tampering | pnpm installs (vitest, @testing-library/*, jsdom) | mitigate | All packages are mainstream, audited in RESEARCH §Package Legitimacy (Approved); no [ASSUMED]/[SUS] packages in this plan |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `pnpm --filter @familysync/api test` runs (devBypass test green; expand/events stubs RED as designed)
|
||||
- `pnpm --filter @familysync/pwa test` runner executes under jsdom
|
||||
- Live MariaDB has has_rrule + is_shared columns
|
||||
- `tsc --noEmit` clean in apps/api after schema edits
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Schema columns added, pushed, and verified against the live DB
|
||||
- PWA test runner operational
|
||||
- Dev-auth bypass green with production hard guard
|
||||
- ICS fixtures parse; RED stubs in place for later waves
|
||||
</success_criteria>
|
||||
|
||||
<artifacts_produced>
|
||||
## Artifacts this phase produces (Plan 01)
|
||||
|
||||
New symbols/files created here (exclude from drift verification):
|
||||
- `calendar_events.hasRrule` Drizzle column + `idx_calendar_events_has_rrule` index
|
||||
- `calendars.isShared` Drizzle column
|
||||
- `devAuthBypass` (function) — apps/api/src/auth/devBypass.ts
|
||||
- `DEV_USER` (const, internal to devBypass.ts)
|
||||
- apps/pwa/vitest.config.ts (new)
|
||||
- apps/pwa `test` npm script + vitest/@testing-library/jsdom devDependencies
|
||||
- apps/api/tests/fixtures/{weekly-dst,allday-birthday,exdate-series}.ics
|
||||
- apps/api/tests/broker/expand.test.ts, apps/api/tests/routes/events.test.ts, apps/api/tests/auth/devBypass.test.ts (new test files)
|
||||
- apps/pwa/src/lib/hydrateEvents.test.ts, apps/pwa/src/lib/calendarConfig.test.ts (new test files)
|
||||
</artifacts_produced>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/02-calendar-display/02-01-SUMMARY.md` when done
|
||||
</output>
|
||||
Reference in New Issue
Block a user