---
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 `
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