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:
@@ -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."
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user