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.
4.7 KiB
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:
iduuid pkhousehold_idfkowner_idfk usersnametextcolortext nullablevisibilitytext —'private' | 'household'. Check constraint.created_at,updated_at
calendar_events:
iduuid pkcalendar_idfk calendars (events inherit household scope through the calendar)titletextstart_attimestamptz,end_attimestamptzall_daybooleanlocationtext nullablenotestext nullableowner_idfk usersrruletext nullable (reserved; no UI)external_sourcetext nullable,external_idtext nullable (reserved)created_at,updated_at
Index on (calendar_id, start_at) for range queries.
Seed (in pnpm db:seed, idempotent)
- One
Homecalendar per household,visibility = 'household'. - One
Personalcalendar per user on first login,visibility = 'private'.
Visibility helper
canSeeCalendar(userId, calendarId):
visibility = 'household'→ must be a member of the calendar's householdvisibility = 'private'→ must be the calendar'sowner_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.createEventrequires acalendarIdthe user can write to.
UI (src/modules/calendar/components/)
/calendarpage 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 }.resolveConfigOptionsreturns{ 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 onePersonal(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
resolveConfigOptionsadapter. - One Playwright happy-path test: create calendar → create event in it → see on calendar → edit → delete event → delete calendar.