c8852d2014
4 plans across 4 waves covering DASH-01..07 + CAL-01..03. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
256 lines
18 KiB
Markdown
256 lines
18 KiB
Markdown
---
|
|
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>
|