Files
schalli c8852d2014 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>
2026-06-23 14:42:11 +02:00

18 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
05-dashboard-calendar 02 execute 2
05-01
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
true
DASH-04
DASH-06
truths artifacts key_links
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
path provides
apps/web/src/components/dashboard/widgets/search-widget.tsx Search widget with provider dropdown + new-tab open
path provides
apps/web/src/components/dashboard/widgets/note-widget.tsx Markdown notes widget with debounced autosave
path provides
apps/web/src/app/(portal)/settings/dashboard/page.tsx Widget settings panel (config per widget instance)
path provides contains
apps/api/prisma/schema.prisma SearchProvider model model SearchProvider
from to via pattern
apps/web/src/components/dashboard/widgets/note-widget.tsx /api/dashboard/widgets/:id/config debounced PATCH autosave widgets/.*config
from to via pattern
apps/web/src/components/dashboard/widgets/search-widget.tsx window.open provider urlTemplate with {query} 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.

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

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

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

<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>
Create `.planning/phases/05-dashboard-calendar/05-02-SUMMARY.md` when done