docs(05): create dashboard & calendar phase plan

4 plans across 4 waves covering DASH-01..07 + CAL-01..03.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 14:42:11 +02:00
parent b5ab2c806e
commit c8852d2014
5 changed files with 1035 additions and 2 deletions
@@ -0,0 +1,255 @@
---
phase: 05-dashboard-calendar
plan: 02
type: execute
wave: 2
depends_on: ["05-01"]
files_modified:
- apps/api/prisma/schema.prisma
- apps/api/src/dashboard/dashboard.controller.ts
- apps/api/src/dashboard/dashboard.service.ts
- apps/api/src/dashboard/dto/create-search-provider.dto.ts
- apps/web/package.json
- apps/web/src/components/dashboard/widgets/search-widget.tsx
- apps/web/src/components/dashboard/widgets/note-widget.tsx
- apps/web/src/components/dashboard/widget-registry.ts
- apps/web/src/app/(portal)/settings/dashboard/page.tsx
- apps/web/src/components/settings/widget-settings-panel.tsx
- apps/web/src/components/settings/search-provider-form.tsx
- apps/web/src/lib/dashboard-api.ts
- apps/web/src/components/dashboard/widgets/search-widget.test.tsx
- apps/web/src/components/dashboard/widgets/note-widget.test.tsx
autonomous: true
requirements: [DASH-04, DASH-06]
must_haves:
truths:
- "User can add a search widget, pick a provider, and a web search opens in a new browser tab"
- "User can add a notes widget, type Markdown, and content autosaves silently"
- "User can configure clock timezone/date, notes title, and custom search providers in Settings > Dashboard"
artifacts:
- path: "apps/web/src/components/dashboard/widgets/search-widget.tsx"
provides: "Search widget with provider dropdown + new-tab open"
- path: "apps/web/src/components/dashboard/widgets/note-widget.tsx"
provides: "Markdown notes widget with debounced autosave"
- path: "apps/web/src/app/(portal)/settings/dashboard/page.tsx"
provides: "Widget settings panel (config per widget instance)"
- path: "apps/api/prisma/schema.prisma"
provides: "SearchProvider model"
contains: "model SearchProvider"
key_links:
- from: "apps/web/src/components/dashboard/widgets/note-widget.tsx"
to: "/api/dashboard/widgets/:id/config"
via: "debounced PATCH autosave"
pattern: "widgets/.*config"
- from: "apps/web/src/components/dashboard/widgets/search-widget.tsx"
to: "window.open"
via: "provider urlTemplate with {query}"
pattern: "window\\.open"
---
<objective>
Add two more widget vertical slices on top of the 05-01 foundation: the Search widget (provider dropdown + search field + button that opens a web search in a new tab) and the Notes widget (Markdown editor with compact toolbar and debounced autosave). Implement the Widget Settings panel (Settings > Dashboard) where users configure per-instance settings: clock timezone + date toggle, notes title, and custom search providers. Add the SearchProvider backend (seed defaults + custom CRUD).
Purpose: Complete DASH-04 (search) and DASH-06 (notes) as working slices, and deliver the widget-config half of D-03/D-12/D-13/D-15/D-17.
Output: Functional search + notes widgets and a widget settings page.
</objective>
<artifacts_this_phase_produces>
Symbols created by THIS plan (exclude from drift verification — they are new):
**Prisma models:** `SearchProvider`
**NestJS:** `CreateSearchProviderDto`; new DashboardService methods `getSearchProviders`, `addSearchProvider`, `removeSearchProvider`; new DashboardController routes `GET /dashboard/search-providers`, `POST /dashboard/search-providers`, `DELETE /dashboard/search-providers/:id`
**Frontend components:** `SearchWidget`, `NoteWidget`, `WidgetSettingsPanel`, `SearchProviderForm`
**Frontend additions:** `dashboard-api.ts` functions `fetchSearchProviders`, `addSearchProvider`, `removeSearchProvider`; registry wiring of real `SearchWidget`/`NoteWidget` components
**npm:** `@uiw/react-md-editor`
**Default search providers (seed):** Google, Bing, DuckDuckGo
</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/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
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Search + Notes widgets</name>
<files>apps/web/package.json, apps/web/src/components/dashboard/widgets/search-widget.tsx, apps/web/src/components/dashboard/widgets/note-widget.tsx, apps/web/src/components/dashboard/widget-registry.ts, apps/web/src/lib/dashboard-api.ts, apps/web/src/components/dashboard/widgets/search-widget.test.tsx, apps/web/src/components/dashboard/widgets/note-widget.test.tsx</files>
<read_first>
- apps/web/src/components/dashboard/widget-registry.ts (from 05-01 — replace placeholder search/note components with real ones; keep WIDGET_CONSTRAINTS unchanged)
- apps/web/src/components/dashboard/widgets/clock-widget.tsx (from 05-01 — WidgetProps usage pattern, config reading)
- apps/web/src/lib/dashboard-api.ts (from 05-01 — updateWidgetConfig + add provider fns here)
- .planning/phases/05-dashboard-calendar/05-RESEARCH.md lines 584-645 (Notes widget autosave + MDEditor commands + AbortController pattern) and lines 33-42 (D-14/D-15/D-16/D-17/D-18)
- .planning/phases/05-dashboard-calendar/05-UI-SPEC.md lines 168-178 (search + notes interaction contracts), lines 210-212 (search placeholder, notes default title copy)
</read_first>
<behavior>
- Test (search-widget.test.tsx): selecting provider "Google" and submitting query "hello" calls `window.open` with `https://www.google.com/search?q=hello` and a `_blank` target; Enter key in the input also triggers it
- Test (note-widget.test.tsx): typing into the editor schedules a debounced PATCH to `/api/dashboard/widgets/:id/config` with `{ content }` after the debounce window (use fake timers); rapid typing collapses to a single save after the last keystroke
- Search widget renders provider dropdown (default Google/Bing/DuckDuckGo), input, button left-to-right (D-14)
</behavior>
<action>
Install editor: `cd apps/web && pnpm add @uiw/react-md-editor@4.1.1` (legitimacy: Approved in RESEARCH Audit — uiwjs, 775K/wk; no checkpoint).
`search-widget.tsx` (`'use client'`, DASH-04, D-14/D-15): horizontal layout — provider `<select>` left (~120px), text `<input>` center (flex-1, placeholder from t('widgets.searchPlaceholder')), search button right. On submit (button click or Enter) build the target URL from the selected provider's urlTemplate by replacing `{query}` with `encodeURIComponent(query)` and call `window.open(url, '_blank', 'noopener,noreferrer')`. Provider list: fetch via dashboard-api.fetchSearchProviders() (defaults Google `https://www.google.com/search?q={query}`, Bing `https://www.bing.com/search?q={query}`, DuckDuckGo `https://duckduckgo.com/?q={query}`); selected provider persists per instance via updateWidgetConfig({ providerId }). Falls back to the three hardcoded defaults if the fetch fails so the widget always works.
`note-widget.tsx` (`'use client'`, DASH-06, D-16/D-17/D-18): editable title above (Body 14px weight 600, from config.title default t('widgets.notesDefaultTitle')), then MDEditor from @uiw/react-md-editor with a compact `commands` array [bold, italic, strikethrough, divider, unorderedListCommand, checkedListCommand, divider, link, code] (D-16 toolbar), `preview="edit"`, `visibleDragbar={false}`, `data-color-mode="auto"` wrapper (next-themes dark compat, Assumption A1). Autosave: debounce 1000ms (UI-SPEC) / 1500ms acceptable; on each change schedule a PATCH via updateWidgetConfig({ content, title }); abort the in-flight request with AbortController before issuing a new one (Pitfall 7); swallow AbortError. On non-abort error show a small red dot top-right (t('widgets.autosaveError') tooltip). Enable rehype-sanitize for rendered Markdown (security — XSS via Markdown, RESEARCH Security Domain).
Update `widget-registry.ts`: replace the search and note placeholder component refs with the real SearchWidget and NoteWidget. Do NOT change WIDGET_CONSTRAINTS.
Add to `dashboard-api.ts`: `fetchSearchProviders()` → GET /api/dashboard/search-providers; `addSearchProvider(payload)` → POST; `removeSearchProvider(id)` → DELETE /api/dashboard/search-providers/:id. All with `credentials:'include'`.
Write both test files per <behavior> with @testing-library/react + vitest fake timers; stub window.open and fetch.
</action>
<verify>
<automated>cd apps/web && pnpm vitest run src/components/dashboard/widgets/search-widget.test.tsx src/components/dashboard/widgets/note-widget.test.tsx</automated>
</verify>
<acceptance_criteria>
- apps/web/package.json dependencies contains `@uiw/react-md-editor`
- search-widget.tsx contains `window.open` and replaces `{query}` with an encoded query
- note-widget.tsx contains an `AbortController` usage and a debounce timer
- note-widget.tsx enables `rehype-sanitize` (or MDEditor sanitize option) for rendered markdown
- widget-registry.ts references `SearchWidget` and `NoteWidget` (no placeholder stub for these two)
- both vitest files exit 0
</acceptance_criteria>
<done>Search opens web search in new tab; notes autosaves Markdown with debounce + abort; tests green.</done>
</task>
<task type="auto">
<name>Task 2: SearchProvider backend (model + seed defaults + CRUD)</name>
<files>apps/api/prisma/schema.prisma, apps/api/src/dashboard/dashboard.controller.ts, apps/api/src/dashboard/dashboard.service.ts, apps/api/src/dashboard/dto/create-search-provider.dto.ts</files>
<read_first>
- apps/api/prisma/schema.prisma (from 05-01 — add SearchProvider model following DashboardLayout/WidgetInstance conventions)
- apps/api/src/dashboard/dashboard.controller.ts (from 05-01 — add three routes, reuse userId/tenantId extraction)
- apps/api/src/dashboard/dashboard.service.ts (from 05-01 — add provider methods, reuse PrismaService)
- apps/api/src/domaincheck/dto/check-domain.dto.ts (class-validator DTO pattern)
</read_first>
<action>
Add Prisma model `SearchProvider`: `id String @id @default(uuid())`, `userId String?` (null = global default, non-null = user custom), `tenantId String?`, `name String`, `urlTemplate String` (must contain `{query}`), `isDefault Boolean @default(false)`, `createdAt DateTime @default(now())`, `@@index([userId])`. The three default providers (Google/Bing/DuckDuckGo) are returned by the service even when no DB rows exist — implement defaults as constants merged with user-custom rows (avoids a separate seed migration). D-15.
Add DashboardController routes: `@Get('search-providers')` returns defaults + user's custom providers; `@Post('search-providers')` creates a user custom provider; `@Delete('search-providers/:id')` deletes only own custom provider (cannot delete defaults). Reuse userId/tenantId extraction.
Add DashboardService methods: `getSearchProviders(userId)` merges the three default constants with `prisma.searchProvider.findMany({ where: { userId } })`; `addSearchProvider(userId, tenantId, dto)`; `removeSearchProvider(id, userId)` with ownership check (NotFoundException if not own).
Create `create-search-provider.dto.ts`: `@IsString() @IsNotEmpty() name!: string` and `@IsString() @Matches(/\{query\}/, { message: 'urlTemplate must contain {query}' }) urlTemplate!: string`.
</action>
<verify>
<automated>cd apps/api && npx prisma validate && npx tsc --noEmit</automated>
</verify>
<acceptance_criteria>
- schema.prisma contains `model SearchProvider`
- dashboard.controller.ts contains `@Get('search-providers')`, `@Post('search-providers')`, `@Delete('search-providers/:id')`
- dashboard.service.ts getSearchProviders returns the three defaults Google/Bing/DuckDuckGo even with an empty DB
- create-search-provider.dto.ts validates urlTemplate contains `{query}`
- `npx prisma validate` and `npx tsc --noEmit` exit 0
</acceptance_criteria>
<done>Search provider CRUD works; defaults always available; custom providers user-scoped.</done>
</task>
<task type="auto">
<name>Task 3: Widget settings panel (Settings > Dashboard)</name>
<files>apps/web/src/app/(portal)/settings/dashboard/page.tsx, apps/web/src/components/settings/widget-settings-panel.tsx, apps/web/src/components/settings/search-provider-form.tsx, apps/web/src/lib/dashboard-api.ts</files>
<read_first>
- apps/web/src/app/(portal)/settings/layout.tsx (from 05-01 — this page renders inside the settings sub-sidebar layout)
- apps/web/src/components/dashboard/widget-registry.ts (WIDGET_REGISTRY for widget type labels/icons)
- apps/web/src/lib/dashboard-api.ts (fetchWidgets, updateWidgetConfig, search provider fns)
- .planning/phases/05-dashboard-calendar/05-UI-SPEC.md lines 119-120 (WidgetSettingsPanel spec), lines 16-19/32-33/40-41 D-03/D-12/D-13/D-15/D-17
</read_first>
<action>
Create `settings/dashboard/page.tsx` (D-03): `'use client'`, fetches the user's widget instances (fetchWidgets) and renders `WidgetSettingsPanel`. Title from t('settings.categoryWidgets').
Create `widget-settings-panel.tsx`: lists all placed widget instances grouped/labeled by type via WIDGET_REGISTRY. Each instance is expandable to its type-specific config form, persisting via updateWidgetConfig(instanceId, partialConfig):
- clock: timezone select (IANA list — at minimum Europe/Berlin, Europe/London, America/New_York, Asia/Tokyo, UTC) + "Datum anzeigen" toggle (D-12/D-13)
- search: shows SearchProviderForm for managing custom providers (D-15)
- note: editable title field (D-17)
- calendar: a hint that calendar sources are managed under Settings > Dashboard > Kalender (link); no per-instance config here in this plan
Forms use the established Tailwind token classes (bg-card, border-border, text-foreground, etc).
Create `search-provider-form.tsx`: lists current providers (defaults shown read-only, custom deletable), plus an add form (name + urlTemplate with `{query}` placeholder hint). Add via addSearchProvider, delete via removeSearchProvider. Validate client-side that urlTemplate contains `{query}` before submit.
Ensure dashboard-api.ts exposes the provider functions (added in 05-02 Task 1) — no duplication.
</action>
<verify>
<automated>cd apps/web && pnpm exec tsc --noEmit && pnpm vitest run src/components/dashboard 2>/dev/null; cd apps/web && pnpm exec tsc --noEmit</automated>
</verify>
<acceptance_criteria>
- settings/dashboard/page.tsx exists and renders WidgetSettingsPanel
- widget-settings-panel.tsx contains a timezone select and a date-toggle for clock config
- widget-settings-panel.tsx contains a title input for note config
- search-provider-form.tsx validates `{query}` presence client-side
- `pnpm exec tsc --noEmit` exits 0
</acceptance_criteria>
<done>Settings > Dashboard lets users configure clock timezone/date, note titles, and custom search providers.</done>
</task>
<task type="auto">
<name>Task 4: [BLOCKING] Prisma schema push</name>
<files>apps/api/prisma/schema.prisma</files>
<read_first>
- apps/api/prisma/schema.prisma (SearchProvider model from Task 2 must exist)
</read_first>
<action>
After Task 2 adds SearchProvider, push the schema to the running PostgreSQL container so the live DB has the new table. Run `npx prisma db push` from apps/api, then `npx prisma generate`. MANDATORY — type checks pass without it (false-positive). Only the new SearchProvider table is added (no destructive change expected); if data loss is reported, STOP and flag for manual review rather than passing `--accept-data-loss`.
</action>
<verify>
<automated>cd apps/api && npx prisma db push --skip-generate && npx prisma generate</automated>
</verify>
<acceptance_criteria>
- `npx prisma db push` exits 0 and reports schema in sync on a second run
- live DB contains the SearchProvider table
- `npx prisma generate` exits 0
</acceptance_criteria>
<done>Live PostgreSQL schema includes SearchProvider; client regenerated.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Browser → Search provider URL | User-supplied urlTemplate opened in new tab |
| Browser → Notes content | User Markdown rendered in widget |
| API → PostgreSQL | User-scoped search provider CRUD + widget config |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-05-05 | Tampering (XSS) | note-widget.tsx Markdown render | mitigate | Enable rehype-sanitize on MDEditor preview (RESEARCH Security Domain) |
| T-05-06 | Tampering | search urlTemplate `{query}` substitution | mitigate | encodeURIComponent on query; urlTemplate validated to contain `{query}`; open with `noopener,noreferrer` |
| T-05-07 | Elevation of Privilege | search-providers DELETE | mitigate | removeSearchProvider verifies userId ownership; default providers (userId null) cannot be deleted |
| T-05-08 | Tampering | CreateSearchProviderDto | mitigate | class-validator: IsString/IsNotEmpty name, Matches `{query}` on urlTemplate (ASVS V5) |
| T-05-SC | Tampering | npm install @uiw/react-md-editor | mitigate | Package Approved in RESEARCH Legitimacy Audit (775K/wk); no [ASSUMED]/[SUS] → no blocking checkpoint |
</threat_model>
<verification>
- `cd apps/api && npx prisma validate` exits 0
- `cd apps/api && npx tsc --noEmit` exits 0
- `cd apps/web && pnpm exec tsc --noEmit` exits 0
- `cd apps/web && pnpm vitest run src/components/dashboard` green
- `npx prisma db push` reports in sync
</verification>
<success_criteria>
- User adds search widget, selects provider, runs a web search opening in a new tab
- User adds notes widget, types Markdown, content autosaves silently with debounce
- User configures clock timezone + date, note title, and custom search providers in Settings > Dashboard
- All widget mutations remain user-scoped
</success_criteria>
<output>
Create `.planning/phases/05-dashboard-calendar/05-02-SUMMARY.md` when done
</output>