--- phase: 08-dashboard-widgets-vollimplementierung plan: 01 type: execute wave: 1 depends_on: [] files_modified: - apps/web/src/components/dashboard/widget-registry.tsx - apps/web/src/components/dashboard/widget-registry.test.tsx - apps/web/src/components/dashboard/widget-catalog-modal.tsx - apps/web/src/components/dashboard/widgets/calculator-widget.tsx - apps/web/src/components/dashboard/widgets/calculator-widget.test.tsx - apps/web/src/app/(portal)/page.tsx - apps/web/src/messages/de.json - apps/web/src/messages/en.json - apps/api/src/dashboard/dto/create-widget.dto.ts autonomous: true requirements: [DASH-08, DASH-11] must_haves: truths: - "User can open the widget catalog and see Calculator, Favoriten, Link and Stoppuhr as addable widget types" - "User can add a Calculator widget and perform +, -, x, / with mouse and keyboard" - "Every WidgetType has minW, minH, defaultW, defaultH constraint fields" artifacts: - "apps/web/src/components/dashboard/widgets/calculator-widget.tsx" - "apps/web/src/components/dashboard/widgets/calculator-widget.test.tsx" - "apps/web/src/components/dashboard/widget-registry.test.tsx" key_links: - "widget-registry.tsx WidgetType union includes calculator/favorites/link/stopwatch" - "create-widget.dto.ts @IsIn accepts the four new widget types" - "page.tsx calls wireCalculatorWidget(CalculatorWidget)" --- Deliver the shared widget-registry foundation for all Phase-8 widget types (DASH-11) and the first working vertical slice: a Calculator widget (DASH-08). This plan extends the registry once for all four new types (calculator, favorites, link, stopwatch) — adding their unified constraint-field structure, catalog entries, wiring hooks, DTO validation, and i18n keys — so subsequent plans only add their component file and one wiring line. It then delivers the fully functional Calculator widget. Purpose: Establish the registration plumbing every later widget slice depends on, and ship the simplest end-to-end widget first. Output: A user can add and use a Calculator widget from the dashboard catalog; all widget types carry the same constraint-field structure. **As a** portal user, **I want to** add a calculator widget to my dashboard and do quick arithmetic with keyboard support, **so that** I do not have to leave the dashboard for simple calculations. @$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/08-dashboard-widgets-vollimplementierung/08-CONTEXT.md @.planning/phases/08-dashboard-widgets-vollimplementierung/08-RESEARCH.md ## Artifacts this phase produces (Plan 01) New symbols/files introduced by this plan (exclude from source-drift verification): - WidgetType union members: `'calculator'`, `'favorites'`, `'link'`, `'stopwatch'` - `WIDGET_CONSTRAINTS` entries: `calculator`, `favorites`, `link`, `stopwatch` - `WIDGET_REGISTRY` entries: `calculator`, `favorites`, `link`, `stopwatch` - Wiring functions: `wireCalculatorWidget`, `wireFavoritesWidget`, `wireLinkWidget`, `wireStopwatchWidget` (exported from widget-registry.tsx) - Icon components in widget-registry.tsx: `CalculatorIcon`, `FavoritesIcon`, `LinkIcon`, `StopwatchIcon` - Component: `CalculatorWidget` (apps/web/src/components/dashboard/widgets/calculator-widget.tsx) - Test files: `calculator-widget.test.tsx`, `widget-registry.test.tsx` - i18n keys under `widgets`: `calculator.*`, `favorites.*`, `link.*`, `stopwatch.*` Task 1: Failing tests for Calculator widget and registry constraint structure (RED) apps/web/src/components/dashboard/widgets/calculator-widget.test.tsx apps/web/src/components/dashboard/widget-registry.test.tsx - apps/web/src/components/dashboard/widgets/note-widget.test.tsx (test/mocking pattern: next-intl mock, vitest, @testing-library/react) - apps/web/src/components/dashboard/widgets/clock-widget.tsx (WidgetProps consumption) - apps/web/src/components/dashboard/widget-registry.tsx (WidgetType, WIDGET_CONSTRAINTS structure) - apps/web/vitest.config.ts (test runner config) calculator-widget.test.tsx (mock next-intl like note-widget.test.tsx; import CalculatorWidget after mocks): - Renders with initial display "0". - Clicking 7, +, 3, = shows "10". - Clicking 8, x, 2, = shows "16". - Division by zero (5, /, 0, =) shows the error label "Fehler". - Keyboard: dispatching keydown "5", "*", "6", "Enter" on the calculator container shows "30". - Comma/decimal: 1, comma, 5, +, 1, comma, 5, = shows "3". widget-registry.test.tsx: - For every value in the WidgetType union, WIDGET_CONSTRAINTS[type] has numeric minW, minH, defaultW, defaultH (DASH-11 structure check). - WIDGET_CONSTRAINTS contains keys calculator, favorites, link, stopwatch (plus existing clock, search, calendar, note). Create both test files. Mock next-intl with a passthrough translator returning the key (or a small map) exactly as note-widget.test.tsx does. In calculator-widget.test.tsx use @testing-library/react render + userEvent/fireEvent; query the display by its aria-label "Anzeige" (role output). For keyboard tests, fireEvent.keyDown on the container element (role="application", aria-label "Taschenrechner"). Import CalculatorWidget from './calculator-widget' AFTER the mocks. In widget-registry.test.tsx import WIDGET_CONSTRAINTS and iterate over the four expected keys plus the existing four; assert each has the four numeric fields. These tests MUST fail now (calculator-widget.tsx and the new constraint keys do not exist yet). pnpm --filter @tessera/web test --run calculator-widget widget-registry 2>&1 | grep -Eq "fail|FAIL|Cannot find|error" && echo RED-OK - calculator-widget.test.tsx exists and imports from './calculator-widget' - widget-registry.test.tsx exists and asserts four numeric constraint fields per WidgetType - Running the two test files fails (RED) because the implementation and constraint keys do not yet exist Both test files exist and fail for the right reason (missing implementation), establishing the RED baseline. Task 2: Registry foundation for all four types + Calculator implementation (GREEN) apps/web/src/components/dashboard/widget-registry.tsx apps/web/src/components/dashboard/widget-catalog-modal.tsx apps/web/src/components/dashboard/widgets/calculator-widget.tsx apps/web/src/app/(portal)/page.tsx apps/web/src/messages/de.json apps/web/src/messages/en.json apps/api/src/dashboard/dto/create-widget.dto.ts - apps/web/src/components/dashboard/widget-registry.tsx (WidgetType union, WIDGET_CONSTRAINTS, WIDGET_REGISTRY, wireClockWidget/wireNoteWidget pattern, inline SVG icon pattern) - apps/web/src/components/dashboard/widget-catalog-modal.tsx (hardcoded WIDGET_TYPES array) - apps/web/src/app/(portal)/page.tsx (wireXWidget calls and imports) - apps/web/src/components/dashboard/widgets/clock-widget.tsx (WidgetProps + Tailwind pattern) - /home/vicolab/Schreibtisch/personal-dashboard/src/components/CalculatorWidget.tsx (full calculator logic to port: parseDisplay, formatNumber, calculate, inputDigit, inputDecimal, chooseOperator, applyEquals, backspace, clearAll, clearEntry, toggleSign, applyUnary, memory functions, handleKeyboard) - .planning/phases/08-dashboard-widgets-vollimplementierung/08-RESEARCH.md (Grid-Constraints table, Pattern 1, Pattern 6, i18n keys section, Anti-Patterns) Extend widget-registry.tsx: add `'calculator' | 'favorites' | 'link' | 'stopwatch'` to the WidgetType union. Add WIDGET_CONSTRAINTS entries with the per-widget values from D-01 (research Grid-Constraints table): calculator minW 2 minH 4 defaultW 3 defaultH 5; favorites minW 2 minH 3 defaultW 3 defaultH 5; link minW 2 minH 2 defaultW 2 defaultH 2; stopwatch minW 2 minH 2 defaultW 3 defaultH 3. Add four inline SVG icon components (CalculatorIcon, FavoritesIcon, LinkIcon, StopwatchIcon) following the existing ClockIcon SVG pattern (24x24, stroke currentColor). Add four WIDGET_REGISTRY entries (nameKey/descriptionKey pointing at the new i18n keys, icon, spread of WIDGET_CONSTRAINTS., component: PlaceholderWidget). Add four exported wiring functions wireCalculatorWidget, wireFavoritesWidget, wireLinkWidget, wireStopwatchWidget following the wireClockWidget guard pattern (module-level boolean flag, assign WIDGET_REGISTRY..component). Do NOT change existing clock/search/calendar/note constraint values. Update widget-catalog-modal.tsx: extend the hardcoded WIDGET_TYPES array to include 'calculator', 'favorites', 'link', 'stopwatch' after the existing four (Pitfall 6). Update create-widget.dto.ts: change the @IsIn list to accept all eight types: clock, search, calendar, note, calculator, favorites, link, stopwatch (Pitfall 7) so POST /dashboard/widgets does not reject the new types with 400. Create calculator-widget.tsx as `export function CalculatorWidget({ config, isEditMode }: WidgetProps)` (import type WidgetProps from '../widget-registry'; 'use client'). Port the arithmetic and keyboard logic verbatim from the personal-dashboard reference (keep parseDisplay/formatNumber/calculate and all handlers). Replace all CSS-module classes (styles.*) with Tailwind utility classes matching the Tessera widget look (flex h-full flex-col, muted/foreground/primary tokens); do NOT import any .module.css. Keep the container attributes tabIndex={0} role="application" aria-label from t('calculator.name'), onKeyDown={handleKeyboard}, and keep event.preventDefault() + event.stopPropagation() in every handled key branch (Pitfall 1 — prevents react-grid-layout interference). The display element must expose aria-label "Anzeige". Use useTranslations('widgets') for the container aria-label; digit/operator button glyphs stay literal. The error sentinel returned by formatNumber stays "Fehler". Wire the calculator into the dashboard: in page.tsx import CalculatorWidget and call wireCalculatorWidget(CalculatorWidget) alongside the existing wire calls. Add i18n keys under the "widgets" namespace in both de.json and en.json for calculator, favorites, link, stopwatch using the key set from the research i18n section (calculator.name/description; favorites.name/description/loading/empty/addTitle/addUrl/addButton/listView/gridView/editButton/deleteButton/saveButton/cancelButton/error; link.name/description; stopwatch.name/description/start/stop/reset/lap). German values from research; English values as faithful translations. pnpm --filter @tessera/web test --run calculator-widget widget-registry - calculator-widget.test.tsx and widget-registry.test.tsx pass (GREEN) - grep -c "calculator" apps/web/src/components/dashboard/widget-catalog-modal.tsx returns >= 1 - create-widget.dto.ts contains 'stopwatch' inside the @IsIn array - widget-registry.tsx exports wireCalculatorWidget, wireFavoritesWidget, wireLinkWidget, wireStopwatchWidget - calculator-widget.tsx contains no import of a .module.css file (grep -c "module.css" apps/web/src/components/dashboard/widgets/calculator-widget.tsx returns 0) - page.tsx contains wireCalculatorWidget(CalculatorWidget) - Both de.json and en.json contain a "calculator" object under "widgets" Adding a Calculator widget from the catalog renders a working calculator with mouse and keyboard input; all four new types are registered with unified constraint fields; the API accepts the new widget types. Task 3: Full suite + type-check gate apps/web/src/components/dashboard/widget-registry.tsx - apps/web/package.json (test + type-check scripts) - apps/api/package.json (build/type-check scripts) Run the full web test suite and TypeScript type-check for both web and api to confirm the registry/DTO changes did not break existing widgets (Clock, Search, Calendar, Note) or the API build. Fix any type errors introduced by the WidgetType union widening (e.g. exhaustive Record typing in WIDGET_CONSTRAINTS/WIDGET_REGISTRY). Do not add new features here. pnpm --filter @tessera/web test --run && pnpm --filter @tessera/web exec tsc --noEmit && pnpm --filter @tessera/api exec tsc --noEmit - Full @tessera/web vitest suite exits 0 - tsc --noEmit passes for @tessera/web - tsc --noEmit passes for @tessera/api Existing widgets remain functional and both packages type-check cleanly after the registry and DTO changes. ## Trust Boundaries | Boundary | Description | |----------|-------------| | client → API (POST /dashboard/widgets) | Untrusted widgetType string crosses into the API | | keyboard → grid | Calculator key events could bubble into react-grid-layout | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-08-01 | Tampering | create-widget.dto.ts | low | mitigate | @IsIn allow-list restricted to the eight known widget types; unknown types rejected with 400 | | T-08-02 | Denial of Service | Calculator keyboard handler | low | mitigate | event.stopPropagation() in handled branches prevents grid drag/scroll interference (Pitfall 1) | - pnpm --filter @tessera/web test --run passes (all widget suites including new calculator + registry) - tsc --noEmit clean for web and api - Manual smoke (optional): edit mode → add Calculator → 7 + 3 = 10; keyboard entry works - Calculator widget is addable from the catalog and performs correct arithmetic via mouse and keyboard (DASH-08) - All WidgetType values carry minW/minH/defaultW/defaultH fields (DASH-11 structure per D-01) - Registry wiring hooks for favorites/link/stopwatch exist for later plans to consume Create `.planning/phases/08-dashboard-widgets-vollimplementierung/08-01-SUMMARY.md` when done