Files
schalli dcff93e2ae docs(08-04): complete Link widget plan summary
- 3/3 tasks complete (RED test → GREEN implementation → full suite gate)
- 88/88 web tests passing (18 test files, +7 link-widget tests)
- web + api tsc --noEmit clean
- LinkWidget: single-link D-06 via FavoriteLink backend reuse
2026-07-01 10:51:08 +02:00

7.0 KiB

phase, plan, subsystem, tags, status, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags status dependency_graph tech_stack key_files decisions metrics
08-dashboard-widgets-vollimplementierung 04 dashboard-widgets
link
widget
tdd
dashboard
persistence
i18n
icons
single-link
complete
requires provides affects
08-01 (wireLinkWidget, WIDGET_CONSTRAINTS.link, i18n keys)
08-03 (FavoriteLink backend, favorites-api.ts — shared via widgetId scoping)
LinkWidget component (single-link, list/tile view, add/edit/delete, letter fallback)
wireLinkWidget() wired in page.tsx
i18n keys
link.* (de.json + en.json)
apps/web/src/app/(portal)/page.tsx
added patterns
TDD Red/Green cycle (LinkWidget single-link behavioral contract)
Single-link enforcement via conditional rendering (add form hidden when link!=null)
Reuse of FavoriteLink backend (widgetId=instanceId, no new backend)
useEffect dep array excludes t() to prevent re-fetch loops (from Plan 03)
Letter fallback span always in DOM (img layered on top via absolute positioning)
target="_blank" rel="noreferrer" anchor pattern (T-08-12)
created modified
apps/web/src/components/dashboard/widgets/link-widget.tsx
apps/web/src/components/dashboard/widgets/link-widget.test.tsx
apps/web/src/app/(portal)/page.tsx (wireLinkWidget added)
apps/web/src/messages/de.json (link.* keys added)
apps/web/src/messages/en.json (link.* keys added)
Single-link enforcement: add form rendered only when link==null (conditional JSX), not via counter/limit logic — simpler and testable
renderIcon() helper extracted to avoid duplication between list+tile view (same icon logic, different size)
useEffect dep [instanceId] only — same rationale as Plan 03 (t() mock ref causes re-fetch loops)
letter-fallback testid: 'link-letter-fallback' (single link, no id suffix needed unlike favorites)
Worktree fast-forward: git merge main --ff-only before any task work (same pattern as Plan 03)
duration completed tasks_completed tests_added files_created files_modified
~4 minutes 2026-07-01 3 7 2 3

Phase 08 Plan 04: Link Widget Summary

Single-Link-Widget (DASH-09 / D-06) via TDD Red/Green-Zyklus: ein kompaktes Widget das genau einen Link anzeigt, den FavoriteLink-Backend von Plan 03 (widgetId-Scoping) wiederverwendet, und zwischen Zeilen- und Kachelansicht umgeschaltet werden kann.

What Was Built

  • LinkWidget (link-widget.tsx): 'use client', useTranslations('widgets'). Laed beim Mount via fetchFavorites(instanceId) und nimmt den ersten Eintrag als den einzigen Link (Single-Link-Constraint D-06). Kein neues Backend — FavoriteLink-Tabelle und FavoritesModule aus Plan 03 werden via widgetId=instanceId isoliert genutzt.

  • Single-Link-Enforcement: Das Hinzufuegen-Formular wird nur gerendert wenn link == null (nicht wenn ein Link bereits existiert). Das ist der "D-06: genau einen Link" Contract.

  • Ansichten (D-06): Standard-Listenansicht (data-testid="link-list": Icon + Titel in einer Zeile) und Kachelansicht (data-testid="link-tile": Icon oben, Titel darunter zentriert). Umschaltbar im Edit-Modus via viewMode-Toggle-Buttons, persistent via updateWidgetConfig().

  • CRUD im Edit-Modus: Hinzufuegen-Formular (wenn kein Link vorhanden), Inline-Bearbeiten (Titel, URL, optionale Logo-URL), Loeschen-Button. Alle CRUD-Operationen rufen createFavorite/updateFavorite/deleteFavorite auf.

  • Icon-Display: <img src={iconUrl}> mit onError-Hide + <span data-testid="link-letter-fallback"> als Buchstaben-Fallback. Kein dangerouslySetInnerHTML (T-08-11). renderIcon()-Helper fuer list/tile.

  • Sicherheit: Links mit target="_blank" rel="noreferrer" (T-08-12). Anchor-Klick im Edit-Modus via preventDefault() blockiert.

  • i18n: link.*-Keys in de.json und en.json hinzugefuegt: loading, empty, addTitle, addUrl, addButton, listView, gridView, editButton, deleteButton, saveButton, cancelButton, error.

  • page.tsx Wiring: wireLinkWidget(LinkWidget) und entsprechender Import hinzugefuegt.

