wip: phase 05 plan-phase paused at revision verify (checker v2 pending)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 09:54:43 +02:00
parent c8852d2014
commit e60d34dc28
6 changed files with 823 additions and 59 deletions
@@ -0,0 +1,84 @@
---
context: phase
phase: 05-dashboard-calendar
task: null
total_tasks: null
status: planning_revision
last_updated: 2026-06-24T07:53:36.605Z
---
<current_state>
Phase 05 plan-phase workflow, mid-revision loop (iteration 1/3).
Planner created initial 4 plans → checker found 2 blockers → planner revised (split 05-03) → produced 5 plans on disk. Checker has NOT yet verified revised plans.
5 plans on disk:
- 05-01-PLAN.md — Dashboard foundation: Prisma models, CRUD API, grid, edit-mode, clock widget, settings shell (Wave 1) [DASH-01/02/03/07]
- 05-02-PLAN.md — Search + Notes widgets, widget settings panel, SearchProvider backend (Wave 2) [DASH-04/06]
- 05-03-PLAN.md — Calendar backend: Prisma model, AES-256-GCM crypto, source CRUD, CalDAV/ICS/Exchange providers, DB push (Wave 3) [CAL-01/02/03, DASH-05]
- 05-04-PLAN.md — Calendar frontend: event aggregation, calendar widget, settings UI (Wave 3, depends on 05-03) [CAL-01/02/03, DASH-05]
- 05-05-PLAN.md — Visual verification checkpoint (Wave 4) [all 10 reqs]
</current_state>
<completed_work>
Session Work (2026-06-23):
- UI-SPEC.md: Created and verified (6/6 dimensions passed, 1 revision for typography)
- RESEARCH.md: Created (react-grid-layout v2, tsdav/node-ical/ews for calendar, @uiw/react-md-editor for notes, AES-256-GCM for credential encryption)
- VALIDATION.md: Draft created
- PATTERNS.md: Created (30 files classified, 24 analogs found)
- Initial 4 plans: Created by gsd-planner (opus)
- Plan checker v1: Found 2 blockers (05-03 missing requirements frontmatter, 05-03 scope 5 tasks/20 files too large)
- Planner revision: Split 05-03 into backend (05-03) + frontend (05-04), renamed old 05-04 verification to 05-05
</completed_work>
<remaining_work>
Plan-phase workflow steps remaining:
1. Re-run gsd-plan-checker on all 5 revised plans (step 10)
2. Handle checker return (step 11)
3. If passes → requirements coverage gate (step 13)
4. Decision coverage gate (step 13a)
5. Record planning in STATE.md (step 13b)
6. Annotate ROADMAP wave dependencies (step 13c)
7. Commit all plans (step 13d)
8. Post-planning gap analysis (step 13e)
9. Present final status and next steps (step 14)
</remaining_work>
<decisions_made>
- Split 05-03 into backend/frontend plans — checker found 5 tasks/20 files in complex calendar domain exceeded scope sanity
- MVP vertical-slice mode — per ROADMAP **Mode:** mvp
- UI-SPEC: 4 font sizes (12/14/18/28px), 2 weights (400/600) — reduced from 7 sizes/3 weights after checker blocked
- Schema push: [BLOCKING] Prisma db push task injected in each schema-modifying plan
- Security: ASVS L1 threat models required in each plan
</decisions_made>
<blockers>
None — revision produced files, just needs verification pass.
</blockers>
## Required Reading (in order)
1. `.planning/phases/05-dashboard-calendar/05-CONTEXT.md` — 22 locked decisions (D-01..D-22)
2. `.planning/phases/05-dashboard-calendar/05-RESEARCH.md` — Tech stack decisions, library choices
3. `.planning/phases/05-dashboard-calendar/05-UI-SPEC.md` — Visual/interaction contracts
4. `.planning/phases/05-dashboard-calendar/05-*-PLAN.md` — All 5 plan files
5. `$HOME/.claude/gsd-core/workflows/plan-phase.md` — Workflow definition (steps 10-14 remain)
## Infrastructure State
- No servers running
- No background processes
- Docker stack not started for this phase yet
<context>
Plan-phase orchestrator role. Was executing the plan-phase workflow from gsd-core/workflows/plan-phase.md.
Revision iteration 1 of max 3. prev_issue_count=2 (blockers). stall_reentry_count=0.
The planner agent hit its session token limit during revision but still wrote the split files to disk successfully.
Uncommitted files: 05-04-PLAN.md (modified), 05-05-PLAN.md (new), 05-PATTERNS.md (new), 05-VALIDATION.md (new).
</context>
<next_action>
Start with: Spawn gsd-plan-checker agent on all 5 plans (.planning/phases/05-dashboard-calendar/05-0[1-5]-PLAN.md). If verification passes, proceed through steps 13→14 to finalize planning. If issues found, iterate (revision count 2/3).
Alternatively: run `/gsd-plan-phase 05` which will detect existing plans and offer to continue.
</next_action>
@@ -2,28 +2,57 @@
phase: 05-dashboard-calendar
plan: 04
type: execute
wave: 4
wave: 3
depends_on: ["05-01", "05-02", "05-03"]
files_modified: []
autonomous: false
requirements: [DASH-01, DASH-02, DASH-03, DASH-04, DASH-05, DASH-06, DASH-07, CAL-01, CAL-02, CAL-03]
files_modified:
- 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
autonomous: true
requirements: [DASH-05, CAL-02, CAL-03]
must_haves:
truths:
- "Human confirms the dashboard grid, all four widgets, settings, and calendar integration work end-to-end"
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"
artifacts:
- path: "apps/web/src/components/dashboard/widgets/calendar-widget.tsx"
provides: "Upcoming-events list widget"
- path: "apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx"
provides: "Calendar source management UI"
- path: "apps/web/src/lib/calendar-api.ts"
provides: "Calendar API client (sources CRUD + events)"
key_links:
- from: "apps/web/src/components/dashboard/widgets/calendar-widget.tsx"
to: "/api/calendar/events"
via: "fetch in effect"
pattern: "calendar/events"
- from: "apps/web/src/components/settings/calendar-settings-panel.tsx"
to: "/api/calendar/sources"
via: "updateSource visibility toggle"
pattern: "isVisible"
---
<objective>
Final human verification of the complete Phase 05 dashboard & calendar experience. All implementation is automated in plans 05-01 through 05-03; this plan pauses for the user to visually and functionally confirm the full flow before the phase closes.
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: Catch visual/interaction regressions that automated tests cannot (drag feel, theme correctness, real calendar fetch). Closes the phase against ROADMAP success criteria 1-5.
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: Human sign-off (or a gap list to feed `/gsd-plan-phase --gaps`).
Output: A working calendar widget plus a source-management settings page wired to the 05-03 backend.
</objective>
<artifacts_this_phase_produces>
No new symbols — verification-only plan.
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>
@@ -32,7 +61,12 @@ No new symbols — verification-only plan.
</execution_context>
<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
@@ -41,60 +75,78 @@ No new symbols — verification-only plan.
<tasks>
<task type="auto">
<name>Task 1: Pre-flight — start stack and run full test suite</name>
<files></files>
<task type="auto" tdd="true">
<name>Task 1: Calendar API client + calendar widget</name>
<files>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</files>
<read_first>
- .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
- 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)
</read_first>
<behavior>
- 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')
</behavior>
<action>
Confirm the full Docker Compose stack is running (web + api + postgres). Run the complete web test suite and the api type-check to confirm the phase is green before asking the human to verify. If anything fails, report it and do NOT proceed to the human checkpoint. Ensure CALENDAR_ENCRYPTION_KEY is set in the api environment so calendar endpoints respond.
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.
</action>
<verify>
<automated>cd apps/web && pnpm test && cd ../api && npx tsc --noEmit</automated>
<automated>cd apps/web && pnpm vitest run src/components/dashboard/widgets/calendar-widget.test.tsx && pnpm exec tsc --noEmit</automated>
</verify>
<acceptance_criteria>
- `cd apps/web && pnpm test` exits 0 (full suite green)
- `cd apps/api && npx tsc --noEmit` exits 0
- Docker stack reachable (web responds, api /health responds)
- 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
</acceptance_criteria>
<done>Full automated suite green and stack running; ready for human verification.</done>
<done>Calendar widget shows aggregated upcoming events from visible sources, cached refresh, read-only, with correct empty states.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Complete Phase 05 dashboard & calendar: a configurable drag-and-drop widget grid as the portal start page, four widgets (Clock, Search, Notes, Calendar), per-user layout persistence, a Settings page (via avatar menu) with widget config and calendar source management, and multi-protocol calendar integration (CalDAV / Exchange / ICS).
</what-built>
<how-to-verify>
Open the portal in a browser (logged in as a normal user).
<task type="auto" tdd="true">
<name>Task 2: Calendar settings page (source management + visibility)</name>
<files>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</files>
<read_first>
- 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)
</read_first>
<behavior>
- 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
</behavior>
<action>
Create `settings/dashboard/calendar/page.tsx` (D-11): `'use client'`, renders CalendarSettingsPanel. Title t('settings.categoryCalendar').
Dashboard grid + persistence (DASH-01/02/07):
1. Confirm the start page shows the empty-state ("Keine Widgets aktiv") with a visible pencil edit button.
2. Click the pencil (top-right) — grid lines appear, "Widget hinzufuegen" button appears.
3. Add each widget type from the catalog modal (Clock, Search, Notes, Calendar). Confirm all four appear.
4. In edit mode, drag a widget to a new position and resize it. Confirm snapping + reflow.
5. Click the checkmark to exit edit mode. Reload the page — confirm the layout persists exactly (DASH-07).
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).
Clock (DASH-03): confirm it ticks. In Settings > Dashboard > Widgets, set a timezone and toggle date — confirm the widget updates.
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.
Search (DASH-04): pick a provider (Google/Bing/DuckDuckGo), type a query, press Enter/click — confirm a new browser tab opens the correct search. Add a custom provider in Settings and confirm it appears in the dropdown.
Notes (DASH-06): type Markdown (bold, checkbox list). Confirm live rendering + toolbar. Wait ~1s, reload — confirm content persisted (autosave). Set a custom title in Settings.
Settings (D-19/D-20): open via the avatar menu (NOT the sidebar). Confirm the sub-sidebar with Dashboard > Widgets / Kalender and the "Zurueck zum Dashboard" link.
Calendar (CAL-01/02/03, DASH-05): in Settings > Dashboard > Kalender add a real ICS source (e.g. a public .ics URL). Confirm connection success. Confirm the Calendar widget lists upcoming events with source color dots. Toggle the source's visibility off — confirm its events disappear from the widget. (CalDAV/Exchange: test if you have credentials; ICS is the minimum.)
Cross-cutting:
- Toggle dark/light theme — confirm all widgets (esp. Notes Markdown editor) render correctly in both.
- Switch DE/EN — confirm all dashboard/settings strings translate (no raw keys).
- Resize the browser narrow (<768px) — confirm widgets stack vertically (D-22).
Report any visual or functional issue; otherwise approve.
</how-to-verify>
<resume-signal>Type "approved" if everything works, or describe each issue found.</resume-signal>
Write calendar-settings.test.tsx per <behavior> with mocked calendar-api.
</action>
<verify>
<automated>cd apps/web && pnpm vitest run "src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx" && pnpm exec tsc --noEmit</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>Users manage CalDAV/Exchange/ICS sources, toggle widget visibility, test connections, and delete with confirmation.</done>
</task>
</tasks>
@@ -102,26 +154,32 @@ No new symbols — verification-only plan.
<threat_model>
## Trust Boundaries
No new trust boundaries — verification-only plan; all enforcement was implemented and threat-modeled in plans 05-01 through 05-03.
| 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-V1 | Information Disclosure | manual calendar source test | accept | Human uses own test credentials in a dev stack; no production data |
| 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>
<verification>
- Full web test suite green (Task 1)
- API type-check green (Task 1)
- Human confirms all ROADMAP Phase 05 success criteria 1-5
- `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
</verification>
<success_criteria>
- Human approves the complete dashboard + calendar experience, OR
- A concrete gap list is produced for `/gsd-plan-phase 05 --gaps`
- 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>
<output>
Create `.planning/phases/05-dashboard-calendar/05-04-SUMMARY.md` when done
</output>
</content>
</invoke>
@@ -0,0 +1,131 @@
---
phase: 05-dashboard-calendar
plan: 05
type: execute
wave: 4
depends_on: ["05-01", "05-02", "05-03", "05-04"]
files_modified: []
autonomous: false
requirements: [DASH-01, DASH-02, DASH-03, DASH-04, DASH-05, DASH-06, DASH-07, CAL-01, CAL-02, CAL-03]
must_haves:
truths:
- "Human confirms the dashboard grid, all four widgets, settings, and calendar integration work end-to-end"
artifacts: []
key_links: []
---
<objective>
Final human verification of the complete Phase 05 dashboard & calendar experience. All implementation is automated in plans 05-01 through 05-04; this plan pauses for the user to visually and functionally confirm the full flow before the phase closes.
Purpose: Catch visual/interaction regressions that automated tests cannot (drag feel, theme correctness, real calendar fetch). Closes the phase against ROADMAP success criteria 1-5.
Output: Human sign-off (or a gap list to feed `/gsd-plan-phase --gaps`).
</objective>
<artifacts_this_phase_produces>
No new symbols — verification-only plan.
</artifacts_this_phase_produces>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/ROADMAP.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
@.planning/phases/05-dashboard-calendar/05-04-SUMMARY.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Pre-flight — start stack and run full test suite</name>
<files></files>
<read_first>
- .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
- .planning/phases/05-dashboard-calendar/05-04-SUMMARY.md
</read_first>
<action>
Confirm the full Docker Compose stack is running (web + api + postgres). Run the complete web test suite and the api type-check to confirm the phase is green before asking the human to verify. If anything fails, report it and do NOT proceed to the human checkpoint. Ensure CALENDAR_ENCRYPTION_KEY is set in the api environment so calendar endpoints respond.
</action>
<verify>
<automated>cd apps/web && pnpm test && cd ../api && npx tsc --noEmit</automated>
</verify>
<acceptance_criteria>
- `cd apps/web && pnpm test` exits 0 (full suite green)
- `cd apps/api && npx tsc --noEmit` exits 0
- Docker stack reachable (web responds, api /health responds)
</acceptance_criteria>
<done>Full automated suite green and stack running; ready for human verification.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Complete Phase 05 dashboard & calendar: a configurable drag-and-drop widget grid as the portal start page, four widgets (Clock, Search, Notes, Calendar), per-user layout persistence, a Settings page (via avatar menu) with widget config and calendar source management, and multi-protocol calendar integration (CalDAV / Exchange / ICS).
</what-built>
<how-to-verify>
Open the portal in a browser (logged in as a normal user).
Dashboard grid + persistence (DASH-01/02/07):
1. Confirm the start page shows the empty-state ("Keine Widgets aktiv") with a visible pencil edit button.
2. Click the pencil (top-right) — grid lines appear, "Widget hinzufuegen" button appears.
3. Add each widget type from the catalog modal (Clock, Search, Notes, Calendar). Confirm all four appear.
4. In edit mode, drag a widget to a new position and resize it. Confirm snapping + reflow.
5. Click the checkmark to exit edit mode. Reload the page — confirm the layout persists exactly (DASH-07).
Clock (DASH-03): confirm it ticks. In Settings > Dashboard > Widgets, set a timezone and toggle date — confirm the widget updates.
Search (DASH-04): pick a provider (Google/Bing/DuckDuckGo), type a query, press Enter/click — confirm a new browser tab opens the correct search. Add a custom provider in Settings and confirm it appears in the dropdown.
Notes (DASH-06): type Markdown (bold, checkbox list). Confirm live rendering + toolbar. Wait ~1s, reload — confirm content persisted (autosave). Set a custom title in Settings.
Settings (D-19/D-20): open via the avatar menu (NOT the sidebar). Confirm the sub-sidebar with Dashboard > Widgets / Kalender and the "Zurueck zum Dashboard" link.
Calendar (CAL-01/02/03, DASH-05): in Settings > Dashboard > Kalender add a real ICS source (e.g. a public .ics URL). Confirm connection success. Confirm the Calendar widget lists upcoming events with source color dots. Toggle the source's visibility off — confirm its events disappear from the widget. (CalDAV/Exchange: test if you have credentials; ICS is the minimum.)
Cross-cutting:
- Toggle dark/light theme — confirm all widgets (esp. Notes Markdown editor) render correctly in both.
- Switch DE/EN — confirm all dashboard/settings strings translate (no raw keys).
- Resize the browser narrow (<768px) — confirm widgets stack vertically (D-22).
Report any visual or functional issue; otherwise approve.
</how-to-verify>
<resume-signal>Type "approved" if everything works, or describe each issue found.</resume-signal>
</task>
</tasks>
<threat_model>
## Trust Boundaries
No new trust boundaries — verification-only plan; all enforcement was implemented and threat-modeled in plans 05-01 through 05-04.
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-05-V1 | Information Disclosure | manual calendar source test | accept | Human uses own test credentials in a dev stack; no production data |
</threat_model>
<verification>
- Full web test suite green (Task 1)
- API type-check green (Task 1)
- Human confirms all ROADMAP Phase 05 success criteria 1-5
</verification>
<success_criteria>
- Human approves the complete dashboard + calendar experience, OR
- A concrete gap list is produced for `/gsd-plan-phase 05 --gaps`
</success_criteria>
<output>
Create `.planning/phases/05-dashboard-calendar/05-05-SUMMARY.md` when done
</output>
</content>
</invoke>
@@ -0,0 +1,366 @@
# Phase 5: Dashboard & Calendar - Pattern Map
**Mapped:** 2026-06-23
**Files analyzed:** 30
**Analogs found:** 24 / 30
## File Classification
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|-------------------|------|-----------|----------------|---------------|
| `apps/api/prisma/schema.prisma` (modify) | model | CRUD | selbst | exact |
| `apps/api/src/dashboard/dashboard.module.ts` | config | request-response | `apps/api/src/module-registry/module-registry.module.ts` | exact |
| `apps/api/src/dashboard/dashboard.controller.ts` | controller | CRUD | `apps/api/src/module-registry/module-registry.controller.ts` | exact |
| `apps/api/src/dashboard/dashboard.service.ts` | service | CRUD | `apps/api/src/module-registry/module-registry.service.ts` | exact |
| `apps/api/src/dashboard/dto/save-layout.dto.ts` | model | transform | `apps/api/src/domaincheck/dto/check-domain.dto.ts` | exact |
| `apps/api/src/dashboard/dto/update-widget-config.dto.ts` | model | transform | `apps/api/src/domaincheck/dto/check-domain.dto.ts` | exact |
| `apps/api/src/dashboard/dto/create-widget.dto.ts` | model | transform | `apps/api/src/domaincheck/dto/check-domain.dto.ts` | exact |
| `apps/api/src/calendar/calendar.module.ts` | config | request-response | `apps/api/src/module-registry/module-registry.module.ts` | exact |
| `apps/api/src/calendar/calendar.controller.ts` | controller | CRUD | `apps/api/src/module-registry/module-registry.controller.ts` | exact |
| `apps/api/src/calendar/calendar.service.ts` | service | CRUD | `apps/api/src/module-registry/module-registry.service.ts` | role-match |
| `apps/api/src/calendar/providers/caldav.provider.ts` | service | request-response | -- | no-analog |
| `apps/api/src/calendar/providers/ics.provider.ts` | service | request-response | -- | no-analog |
| `apps/api/src/calendar/providers/exchange.provider.ts` | service | request-response | -- | no-analog |
| `apps/api/src/calendar/dto/create-calendar-source.dto.ts` | model | transform | `apps/api/src/domaincheck/dto/check-domain.dto.ts` | exact |
| `apps/api/src/calendar/dto/calendar-events-query.dto.ts` | model | transform | `apps/api/src/domaincheck/dto/check-domain.dto.ts` | exact |
| `apps/web/src/app/(portal)/page.tsx` (modify) | component | request-response | selbst | exact |
| `apps/web/src/app/(portal)/settings/layout.tsx` | component | request-response | `apps/web/src/app/(portal)/layout.tsx` | role-match |
| `apps/web/src/app/(portal)/settings/page.tsx` | component | request-response | `apps/web/src/app/(portal)/page.tsx` | role-match |
| `apps/web/src/app/(portal)/settings/dashboard/page.tsx` | component | request-response | `apps/web/src/app/(portal)/page.tsx` | role-match |
| `apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx` | component | request-response | `apps/web/src/app/(portal)/page.tsx` | role-match |
| `apps/web/src/app/(portal)/settings/dashboard/search/page.tsx` | component | request-response | `apps/web/src/app/(portal)/page.tsx` | role-match |
| `apps/web/src/components/dashboard/dashboard-grid.tsx` | component | event-driven | -- | no-analog |
| `apps/web/src/components/dashboard/edit-mode-toggle.tsx` | component | event-driven | `apps/web/src/components/theme-toggle.tsx` | role-match |
| `apps/web/src/components/dashboard/widget-catalog-modal.tsx` | component | event-driven | -- | no-analog |
| `apps/web/src/components/dashboard/widgets/clock-widget.tsx` | component | event-driven | -- | no-analog |
| `apps/web/src/components/dashboard/widgets/search-widget.tsx` | component | request-response | -- | no-analog |
| `apps/web/src/components/dashboard/widgets/calendar-widget.tsx` | component | request-response | -- | no-analog |
| `apps/web/src/components/dashboard/widgets/note-widget.tsx` | component | event-driven | -- | no-analog |
| `apps/web/src/components/dashboard/widgets/widget-wrapper.tsx` | component | request-response | -- | no-analog |
| `apps/web/src/components/settings/settings-sidebar.tsx` | component | request-response | `apps/web/src/components/layout/sidebar.tsx` | role-match |
| `apps/web/src/lib/stores/dashboard-store.ts` | store | CRUD | `apps/web/src/lib/stores/sidebar-store.ts` | role-match |
| `apps/web/src/components/layout/header.tsx` (modify) | component | request-response | selbst | exact |
## Pattern Assignments
### NestJS Module Pattern (dashboard.module.ts, calendar.module.ts)
**Analog:** `apps/api/src/module-registry/module-registry.module.ts`
**Vollstaendiges Pattern** (Zeilen 1-22):
```typescript
import { Module } from '@nestjs/common';
import { ModuleRegistryController } from './module-registry.controller';
import { ModuleRegistryService } from './module-registry.service';
import { ModuleGuard } from './module.guard';
@Module({
controllers: [ModuleRegistryController],
providers: [ModuleRegistryService, ModuleGuard],
exports: [ModuleRegistryService, ModuleGuard],
})
export class ModuleRegistryModule {}
```
---
### NestJS Controller Pattern (dashboard.controller.ts, calendar.controller.ts)
**Analog:** `apps/api/src/module-registry/module-registry.controller.ts`
**Imports Pattern** (Zeilen 1-14):
```typescript
import {
Controller,
ForbiddenException,
Get,
Param,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { Role } from '@prisma/client';
import { Request } from 'express';
import { Roles } from '../auth/decorators/roles.decorator';
import { RolesGuard } from '../auth/guards/roles.guard';
```
**Tenant-Context Extraction** (Zeilen 48-53, wiederholt in allen Methoden):
```typescript
@Get('active')
async findActive(@Req() req: Request) {
const tenantId = (req as any).tenantId ?? (req as any).user?.tenantId;
if (!tenantId) {
throw new ForbiddenException('No tenant context');
}
return this.moduleRegistryService.findActiveForTenant(tenantId);
}
```
**Hinweis fuer Dashboard:** Dashboard-Endpoints brauchen `userId` zusaetzlich zu `tenantId`. Pattern: `const userId = (req as any).user?.id;`
---
### NestJS Service Pattern (dashboard.service.ts, calendar.service.ts)
**Analog:** `apps/api/src/module-registry/module-registry.service.ts`
**Imports + Constructor** (Zeilen 1-12):
```typescript
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class ModuleRegistryService {
constructor(private readonly prisma: PrismaService) {}
```
**CRUD Pattern - findMany** (Zeilen 17-21):
```typescript
async findAll() {
return this.prisma.module.findMany({
orderBy: { name: 'asc' },
});
}
```
**CRUD Pattern - upsert** (Zeilen 53-82):
```typescript
async activateForTenant(tenantId: string, moduleId: string) {
const moduleExists = await this.prisma.module.findUnique({
where: { id: moduleId },
});
if (!moduleExists) {
throw new NotFoundException(`Module with id '${moduleId}' not found`);
}
return this.prisma.tenantModuleActivation.upsert({
where: { tenantId_moduleId: { tenantId, moduleId } },
update: { isActive: true, activatedAt: new Date() },
create: { tenantId, moduleId, isActive: true },
include: { module: true },
});
}
```
---
### DTO Pattern (alle dto/*.dto.ts)
**Analog:** `apps/api/src/domaincheck/dto/check-domain.dto.ts`
**Vollstaendiges Pattern** (Zeilen 1-34):
```typescript
import {
ArrayMaxSize,
IsArray,
IsNotEmpty,
IsOptional,
IsString,
Matches,
} from 'class-validator';
export class CheckDomainDto {
@IsString()
@IsNotEmpty()
@Matches(/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, {
message: 'Domain must be a valid label...',
})
domain!: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
@ArrayMaxSize(10, { message: 'Maximum 10 TLDs per request' })
tlds?: string[];
}
```
**Wichtig fuer Dashboard DTOs:** `save-layout.dto.ts` braucht `IsObject` oder `IsJSON` fuer das JSONB layouts-Feld. `class-validator` hat `IsObject()` und `IsNotEmptyObject()`.
---
### Prisma Schema Pattern (schema.prisma erweitern)
**Analog:** `apps/api/prisma/schema.prisma` — bestehende Models
**Model mit JSONB + userId + tenantId** (Pattern aus User + LdapConfig):
```prisma
model LdapConfig {
id String @id @default(uuid())
tenantId String @unique
tenant Tenant @relation(fields: [tenantId], references: [id])
serverUrl String
// ... fields ...
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([tenantId])
}
```
**Neue Models folgen diesem Pattern:** `@id @default(uuid())`, `userId String`, `tenantId String`, `createdAt`/`updatedAt`, `@@index` auf userId und tenantId. JSONB-Felder als `Json` Typ.
---
### Zustand Store Pattern (dashboard-store.ts)
**Analog:** `apps/web/src/lib/stores/sidebar-store.ts`
**Store mit Persist** (Zeilen 1-21):
```typescript
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface SidebarState {
isCollapsed: boolean;
isMobileOpen: boolean;
toggle: () => void;
setMobileOpen: (open: boolean) => void;
}
export const useSidebarStore = create<SidebarState>()(
persist(
(set) => ({
isCollapsed: false,
isMobileOpen: false,
toggle: () => set((state) => ({ isCollapsed: !state.isCollapsed })),
setMobileOpen: (open) => set({ isMobileOpen: open }),
}),
{ name: 'tessera-sidebar' }
)
);
```
**Hinweis:** Dashboard-Store soll NICHT persist verwenden (Layout kommt aus DB, nicht LocalStorage). Einfacher Store ohne persist wie `marketplace-store.ts`:
```typescript
import { create } from 'zustand';
export const useMarketplaceStore = create<MarketplaceState>()((set) => ({
// ...
}));
```
---
### Next.js Page Pattern (alle settings pages)
**Analog:** `apps/web/src/app/(portal)/page.tsx`
**Client Component mit next-intl** (Zeilen 1-6):
```typescript
'use client';
import { useTranslations } from 'next-intl';
export default function DashboardPage() {
const t = useTranslations('dashboard');
```
**Tailwind + OKLCH Farbklassen** (Zeilen 9-11):
```typescript
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
<div className="rounded-lg bg-muted p-4">
<span className="text-muted-foreground">
```
---
### Next.js Layout Pattern (settings/layout.tsx)
**Analog:** `apps/web/src/app/(portal)/layout.tsx`
**Nested Layout** (Zeilen 1-13):
```typescript
import { AppShell } from '@/components/layout/app-shell';
export default function PortalLayout({
children,
}: {
children: React.ReactNode;
}) {
return <AppShell>{children}</AppShell>;
}
```
**Settings-Layout wird aehnlich:** Statt `<AppShell>` ein `<div className="flex h-full">` mit `<SettingsSidebar />` und Content-Bereich.
---
### Header Modification (Settings-Link hinzufuegen)
**Analog:** `apps/web/src/components/layout/header.tsx`
**Dropdown-Menu Einfuegepunkt** (Zeilen 130-155): Settings-Link kommt als neues Item zwischen User-Info Block und Logout-Button:
```typescript
{/* Logout button - Zeile 132 */}
<div className="px-2 py-1">
<button
onClick={() => { setDropdownOpen(false); logout(); }}
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-muted transition-colors"
>
```
**Pattern fuer neuen Settings-Link:** Gleiche CSS-Klassen wie Logout-Button, aber mit `<Link href="/settings">` statt `<button onClick={logout}>`. Einfuegen vor dem Logout-`<div>` (vor Zeile 132).
## Shared Patterns
### Tenant-Context Extraction
**Source:** `apps/api/src/module-registry/module-registry.controller.ts` Zeilen 48-53
**Apply to:** Alle Backend-Controller (dashboard.controller.ts, calendar.controller.ts)
```typescript
const tenantId = (req as any).tenantId ?? (req as any).user?.tenantId;
if (!tenantId) {
throw new ForbiddenException('No tenant context');
}
```
### User-Context Extraction (neu fuer Dashboard)
**Source:** Abgeleitet aus Auth-Pattern
**Apply to:** Alle Dashboard/Calendar-Endpoints (user-scoped, nicht tenant-scoped)
```typescript
const userId = (req as any).user?.id;
const tenantId = (req as any).tenantId ?? (req as any).user?.tenantId;
```
### API Fetch Pattern (Frontend)
**Source:** `apps/web/src/components/layout/header.tsx` Zeile 21 (fetchCurrentUser)
**Apply to:** Dashboard-Store, Widget-Komponenten, Settings-Seiten
```typescript
// Alle API-Calls nutzen credentials: 'include' fuer Cookie-Auth
await fetch('/api/dashboard/layout', {
credentials: 'include',
});
```
### i18n Pattern
**Source:** `apps/web/src/app/(portal)/page.tsx` Zeilen 1-6
**Apply to:** Alle Frontend-Komponenten
```typescript
'use client';
import { useTranslations } from 'next-intl';
const t = useTranslations('dashboard');
// Verwendung: {t('empty')}, {t('addWidget')}
```
### Tailwind UI Pattern
**Source:** `apps/web/src/components/layout/header.tsx`
**Apply to:** Alle neuen Komponenten
- Farben: `bg-muted`, `text-muted-foreground`, `bg-primary`, `text-primary-foreground`, `border-border`, `bg-card`, `text-foreground`
- Hover: `hover:bg-muted`, `hover:opacity-90`, `transition-colors`
- Layout: `flex`, `items-center`, `gap-2`, `rounded-md`, `px-4 py-2`
## No Analog Found
| File | Role | Data Flow | Reason |
|------|------|-----------|--------|
| `apps/api/src/calendar/providers/caldav.provider.ts` | service | request-response | Kein externer API-Client-Pattern im Projekt. RESEARCH.md Pattern (tsdav) verwenden |
| `apps/api/src/calendar/providers/ics.provider.ts` | service | request-response | Kein HTTP-Fetch + Parse Pattern. RESEARCH.md Pattern (node-ical) verwenden |
| `apps/api/src/calendar/providers/exchange.provider.ts` | service | request-response | Kein EWS/Graph Pattern. RESEARCH.md Pattern verwenden |
| `apps/web/src/components/dashboard/dashboard-grid.tsx` | component | event-driven | react-grid-layout ist neu im Projekt. RESEARCH.md Code-Beispiel verwenden |
| `apps/web/src/components/dashboard/widget-catalog-modal.tsx` | component | event-driven | Kein Modal-Pattern im Projekt. shadcn/ui Dialog verwenden |
| `apps/web/src/components/dashboard/widgets/*.tsx` | component | event-driven | Widget-Komponenten sind neues Pattern. RESEARCH.md WidgetProps Interface verwenden |
## Metadata
**Analog search scope:** `apps/api/src/`, `apps/web/src/`
**Files scanned:** ~60
**Pattern extraction date:** 2026-06-23
@@ -0,0 +1,77 @@
---
phase: 05
slug: dashboard-calendar
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-06-23
---
# Phase 05 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | vitest |
| **Config file** | apps/web/vitest.config.ts |
| **Quick run command** | `pnpm --filter web test --run` |
| **Full suite command** | `pnpm --filter web test --run && pnpm --filter api test --run` |
| **Estimated runtime** | ~30 seconds |
---
## Sampling Rate
- **After every task commit:** Run `pnpm --filter web test --run`
- **After every plan wave:** Run `pnpm --filter web test --run && pnpm --filter api test --run`
- **Before `/gsd-verify-work`:** Full suite must be green
- **Max feedback latency:** 30 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| *Populated during planning* | | | | | | | | | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] react-grid-layout installation and type stubs
- [ ] Markdown editor library installation
- [ ] Calendar library installations (tsdav, node-ical)
- [ ] Dashboard API test stubs
- [ ] Calendar source API test stubs
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Drag & drop widget rearrangement | DASH-01 | Visual interaction | Open dashboard, enter edit mode, drag widget to new position |
| Widget resize | DASH-02 | Visual interaction | Enter edit mode, resize widget via drag handle |
| Calendar events display | CAL-03 | External service | Configure calendar source, verify events appear |
| Responsive grid scaling | DASH-03 | Visual breakpoints | Resize browser window, verify grid scales proportionally |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 30s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending