From e60d34dc289c8cd23af6bbd45e5f71d133c57205 Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 24 Jun 2026 09:54:43 +0200 Subject: [PATCH] wip: phase 05 plan-phase paused at revision verify (checker v2 pending) Co-Authored-By: Claude Sonnet 4.6 --- .planning/HANDOFF.json | 48 +++ .../05-dashboard-calendar/.continue-here.md | 84 ++++ .../05-dashboard-calendar/05-04-PLAN.md | 176 ++++++--- .../05-dashboard-calendar/05-05-PLAN.md | 131 +++++++ .../05-dashboard-calendar/05-PATTERNS.md | 366 ++++++++++++++++++ .../05-dashboard-calendar/05-VALIDATION.md | 77 ++++ 6 files changed, 823 insertions(+), 59 deletions(-) create mode 100644 .planning/HANDOFF.json create mode 100644 .planning/phases/05-dashboard-calendar/.continue-here.md create mode 100644 .planning/phases/05-dashboard-calendar/05-05-PLAN.md create mode 100644 .planning/phases/05-dashboard-calendar/05-PATTERNS.md create mode 100644 .planning/phases/05-dashboard-calendar/05-VALIDATION.md diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json new file mode 100644 index 0000000..a20e54b --- /dev/null +++ b/.planning/HANDOFF.json @@ -0,0 +1,48 @@ +{ + "version": "1.0", + "timestamp": "2026-06-24T07:53:36.605Z", + "phase": "05", + "phase_name": "dashboard-calendar", + "phase_dir": ".planning/phases/05-dashboard-calendar", + "plan": null, + "task": null, + "total_tasks": null, + "status": "paused", + "workflow": "plan-phase", + "workflow_step": "revision-verify", + "completed_tasks": [ + {"id": "ui-spec", "name": "UI-SPEC.md created and verified (6/6 dimensions passed)", "status": "done", "commit": "2005448"}, + {"id": "research", "name": "RESEARCH.md created (react-grid-layout, calendar libs, markdown editor)", "status": "done", "commit": "b5ab2c8"}, + {"id": "validation", "name": "VALIDATION.md draft created", "status": "done", "commit": null}, + {"id": "patterns", "name": "PATTERNS.md created (30 files, 24 analogs)", "status": "done", "commit": null}, + {"id": "initial-plans", "name": "4 initial plans created by planner", "status": "done", "commit": "c8852d2"}, + {"id": "checker-v1", "name": "Plan checker run — found 2 blockers (05-03 missing requirements field, 05-03 scope too large)", "status": "done"}, + {"id": "revision", "name": "Planner revision — split 05-03 into 05-03 (backend) + 05-04 (frontend), old 05-04 became 05-05", "status": "done", "commit": null} + ], + "remaining_tasks": [ + {"id": "checker-v2", "name": "Re-run plan-checker on revised 5 plans", "status": "not_started"}, + {"id": "req-coverage", "name": "Requirements coverage gate (step 13)", "status": "not_started"}, + {"id": "decision-coverage", "name": "Decision coverage gate (step 13a)", "status": "not_started"}, + {"id": "state-update", "name": "Record planning completion in STATE.md (step 13b)", "status": "not_started"}, + {"id": "roadmap-annotate", "name": "Annotate ROADMAP with wave dependencies (step 13c)", "status": "not_started"}, + {"id": "commit-plans", "name": "Commit all plan artifacts (step 13d)", "status": "not_started"}, + {"id": "gap-analysis", "name": "Post-planning gap analysis (step 13e)", "status": "not_started"}, + {"id": "final-status", "name": "Present final status (step 14)", "status": "not_started"} + ], + "blockers": [], + "human_actions_pending": [], + "decisions": [ + {"decision": "Split 05-03 into backend (05-03) and frontend (05-04) plans", "rationale": "Checker found 5 tasks/20 files in complex calendar domain too large for single plan", "phase": "05"}, + {"decision": "Visual verification plan renumbered from 05-04 to 05-05", "rationale": "Shifted to accommodate calendar split", "phase": "05"}, + {"decision": "MVP vertical-slice mode", "rationale": "ROADMAP Mode: mvp for phase 05", "phase": "05"}, + {"decision": "UI-SPEC: 4 font sizes (12/14/18/28), 2 weights (400/600)", "rationale": "Checker blocked 7 sizes and 3 weights, reduced in revision", "phase": "05"} + ], + "uncommitted_files": [ + ".planning/phases/05-dashboard-calendar/05-04-PLAN.md", + ".planning/phases/05-dashboard-calendar/05-05-PLAN.md", + ".planning/phases/05-dashboard-calendar/05-PATTERNS.md", + ".planning/phases/05-dashboard-calendar/05-VALIDATION.md" + ], + "next_action": "Re-run plan-checker (gsd-plan-checker agent) on all 5 revised plans. If passes, proceed through gates 13→13a→13b→13c→13d→13e→14 to finalize planning.", + "context_notes": "Plan-phase workflow paused after revision iteration 1. Planner hit session limit during revision but still wrote the split files to disk. Checker has NOT yet verified the revised plans. The 5 plans cover waves 1-4 with sequential dependencies. All 10 requirement IDs (DASH-01..07, CAL-01..03) should now be covered across the 5 plans." +} diff --git a/.planning/phases/05-dashboard-calendar/.continue-here.md b/.planning/phases/05-dashboard-calendar/.continue-here.md new file mode 100644 index 0000000..944a048 --- /dev/null +++ b/.planning/phases/05-dashboard-calendar/.continue-here.md @@ -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 +--- + + +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] + + + + +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 + + + + +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) + + + + +- 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 + + + +None — revision produced files, just needs verification pass. + + +## 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 + + +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). + + + +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. + diff --git a/.planning/phases/05-dashboard-calendar/05-04-PLAN.md b/.planning/phases/05-dashboard-calendar/05-04-PLAN.md index 9245127..53095fb 100644 --- a/.planning/phases/05-dashboard-calendar/05-04-PLAN.md +++ b/.planning/phases/05-dashboard-calendar/05-04-PLAN.md @@ -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" --- -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. -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. @@ -32,7 +61,12 @@ No new symbols — verification-only plan. +@.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. - - Task 1: Pre-flight — start stack and run full test suite - + + 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 - - .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) + + - 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') + - 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 with mocked fetch. - cd apps/web && pnpm test && cd ../api && npx tsc --noEmit + cd apps/web && pnpm vitest run src/components/dashboard/widgets/calendar-widget.test.tsx && pnpm exec tsc --noEmit - - `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 - Full automated suite green and stack running; ready for human verification. + Calendar widget shows aggregated upcoming events from visible sources, cached refresh, read-only, with correct empty states. - - - 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). - - - Open the portal in a browser (logged in as a normal user). + + 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'). - 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. - - Type "approved" if everything works, or describe each issue found. + Write calendar-settings.test.tsx per 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. @@ -102,26 +154,32 @@ No new symbols — verification-only plan. ## 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) | -- 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 -- 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 Create `.planning/phases/05-dashboard-calendar/05-04-SUMMARY.md` when done + + diff --git a/.planning/phases/05-dashboard-calendar/05-05-PLAN.md b/.planning/phases/05-dashboard-calendar/05-05-PLAN.md new file mode 100644 index 0000000..1ee7aed --- /dev/null +++ b/.planning/phases/05-dashboard-calendar/05-05-PLAN.md @@ -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: [] +--- + + +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`). + + + +No new symbols — verification-only plan. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.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 + + + + + + Task 1: Pre-flight — start stack and run full test suite + + + - .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 + + + 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. + + + cd apps/web && pnpm test && cd ../api && npx tsc --noEmit + + + - `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) + + Full automated suite green and stack running; ready for human verification. + + + + + 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). + + + 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. + + Type "approved" if everything works, or describe each issue found. + + + + + +## 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 | + + + +- Full web test suite green (Task 1) +- API type-check green (Task 1) +- Human confirms all ROADMAP Phase 05 success criteria 1-5 + + + +- Human approves the complete dashboard + calendar experience, OR +- A concrete gap list is produced for `/gsd-plan-phase 05 --gaps` + + + +Create `.planning/phases/05-dashboard-calendar/05-05-SUMMARY.md` when done + + + diff --git a/.planning/phases/05-dashboard-calendar/05-PATTERNS.md b/.planning/phases/05-dashboard-calendar/05-PATTERNS.md new file mode 100644 index 0000000..f62305d --- /dev/null +++ b/.planning/phases/05-dashboard-calendar/05-PATTERNS.md @@ -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()( + 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()((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 +
+
+ +``` + +--- + +### 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 {children}; +} +``` + +**Settings-Layout wird aehnlich:** Statt `` ein `
` mit `` 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 */} +
+