Tasks

Task Description Status Commit
1 RED: Failing tests fuer LinkWidget single-link contract Complete 4657e50
2 GREEN: LinkWidget implementation + i18n keys + page.tsx wiring Complete e9914f6
3 Full Phase-8 Suite + Type-Check Gate Complete (verification only)

Test Results

  • link-widget.test.tsx: 7 Tests — fetchFavorites mit instanceId, Anchor target/_blank/rel=noreferrer, empty state, add-Formular, single-link enforcement, inline edit, view toggle (list/tile), letter fallback
  • Full Web Suite: 88/88 Tests bestehen (18 Test-Dateien; +7 neue gegenueber Plan 03's 81/81)
  • Web TypeScript: sauber (0 Fehler)
  • API TypeScript: sauber (0 Fehler)

Deviations from Plan

Worktree Fast-Forward (auto-fix, Rule 3)

Found during: Pre-task setup

Issue: Das Worktree-Branch worktree-agent-a2d7feebc2349e393 war bei 88db54f (Quick-Fix-Commit aus frueherem Task), nicht am erwarteten Basis-Commit 7b42a3a (Main nach Wave 3). Phase-08 wave-1, wave-2 und wave-3 Artefakte (widget-registry, FavoritesModule, StopwatchWidget) fehlten im Worktree.

Fix: git merge main --ff-only — Fast-Forward auf 7b42a3a. Keine Konflikte. Node-Modules-Symlinks auf main repo danach erstellt.

Impact: Keiner. Alle vorherigen Plan-Artefakte standen vollstaendig zur Verfuegung.

Found during: Task 1 Verifikation (RED-Phase)

Issue: Worktree hatte keine node_modules (vitest: not found).

Fix: Symlinks von MAIN_REPO/{,apps/web,apps/api,packages/shared}/node_modules -> entsprechende Worktree-Verzeichnisse.

Impact: Keiner.

dangerouslySetInnerHTML grep-count = 1 (kein Problem)

Das Vorkommen ist in einem JSDoc-Kommentar (Zeile 26) der erklaert WARUM dangerouslySetInnerHTML nicht verwendet wird. Kein tatsaechlicher JSX-Attribut-Einsatz. T-08-11 korrekt mitigiert.

Known Stubs

Keine Stubs. LinkWidget ist vollstaendig implementiert und mit dem FavoriteLink-Backend verbunden.

Threat Surface Scan

Alle Threats aus dem Plan-Threat-Register wurden mitigiert. Keine neuen Sicherheitsoberflaechen eingefuehrt:

Threat ID Status
T-08-10 (Spoofing via FavoritesService-Reuse) Mitigiert — FavoritesService userId+widgetId-Scope von Plan 03 uebernommen (kein neuer Endpunkt)
T-08-11 (XSS via icon render) Mitigiert — iconUrl nur via <img src>; kein dangerouslySetInnerHTML
T-08-12 (open redirect) Mitigiert — Anchor mit target="_blank" rel="noreferrer"

Self-Check: PASSED

Item Status
link-widget.tsx FOUND
link-widget.test.tsx FOUND
wireLinkWidget in page.tsx FOUND
link.* keys in de.json FOUND
link.* keys in en.json FOUND
Commit 4657e50 (RED) FOUND
Commit e9914f6 (GREEN) FOUND
Full suite 88/88 PASSED
Web TypeScript: 0 errors PASSED
API TypeScript: 0 errors PASSED