Files
tessera-ctl/.planning/phases/05-dashboard-calendar/05-04-PLAN.md
T

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
05-dashboard-calendar 04 execute 4
05-01
05-02
05-03
apps/web/src/components/dashboard/widgets/calendar-widget.tsx
apps/web/src/components/dashboard/widget-registry.ts
apps/web/src/lib/calendar-api.ts
apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx
apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx
apps/web/src/components/settings/calendar-settings-panel.tsx
apps/web/src/components/settings/calendar-source-form.tsx
apps/web/src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx
true
DASH-05
CAL-02
CAL-03
truths artifacts key_links
User can configure CalDAV, Exchange, and ICS calendar sources in Settings > Dashboard > Kalender
User can toggle which calendar sources are visible in the widget
Calendar widget shows upcoming events aggregated from selected sources
path provides
apps/web/src/components/dashboard/widgets/calendar-widget.tsx Upcoming-events list widget
path provides
apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx Calendar source management UI
path provides
apps/web/src/lib/calendar-api.ts Calendar API client (sources CRUD + events)
from to via pattern
apps/web/src/components/dashboard/widgets/calendar-widget.tsx /api/calendar/events fetch in effect calendar/events
from to via pattern
apps/web/src/components/settings/calendar-settings-panel.tsx /api/calendar/sources updateSource visibility toggle isVisible
Deliver the calendar frontend slice: a user manages CalDAV/Exchange/ICS calendar sources in Settings > Dashboard > Kalender, toggles which sources are visible, and the Calendar widget on the dashboard shows upcoming events aggregated and normalized across all visible sources (read-only). All data flows through the calendar backend built in 05-03 — the browser never fetches external calendars directly.

Purpose: Implements DASH-05 (calendar widget), CAL-02 (visibility selection), and CAL-03 (event previews for selected sources). The matching CAL-01 source integration + event aggregation backend ships in 05-03; this plan consumes it.

Output: A working calendar widget plus a source-management settings page wired to the 05-03 backend.

<artifacts_this_phase_produces> Symbols created by THIS plan (exclude from drift verification — they are new):

Frontend components: CalendarWidget, CalendarSettingsPanel, CalendarSourceForm Frontend modules: calendar-api.ts (fetchSources/addSource/updateSource/deleteSource/testSource/fetchEvents); registry wiring of real CalendarWidget

Consumed (built in 05-03, do NOT redefine): GET /api/calendar/events, GET|POST|PATCH|DELETE /api/calendar/sources, POST /api/calendar/sources/:id/test, the CalendarEvent shape. </artifacts_this_phase_produces>

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/05-dashboard-calendar/05-CONTEXT.md @.planning/phases/05-dashboard-calendar/05-RESEARCH.md @.planning/phases/05-dashboard-calendar/05-PATTERNS.md @.planning/phases/05-dashboard-calendar/05-UI-SPEC.md @.planning/phases/05-dashboard-calendar/05-01-SUMMARY.md @.planning/phases/05-dashboard-calendar/05-02-SUMMARY.md @.planning/phases/05-dashboard-calendar/05-03-SUMMARY.md Task 1: Calendar API client + calendar widget apps/web/src/lib/calendar-api.ts, apps/web/src/components/dashboard/widgets/calendar-widget.tsx, apps/web/src/components/dashboard/widget-registry.ts, apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx - apps/web/src/components/dashboard/widget-registry.ts (replace calendar placeholder with real CalendarWidget) - apps/web/src/components/dashboard/widgets/clock-widget.tsx (WidgetProps pattern) - .planning/phases/05-dashboard-calendar/05-03-SUMMARY.md (CalendarEvent shape + GET /api/calendar/events contract from the backend plan) - .planning/phases/05-dashboard-calendar/05-RESEARCH.md lines 287-310 (CalendarEvent interface), lines 364-412 (anti-pattern: never fetch calendars in the browser) - .planning/phases/05-dashboard-calendar/05-UI-SPEC.md lines 110, 206-209 (calendar widget spec: time/title/source color dot; empty-state copy) - Test (calendar-widget.test.tsx): given a mocked `/api/calendar/events` response with two events, the widget renders both titles, their times, and a source color dot per event - given an empty events response, renders t('widgets.calendarEmptyNoEvents') - given a no-sources response, renders t('widgets.calendarEmptyNoSources') Create `calendar-api.ts`: fetchSources, addSource, updateSource (incl. isVisible toggle), deleteSource, testSource, fetchEvents — all `credentials:'include'`. Type the returns against the `CalendarEvent` and source shapes documented in 05-03-SUMMARY (do not redefine the backend contract — mirror it).
Create `calendar-widget.tsx` (DASH-05, D-10 read-only): `'use client'`. On mount fetch /api/calendar/events via calendar-api.fetchEvents. Render an upcoming-events list: each row shows time (locale-formatted), title, and an 8px source color dot (UI-SPEC color palette). Three states per UI-SPEC copy: no sources configured → t('widgets.calendarEmptyNoSources'); sources but no events → t('widgets.calendarEmptyNoEvents'); events → list. Loading state while fetching. Refresh periodically (every 5 min to match the backend cache TTL). Never fetch external calendars directly from the browser (RESEARCH anti-pattern — always via /api/calendar/events).

Update `widget-registry.ts`: replace the calendar placeholder component with the real CalendarWidget. Keep WIDGET_CONSTRAINTS.

Write calendar-widget.test.tsx per <behavior> with mocked fetch.
cd apps/web && pnpm vitest run src/components/dashboard/widgets/calendar-widget.test.tsx && pnpm exec tsc --noEmit - calendar-api.ts exports fetchSources/addSource/updateSource/deleteSource/testSource/fetchEvents, all using `credentials:'include'` - calendar-widget.tsx fetches `/api/calendar/events` and renders three distinct empty/list states - calendar-widget.tsx renders a source color dot per event row - widget-registry.ts references the real `CalendarWidget` - calendar-widget.test.tsx exits 0; `pnpm exec tsc --noEmit` exits 0 Calendar widget shows aggregated upcoming events from visible sources, cached refresh, read-only, with correct empty states. Task 2: Calendar settings page (source management + visibility) apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx, apps/web/src/components/settings/calendar-settings-panel.tsx, apps/web/src/components/settings/calendar-source-form.tsx, apps/web/src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx - apps/web/src/app/(portal)/settings/layout.tsx (from 05-01 — nested settings layout) - apps/web/src/components/settings/settings-sidebar.tsx (from 05-01 — this page is the "Kalender" sub-item target) - apps/web/src/lib/calendar-api.ts (from Task 1 of this plan) - .planning/phases/05-dashboard-calendar/05-UI-SPEC.md lines 120, 161-166, 204-209, 216-220, 256-261 (calendar settings panel spec, source form fields, connection test, delete confirmation, copy) - Test (calendar-settings.test.tsx): rendering CalendarSettingsPanel with two mocked sources lists both with name + type badge + visibility toggle - toggling a source calls updateSource with the new isVisible (CAL-02) - the add form requires name+type+url before enabling submit Create `settings/dashboard/calendar/page.tsx` (D-11): `'use client'`, renders CalendarSettingsPanel. Title t('settings.categoryCalendar').
Create `calendar-settings-panel.tsx`: fetch sources (calendar-api.fetchSources). Render a source list — each row: color dot, name, type badge (CalDAV/Exchange/ICS), visibility toggle switch (CAL-02 — calls updateSource({isVisible})), connection-status indicator (green check / orange warning from lastSyncError per UI-SPEC), edit + delete actions. Delete uses a confirmation dialog (UI-SPEC destructive: heading + body + "Quelle loeschen"/"Abbrechen"). "Quelle hinzufuegen" button reveals CalendarSourceForm. Empty state: t('settings.calendarEmpty') (UI-SPEC copy).

Create `calendar-source-form.tsx`: fields Name (required), Type (select CalDAV/Exchange/ICS, required), when Exchange show an Exchange-mode select (Exchange Online=graph / Exchange Server=ews, RESEARCH open question 2), URL (required, https validation client-side), Username (optional, hidden for ICS), Password (password input, optional, hidden for ICS), Color (from the 8-color palette in UI-SPEC). On save call addSource (or updateSource when editing); then auto-run testSource and show connection-success/error toast (UI-SPEC copy). Validate URL is https before submit.

Write calendar-settings.test.tsx per <behavior> with mocked calendar-api.
cd apps/web && pnpm vitest run "src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx" && pnpm exec tsc --noEmit - settings/dashboard/calendar/page.tsx renders CalendarSettingsPanel - calendar-settings-panel.tsx has a visibility toggle calling updateSource with isVisible - calendar-source-form.tsx shows an Exchange-mode select only when type is exchange, and hides username/password for ICS - calendar-source-form.tsx validates https before submit - delete uses a confirmation dialog (contains the "Quelle loeschen"/"Delete source" CTA) - calendar-settings.test.tsx exits 0; `pnpm exec tsc --noEmit` exits 0 Users manage CalDAV/Exchange/ICS sources, toggle widget visibility, test connections, and delete with confirmation.

<threat_model>

Trust Boundaries

Boundary Description
Browser → Calendar API Frontend submits source config (incl. credentials) to the 05-03 backend; never fetches external calendars directly

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-05-14 Tampering / SSRF calendar-source-form URL field mitigate Client-side https-only validation before submit; authoritative SSRF + https enforcement lives in the 05-03 backend DTO
T-05-15 Information Disclosure calendar widget render mitigate Widget consumes /api/calendar/events only; passwords never reach the browser (backend GET excludes encryptedPassword)
</threat_model>
- `cd apps/web && pnpm exec tsc --noEmit` exits 0 - `cd apps/web && pnpm vitest run src/components/dashboard/widgets/calendar-widget.test.tsx "src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx"` green

<success_criteria>

  • User manages CalDAV, ICS, and Exchange sources in Settings > Dashboard > Kalender
  • User toggles source visibility; only visible sources feed the widget (CAL-02/CAL-03)
  • Calendar widget shows aggregated upcoming events with source color dots, read-only (DASH-05)
  • Browser never fetches external calendars directly — all data via /api/calendar/events </success_criteria>
Create `.planning/phases/05-dashboard-calendar/05-04-SUMMARY.md` when done