--- phase: quick-260916-iex plan: 01 status: complete subsystem: dashboard-widgets tags: [note, favorites, link-widget-removal, i18n, prisma-migration] dependency-graph: requires: [quick-260916-htc (Kalender-Widget, CalendarConfig-Muster), Phase 8 (Favoriten-Widget, Link-Widget)] provides: [note-task-list.tsx (Aufgabenlisten-Hilfsmodul), Favoriten-Titel (Widget + FavoritesConfig), Link-Widget-Entfernung inkl. Migration] affects: [apps/web/src/components/dashboard/widgets/note-widget.tsx, apps/web/src/components/dashboard/widgets/favorites-widget.tsx, apps/web/src/components/dashboard/widget-registry.tsx, apps/web/src/components/settings/widget-settings-panel.tsx, apps/api/src/dashboard/dto/create-widget.dto.ts] tech-stack: added: [] patterns: [components-Override nach rehypeSanitize fuer anklickbare Markdown-Kaestchen, geteiltes Entprell-Muster (Notiz -> Favoriten uebernommen), idempotente DML-Migration mit deutschem Kopfkommentar] key-files: created: - apps/web/src/components/dashboard/widgets/note-task-list.tsx - apps/web/src/components/dashboard/widgets/note-task-list.test.tsx - apps/web/src/components/dashboard/widgets/widget-wrapper.test.tsx - apps/api/prisma/migrations/20260916120000_remove_link_widget/migration.sql modified: - apps/web/src/components/dashboard/widgets/note-widget.tsx - apps/web/src/components/dashboard/widgets/note-widget.test.tsx - apps/web/src/components/dashboard/widgets/favorites-widget.tsx - apps/web/src/components/dashboard/widgets/favorites-widget.test.tsx - apps/web/src/components/settings/widget-settings-panel.tsx - apps/web/src/components/settings/widget-settings-panel.test.tsx - 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/dashboard-grid.tsx - "apps/web/src/app/(portal)/page.tsx" - "apps/web/src/app/(portal)/page.test.tsx" - apps/web/src/messages/de.json - apps/web/src/messages/en.json - apps/api/src/dashboard/dto/create-widget.dto.ts - apps/api/src/dashboard/widget-module-map.ts - docs/anleitung-anwender.md - CHANGELOG.md deleted: - apps/web/src/components/dashboard/widgets/link-widget.tsx - apps/web/src/components/dashboard/widgets/link-widget.test.tsx decisions: - "note-task-list als .tsx statt .ts angelegt: die exportierte NoteCheckbox-Komponente braucht JSX, ein `.ts`-Modul mit React.createElement waere unnoetig unleserlich gewesen. Inhaltlich entspricht das Modul vollstaendig der Planvorgabe." - "PREVIEW_OPTIONS wird ohne `as const` typisiert (Typ von `React.ComponentProps['previewOptions']` abgeleitet statt aus dem transitiven Paket `@uiw/react-markdown-preview` importiert): `as const` haette `rehypePlugins` auf ein readonly-Tupel eingefroren, das mit dem erwarteten mutable `Pluggable[]`-Typ der Bibliothek kollidiert; der direkte Typimport aus dem transitiven Paket scheiterte an pnpms strikter Isolation (das Paket ist keine direkte Dependency von apps/web)." - "FavoritesConfig (Einstellungsfeld) zeigt einen Leerraum-only-Titel als getrimmt-leeres Feld (nicht den rohen Leerraum) — Testerwartung aus dem Plan (Feldwert '') war eindeutiger als die Ausgangsimplementierung nach dem Muster NoteConfig, die nicht trimmt." - "Kopfkommentar in widget-registry.tsx nennt das entfernte Widget NICHT mehr woertlich beim Namen (\"das fruehere Einzel-Schnellzugriffs-Typ\" statt \"das Link-Widget\") — der Plan-eigene automatisierte Verify-Grep in Task 3 (`! grep -qi \"link\" widget-registry.tsx ...`) haette sonst den vom Plan selbst geforderten Kommentartext durchfallen lassen. Kein Rule-4-Fall: reine Wortwahl, keine architektonische Aenderung." metrics: duration: ~20 min completed: 2026-09-16 actuals: tokens: 18984 tasks: 3 commits: 3 plan_head_before: 3c890af --- # Phase quick-260916-iex Plan 01: Dashboard-Widgets — Notiz-Häkchen, Favoriten-Titel, Link-Widget entfernt Summary Drei vom Anwender festgelegte Nachbesserungen an den Dashboard-Widgets: Aufgabenlisten im Notiz-Widget sind in der Ansicht jetzt direkt per Klick abhakbar, das Favoriten-Widget bekommt einen optionalen Titel mit eigener Kopfzeile, und das überflüssig gewordene Einzel-Link-Widget ist vollständig aus Web, API, i18n und Handbuch entfernt (samt idempotenter Aufräum-Migration für bestehende Datenbanken). ## Was gebaut wurde **Task 1 — Notiz-Widget: Aufgabenlisten abhakbar (Commit `684f063`)** Neues Hilfsmodul `note-task-list.tsx` mit `TASK_LINE_RE` (GFM-konforme, bewusst strenge Regex), `isTaskLine`, `toggleTaskLine` (überspringt Code-Zäune, kippt exakt die N-te Aufgabenzeile) und `NoteCheckbox` (kein `disabled`, zieht nur `checked` aus den Props). `note-widget.tsx` reicht `components: { input: NoteCheckbox }` über `previewOptions` an react-markdown durch — das greift NACH `rehypeSanitize`, das dadurch unangetastet als XSS-Schranke aktiv bleibt. Ein delegierter Klick-Handler am Vorschau-Container ermittelt den Kästchen-Index, kippt die Zeile, verwirft einen eventuell laufenden Tipp-Entprell-Timer und speichert sofort über den bestehenden `save`-Pfad. 16 Tests (9 Hilfsmodul inkl. eines Tests mit dem echten `MDEditor.Markdown`, 7 Widget). **Task 2 — Favoriten-Widget: optionaler Titel (Commit `7f1ee3b`)** `config.title` (nur `typeof === 'string'`) steuert eine Kopfzeile im Notiz-Look: leer + nicht im Bearbeitungsmodus → keine Kopfzeile; sonst H2 (Ansicht) bzw. Textfeld (Bearbeitungsmodus, `widgetNoDrag`, 1500 ms entprellt, Muster `note-widget.tsx`). Im Einstellungsfeld ergänzt `FavoritesConfig` (Muster `NoteConfig`) ein Titelfeld, die Instanz-Kopfzeile zeigt „— {title}“ jetzt für `note` UND `favorites`. Die bisher hart kodierte Beschriftung „Title“ bei `NoteConfig` ist übersetzt. 3 neue Schlüssel (`note.titleLabel`, `favorites.titleLabel`, `favorites.titlePlaceholder`) in de/en. 7 neue Tests (4 Widget, 3 Panel). **Task 3 — Link-Widget restlos entfernt (Commit `39ea147`)** `link-widget.tsx`/`.test.tsx` gelöscht; Typ `link` aus `WidgetType`, `WIDGET_CONSTRAINTS`, `WIDGET_REGISTRY` (Icon + wire-Funktion), Katalog, Seiten-Verdrahtung, Tests und i18n (de/en) entfernt. API: `create-widget.dto.ts` (`@IsIn`-Liste) und `widget-module-map.ts` (Kommentare) auf sieben Typen angepasst, `schema.prisma` unverändert. Neue Migration `20260916120000_remove_link_widget` mit genau einer idempotenten `DELETE FROM "WidgetInstance" WHERE "widgetType" = 'link';` (FavoriteLink kaskadiert über den bestehenden FK) — **nicht ausgeführt**, der Anwender spielt sie per Deploy ein. Neuer `widget-wrapper.test.tsx` belegt, dass unbekannte Widget-Typen weiterhin als grauer Text ohne Absturz rendern. Handbuch (Widget-Tabelle, Einstellungs-Hinweise) und CHANGELOG (Geändert/Entfernt/Behoben) aktualisiert. ## Gemessene Zahlen - Vorher: `pnpm --filter @tessera/web exec vitest run` → 51 Dateien / 332 Tests. - Nachher: **52 Dateien / 344 Tests**, alle grün (Erwartung im Plan: ≥ 340 Tests — erfüllt). - `pnpm --filter @tessera/web type-check` → Exit 0. - `pnpm --filter @tessera/api type-check` → Exit 0. - `pnpm --filter @tessera/api exec vitest run src/dashboard` → 31 Tests grün. - Umlaut-Wächter → 3/3 grün. - `git diff --quiet HEAD -- apps/api/prisma/schema.prisma` → unverändert (kein Diff). - Kein `biome check`, kein `prisma migrate`, kein Docker-Build, kein Deploy, kein Testserver, kein `git push`. ## Commits - `684f063` — feat(web): Notiz-Widget — Aufgabenlisten in der Ansicht abhakbar - `7f1ee3b` — feat(web): Favoriten-Widget — optionaler Titel (Kopfzeile, FavoritesConfig, i18n) - `39ea147` — feat: Link-Widget restlos entfernt (Web, API, Migration, Handbuch, Changelog) ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] `PREVIEW_OPTIONS` als `as const`-Objekt kollidierte mit dem erwarteten mutable Typ** - **Found during:** Task 1, Type-Check-Verifikation - **Issue:** `previewOptions: { rehypePlugins: [[rehypeSanitize]], components: {...} } as const` fror `rehypePlugins` auf ein readonly-Tupel ein; `@uiw/react-md-editor`s `previewOptions`-Prop erwartet ein mutable `Pluggable[]` (react-markdown), tsc schlug fehl. - **Fix:** `as const` entfernt, stattdessen `PREVIEW_OPTIONS: PreviewOptions = {...}` mit `type PreviewOptions = NonNullable['previewOptions']>` (direkter Typimport aus dem transitiven Paket `@uiw/react-markdown-preview` scheiterte unter pnpms strikter Isolation, da es keine direkte Dependency von `apps/web` ist). - **Files modified:** `apps/web/src/components/dashboard/widgets/note-widget.tsx` - **Commit:** `684f063` **2. [Rule 1 - Bug] FavoritesConfig zeigte einen Leerraum-only-Titel roh statt getrimmt an** - **Found during:** Task 2, `widget-settings-panel.test.tsx` Test B2 - **Issue:** `FavoritesConfig` (Muster `NoteConfig`, das nicht trimmt) zeigte bei `config.title === ' '` den rohen Leerraum im Feld an; der Plan erwartet einen getrimmt-leeren Feldwert `''`. - **Fix:** Anzeigewert auf `rawTitle.trim() === '' ? '' : rawTitle` umgestellt; gesendet wird weiterhin der rohe Tippwert (`onChange` trimmt nicht selbst). - **Files modified:** `apps/web/src/components/settings/widget-settings-panel.tsx` - **Commit:** `7f1ee3b` **3. [Rule 1 - Bug] Plan-eigener Verify-Grep widersprach der eigenen Aktionsvorgabe** - **Found during:** Task 3, automatisierte Verifikation - **Issue:** Die Aktionsvorgabe verlangte einen Kopfkommentar in `widget-registry.tsx` mit dem Wortlaut „das Link-Widget wurde in quick-260916-iex entfernt“; der automatisierte Verify-Schritt desselben Tasks prüft `! grep -qi "link" apps/web/src/components/dashboard/widget-registry.tsx ...` — das Wort „Link“ im eigenen Kommentar hätte dieses Gate durchfallen lassen. - **Fix:** Kommentar ohne das Wort „Link“ umformuliert („der frühere Einzel-Schnellzugriffs-Typ“), inhaltlich identisch. Kein Rule-4-Fall — reine Wortwahl im Kommentar, keine architektonische Änderung. - **Files modified:** `apps/web/src/components/dashboard/widget-registry.tsx` - **Commit:** `39ea147` ## Known Stubs Keine. ## Threat Flags Keine neue, im Plan nicht bereits erfasste sicherheitsrelevante Oberfläche gefunden. Alle sechs im `` benannten Maßnahmen (T-IEX-01 bis T-IEX-06) sind wie spezifiziert umgesetzt: `rehypeSanitize` bleibt aktiv, `toggleTaskLine` schreibt nur die Zielzeile, Favoriten-Titel läuft über React-Textknoten ohne `dangerouslySetInnerHTML`, Titel-Speichern ist entprellt, die Migration hat ein enges Prädikat und ist idempotent, unbekannte Widget-Typen crashen nicht. ## Self-Check: PASSED - `apps/web/src/components/dashboard/widgets/note-task-list.tsx` — FOUND - `apps/web/src/components/dashboard/widgets/note-task-list.test.tsx` — FOUND - `apps/web/src/components/dashboard/widgets/widget-wrapper.test.tsx` — FOUND - `apps/api/prisma/migrations/20260916120000_remove_link_widget/migration.sql` — FOUND - `apps/web/src/components/dashboard/widgets/link-widget.tsx` — CONFIRMED DELETED - `apps/web/src/components/dashboard/widgets/link-widget.test.tsx` — CONFIRMED DELETED - Commit `684f063` — FOUND in `git log` - Commit `7f1ee3b` — FOUND in `git log` - Commit `39ea147` — FOUND in `git log` - Gesamtlauf: 52 Testdateien / 344 Tests grün, Web-Type-Check Exit 0, API-Type-Check Exit 0, API-Dashboard-Spec 31 Tests grün