src/lib/dev-login-config.ts — startup assertion: throws if NODE_ENV=production + ENABLE_DEV_LOGIN=true, scoped to runtime (skipped during next build).
Container
scripts/migrate.mjs — runs Drizzle migrations against DATABASE_URL.
deploy/docker-entrypoint.sh — runs migrations then exec node server.js. Skip with RUN_MIGRATIONS=false.
Dockerfile — copies drizzle/, scripts/migrate.mjs, entrypoint into runner stage; ENTRYPOINT now points at the script.
Compose
deploy/compose.yaml — famapp now image: ${FAMAPP_IMAGE:-ghcr.io/ginnoir/famapp:latest} (build still works locally as fallback). Authentik pinned via AUTHENTIK_IMAGE_TAG (default 2024.12.3). New RUN_MIGRATIONS env passed through.
.env.production.example — documents FAMAPP_IMAGE, AUTHENTIK_IMAGE_TAG, RUN_MIGRATIONS.
CI/CD
.github/workflows/ci.yml — push/PR: typecheck + lint + format:check + build.
.github/workflows/release.yml — v* tag: build + push ghcr.io/ginnoir/famapp:vX.Y.Z, :X.Y, :latest to GHCR.
Docs
deploy/README.md — full deploy/rollback/release runbook.
CHANGELOG.md — release log seeded with an Unreleased entry.
docs/tasks/09-pre-deploy-checklist.md — task 09 reframed from one-shot removal to a recurring pre-deploy checklist.
STATUS.md — updated.
Verified: pnpm typecheck, pnpm format, pnpm build, and docker compose config all clean.
105 lines
4.7 KiB
Markdown
105 lines
4.7 KiB
Markdown
# 10 — Calendar module
|
|
|
|
## Goal
|
|
|
|
Implement the `calendar` module: first-class **calendars** (multiple per household, each with its own visibility) and **events** that belong to a calendar. Month/week/day views with CRUD.
|
|
|
|
## Why
|
|
|
|
Calendars are entities, not a singleton concept. A user might run a _Personal_ (private) calendar, a _Family_ (household-shared) calendar, and a _Wedding planning_ calendar that gets share-linked publicly. The calendar module ships that abstraction; widgets and the share-link service then reuse it generically.
|
|
|
|
## Depends on
|
|
|
|
- 04 (module loader + widget contract), 07 (household), 08 (theming)
|
|
|
|
## Scope
|
|
|
|
### Schema (`src/modules/calendar/schema.ts`)
|
|
|
|
`calendars`:
|
|
|
|
- `id` uuid pk
|
|
- `household_id` fk
|
|
- `owner_id` fk users
|
|
- `name` text
|
|
- `color` text nullable
|
|
- `visibility` text — `'private' | 'household'`. Check constraint.
|
|
- `created_at`, `updated_at`
|
|
|
|
`calendar_events`:
|
|
|
|
- `id` uuid pk
|
|
- `calendar_id` fk calendars (events inherit household scope through the calendar)
|
|
- `title` text
|
|
- `start_at` timestamptz, `end_at` timestamptz
|
|
- `all_day` boolean
|
|
- `location` text nullable
|
|
- `notes` text nullable
|
|
- `owner_id` fk users
|
|
- `rrule` text nullable (reserved; no UI)
|
|
- `external_source` text nullable, `external_id` text nullable (reserved)
|
|
- `created_at`, `updated_at`
|
|
|
|
Index on `(calendar_id, start_at)` for range queries.
|
|
|
|
### Seed (in `pnpm db:seed`, idempotent)
|
|
|
|
- One `Home` calendar per household, `visibility = 'household'`.
|
|
- One `Personal` calendar per user on first login, `visibility = 'private'`.
|
|
|
|
### Visibility helper
|
|
|
|
`canSeeCalendar(userId, calendarId)`:
|
|
|
|
- `visibility = 'household'` → must be a member of the calendar's household
|
|
- `visibility = 'private'` → must be the calendar's `owner_id`
|
|
|
|
All event reads go through this. Cross-leakage tests prove it.
|
|
|
|
### Server (`src/modules/calendar/server/`)
|
|
|
|
- `listCalendars()` — every calendar the current user can see.
|
|
- `createCalendar`, `renameCalendar`, `setCalendarVisibility`, `setCalendarColor`, `deleteCalendar`.
|
|
- `listEvents({ from, to, calendarIds })` — `calendarIds: 'all' | string[]`, where `'all'` resolves to every calendar the user can see; explicit ids are filtered against the visibility helper.
|
|
- `createEvent`, `updateEvent`, `deleteEvent` — server actions, validate with Zod. `createEvent` requires a `calendarId` the user can write to.
|
|
|
|
### UI (`src/modules/calendar/components/`)
|
|
|
|
- `/calendar` page with month / week / day toggle. Recommended lib: **FullCalendar** (`@fullcalendar/react` + day/week/month plugins).
|
|
- Calendar list in a sidebar with show/hide toggles per calendar, color swatches, "+ new calendar" button. The visible set is local view state (not persisted yet — separate later concern).
|
|
- Click a day cell → create-event dialog (calendar selector defaults to last-used). Click an event → edit dialog. Drag-resize updates `end_at`.
|
|
- `/calendar/manage` (or modal from sidebar) — create / rename / recolor / change visibility / delete calendars.
|
|
|
|
### Manifest
|
|
|
|
Entity types:
|
|
|
|
- `calendar.calendar` — shareable (read by default; writes via share token possible later), searchable (name).
|
|
- `calendar.event` — shareable (read-only by default), remindable, searchable (title + notes + location).
|
|
|
|
Widgets (every widget configurable per the task 04 contract):
|
|
|
|
- **`calendar.upcoming`** — `config: { calendarIds: "all" | string[]; days: number }`. Default `{ calendarIds: "all", days: 3 }`. `resolveConfigOptions` returns `{ calendars: { id, name, visibility }[] }`.
|
|
- **`calendar.month`** — `config: { calendarIds: "all" | string[] }`. Larger default size.
|
|
|
|
Quick-add: "New event" → opens create dialog with the user's last-used calendar pre-selected. "New calendar" → calendar create dialog.
|
|
|
|
Nav: `/calendar`.
|
|
|
|
## Out of scope
|
|
|
|
- Recurrence UI (rrule column reserved, ignored on read).
|
|
- External calendar sync.
|
|
- Free/busy aggregation across users.
|
|
- Per-user persisted "which calendars are shown" preference (local view state for now).
|
|
- Per-calendar write-share via share token (basic share link is enough for v1).
|
|
|
|
## Acceptance criteria
|
|
|
|
- [ ] Default seeds create one `Home` (household) calendar and one `Personal` (private) calendar per user, idempotently.
|
|
- [ ] CRUD for both calendars and events works end-to-end with optimistic updates.
|
|
- [ ] Private calendars are invisible to other household members in queries, in the sidebar, and in widgets.
|
|
- [ ] `listEvents({ calendarIds: 'all' })` returns events from all visible calendars; explicit ids cannot be used to leak across visibility.
|
|
- [ ] Both widgets register with the shape required by task 04 — a config schema, a default config, and a `resolveConfigOptions` adapter.
|
|
- [ ] One Playwright happy-path test: create calendar → create event in it → see on calendar → edit → delete event → delete calendar.
|