--- 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" --- 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. 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 @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.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 Task 1: Search + Notes widgets 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 - 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) - 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) 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 `` 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 with @testing-library/react + vitest fake timers; stub window.open and fetch. cd apps/web && pnpm vitest run src/components/dashboard/widgets/search-widget.test.tsx src/components/dashboard/widgets/note-widget.test.tsx - 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 Search opens web search in new tab; notes autosaves Markdown with debounce + abort; tests green. Task 2: SearchProvider backend (model + seed defaults + CRUD) 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/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) 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`. cd apps/api && npx prisma validate && npx tsc --noEmit - 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 Search provider CRUD works; defaults always available; custom providers user-scoped. Task 3: Widget settings panel (Settings > Dashboard) 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/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 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. cd apps/web && pnpm exec tsc --noEmit && pnpm vitest run src/components/dashboard 2>/dev/null; cd apps/web && pnpm exec tsc --noEmit - 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 Settings > Dashboard lets users configure clock timezone/date, note titles, and custom search providers. Task 4: [BLOCKING] Prisma schema push apps/api/prisma/schema.prisma - apps/api/prisma/schema.prisma (SearchProvider model from Task 2 must exist) 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`. cd apps/api && npx prisma db push --skip-generate && npx prisma generate - `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 Live PostgreSQL schema includes SearchProvider; client regenerated. ## 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 | - `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 - 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 Create `.planning/phases/05-dashboard-calendar/05-02-SUMMARY.md` when done