feat: Themen-Inhaltsverzeichnis — Dashboard-Widget + dedizierte Seite /themen #662
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Hintergrund
Nutzer beider Rollen (Leser und Editoren) haben keinen strukturierten Einstiegspunkt, um zu entdecken, welche Themen das Archiv enthält — ohne vorher wissen zu müssen, was sie suchen. Die Geschichten decken nur kuratierten Inhalt ab; die Suche erfordert einen bekannten Begriff. Die vorhandene Tag-Hierarchie (inkl. API-Endpunkt und Farbsystem) bildet eine vollständige Grundlage für ein browsebares Inhaltsverzeichnis.
Job-to-be-Done:
Wenn ich das Archiv ohne konkreten Auftrag öffne, will ich sehen, welche Themen es gibt, damit ich gezielt tiefer einsteigen kann.
Daten-Grundlage
Endpunkt (vorhanden):
GET /api/tags/treeAntwort-Typ:
TagTreeNodeDTO { id, name, color, documentCount, children[], parentId }Hinweis:
colorist bereits der aufgelöste Farbwert des Root-Vorfahren (z. B."sage"). Kinder erben automatisch.Filterregel (Frontend): Ein Tag wird nur angezeigt, wenn er selbst oder mindestens ein Nachfahre
documentCount > 0hat. Leere Äste werden vollständig ausgeblendet.Farbtoken: CSS-Variable
--c-tag-{color}(z. B.--c-tag-sage). Diese Variablen sind palette-fest und ändern sich nicht zwischen Light und Dark Mode — nur der umgebende Kontrast wechselt.Teil 1: Dashboard-Widget
Platzierung
ReaderPersonChips, vor demgrid grid-cols-1 gap-1.5 sm:grid-cols-2(RecentDocs / RecentStories)DashboardFamilyPulseundDashboardActivityFeedVisuelle Struktur
▓= farbiger Balken (4 px breit, volle Kartenhöhe), Farbe =--c-tag-{color}In der 320 px-Sidebar: Grid wechselt auf 1-spaltig.
Impl-Ref: Widget
rounded-sm border border-line bg-surface shadow-sm p-5text-xs font-bold uppercase tracking-widest text-ink-3 font-sans mb-4text-xs font-sans text-brand-mint hover:underline underline-offset-2 focus-visible:ring-2 focus-visible:ring-brand-navy outline-nonegrid grid-cols-2 gap-2(in Sidebar:grid-cols-1)flex items-stretch rounded-sm border border-line bg-canvas hover:bg-surface cursor-pointer focus-visible:ring-2 focus-visible:ring-brand-navy outline-none overflow-hiddenw-1 flex-shrink-0 self-stretch+style="background: var(--c-tag-{color})"flex flex-col justify-center px-3 py-3 gap-0.5 flex-1 min-w-0text-sm font-serif font-semibold text-ink truncatetext-xs font-sans tabular-nums text-ink-3Breakpoints: Widget
grid-cols-1grid-cols-2grid-cols-2grid-cols-1Teil 2: Dedizierte Seite
/themenRoute:
frontend/src/routes/themen/+page.svelte++page.server.tsVisuelle Struktur
▓▓= farbiger Balken oben (6 px hoch, volle Kartenbreite), Farbe =--c-tag-{color}Impl-Ref: Seite
max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8text-2xl font-serif font-semibold text-ink mb-6<BackButton>(bestehende Komponente)grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3rounded-sm border border-line bg-surface shadow-sm overflow-hiddenh-1.5 w-full flex-shrink-0+style="background: var(--c-tag-{color})"flex items-center justify-between px-4 pt-4 pb-3 hover:bg-canvas focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-inset outline-nonetext-base font-serif font-semibold text-inktext-sm font-sans tabular-nums text-ink-3 ml-auto mr-1h-3.5 w-3.5 text-brand-mint flex-shrink-0+aria-hidden="true"border-t border-line mx-4flex items-center justify-between px-4 py-2.5 hover:bg-canvas focus-visible:bg-canvas focus-visible:outline-nonepy-2.5text-sm font-sans text-inktext-xs font-sans tabular-nums text-ink-3 ml-auto mr-1h-3 w-3 text-brand-mint flex-shrink-0+aria-hidden="true"px-4 py-2.5 text-sm font-sans text-ink-3 hover:text-ink hover:bg-canvas blockBreakpoints: Seite
grid-cols-1sm:grid-cols-2lg:grid-cols-3Bei mehr Kindern als das Maximum erscheint eine „+ N weitere →"-Zeile, die zur gefilterten Suche nach dem Root-Tag verlinkt.
Navigation / Links
/themen/mit Tag-ID als aktivem Suchfilter/themenKarten-Header/mit Root-Tag-ID als aktivem Suchfilter/themenKind-Zeile/mit Kind-Tag-ID als aktivem SuchfilterDie Suchseite expandiert Tags bereits auf Nachfahren (bestehende Logik in
DocumentService).Accessibility
<a href="...">, kein<button>aria-label="{name}, {count} Dokumente"aria-hidden="true"(dekorativ)focus-visible:ring-2 focus-visible:ring-brand-navyauf allen interaktiven Elemententext-ink-Texte erfüllen ≥ 4.5:1 in Light und Dark Mode (semantische Tokens sichern das automatisch)Edge Cases
documentCount = 0, aber Kinder mit DocsdocumentCount = 0Acceptance Criteria
Out of Scope
/themen(Kind-Tags sind immer sichtbar, kein Toggle)/themen(Kinder von Kindern werden nicht angezeigt)👨💻 Felix Brandt — Senior Fullstack Developer
Observations
1. Tag-Navigation-URL ist falsch spezifiziert — kritisch
Die Navigations-Tabelle sagt: „
/mit Tag-ID als aktivem Suchfilter". Das ist falsch. Der Such-Endpunkt nimmt Tag-Namen als String-Parameter:Bestehende Komponenten zeigen das korrekte Muster:
Im Spec muss „Tag-ID" durchgehend durch „Tag-Name (URL-encoded)" ersetzt werden. Alle vier Zeilen in der Navigations-Tabelle sind davon betroffen.
2.
documentCountist direkter Zähler, nicht rekursivTagService.buildTree()setztdocumentCount = counts.getOrDefault(tag.getId(), 0L)— das istCOUNT(*) FROM document_tags WHERE tag_id = ?, nur direkte Dokumente. Ein Root-Tag mit 0 eigenen Docs aber 42 Kind-Docs hatdocumentCount = 0.Die Issue-Filterregel lautet: „Tag wird angezeigt, wenn er selbst oder mindestens ein Nachfahre > 0 hat". Das erfordert eine rekursive Tree-Walk-Funktion auf dem Frontend — die nicht trivial ist und explizit als
$derived.by()umgesetzt werden muss. Empfehlung: dies als eigene benannte Hilfsfunktion auslagern:3. Sidebar-Prop nicht aufgelöst
Die Spec sagt „per Prop oder Wrapper-Klasse zu steuern" — das ist unentschieden. Für einen implementierenden Agenten braucht es eine klare Entscheidung. Empfehlung:
compact: boolean = falseals Prop aufThemenWidget.svelte:4. i18n-Keys fehlen im Spec
Folgende Strings müssen in
messages/{de,en,es}.jsonergänzt werden — das Spec nennt sie weder noch delegiert es die Entscheidung:themen_widget_titlethemen_allethemen_leerthemen_weiterethemen_dokumente5. Dokumentation muss mit aktualisiert werden
Neue SvelteKit-Route → per CLAUDE.md-Konvention:
CLAUDE.mdRouten-Tabelle:/themeneintragendocs/architecture/c4/l3-frontend-*.puml: neue Route ergänzenRecommendations
encodeURIComponent(tag.name))" ersetzencompact-Prop explizit in den Spec aufnehmen und die Sidebar-Variante damit auflösenhasAnyDocuments()-Funktion explizit benennen — verhindert, dass der Agent Business-Logik in Template-Markup schreibt🏗️ Markus Keller — Application Architect
Observations
1. Backend ist komplett fertig — kein einziger Backend-Commit nötig
GET /api/tags/treeist vorhanden, sicher (Auth-Guard über globale Spring Security), und bereits in den generierten TypeScript-Typen (api.ts:935). Das ist eine reine Frontend-Aufgabe. Das spart erheblich Zeit und Risiko.2.
documentCountist per-Tag (direkt), nicht aggregiertIch habe
TagService.buildTree()gelesen:Die
counts-Map kommt vonSELECT tag_id, COUNT(*) FROM document_tags GROUP BY tag_id— ausschließlich direkte Beziehungen. Ein Root-TagBriefemit KindernBrautbriefe (18),Kriegsbriefe (12)aber 0 eigenen Docs hatdocumentCount = 0im DTO.Das hat zwei Konsequenzen für die Spec:
0— was für Nutzer verwirrend ist (Karte sichtbar, aber Header zeigt 0). Das Issue-Edge-Case sagt: „eigener Zähler zeigt 0 (oder wird weggelassen)" — das „oder wird weggelassen" sollte explizit entschieden werden. Empfehlung: den Zähler weglassen wenndocumentCount = 0, um Verwirrung zu vermeiden.3. Datenfluss: zwei unabhängige Aufrufe sind richtig
Dashboard
+page.server.tsund/themen/+page.server.tsrufen beideGET /api/tags/treeauf — unabhängig. Das ist korrekt. Ein geteilter Store oder ein „einmaliges Laden" wäre Overengineering für diesen Anwendungsfall. Die Antwort ist klein (flacher Tag-Baum einer Familiengröße) und der Endpunkt ist bereits gecacht durch HTTP connection reuse.4. Dokumentations-Pflicht
Per Architektur-Konvention gilt bei neuen SvelteKit-Routen:
CLAUDE.mdRouten-Tabelle →/themenergänzendocs/architecture/c4/l3-frontend-*.puml→ neue Route und die neuen Komponenten (ThemenWidget,ThemenPage) ergänzenKein ADR nötig — keine Entscheidung mit dauerhafter struktureller Konsequenz.
5. Kein Caching-Problem
Der Tag-Baum einer Familienarchiv-Instanz hat typischerweise 10–50 Tags. Der Tree-Endpunkt gibt ein paar Kilobyte JSON zurück. Kein Caching, keine Pagination, keine Virtualisierung nötig. Die Einfachheit ist richtig.
Recommendations
documentCount = 0aber Kindern mit Docs" explizit entscheiden: Zähler weglassen (empfohlen) oder 0 anzeigen🔒 Nora "NullX" Steiner — Security Engineer
Observations
1. Auth-Status des Endpunkts: korrekt
GET /api/tags/treeträgt kein@RequirePermission— das ist beabsichtigt und richtig. Die globale Spring Security Konfiguration erzwingtanyRequest().authenticated(), d. h. der Endpunkt ist für alle eingeloggten Nutzer (Leser und Editoren) erreichbar, ohne eine Schreibberechtigung zu verlangen. Tags sind Metadaten, die alle sehen dürfen.2. Tag-Name im URL:
encodeURIComponentist PflichtDer Such-Link zu
/?tag={tag.name}ist sicher, wenn er korrekt kodiert wird. Bestehende Komponenten machen das bereits richtig:Die Navigation-Tabelle im Issue referenziert aber fälschlich „Tag-ID" — wenn ein Implementierer das als UUID-String übernimmt, fehlt das URL-Encoding für Namen mit Sonderzeichen. Die Korrektur ist ohnehin nötig (Tag-ID → Tag-Name), und
encodeURIComponentmuss explizit im Spec stehen.3. Inline-Style mit CSS-Variable: kein XSS-Risiko
tag.colorkommt aus einem Backend-Enum mit 10 fixen Werten (sage,sienna,amberusw.).TagService.validateColor()wirft eineDomainExceptionbei unbekannten Werten. Der interpolierte String landet in einer CSS-Variablen-Referenz, nicht als ausführbarer Code. Kein XSS-Vektor.4. Keine neuen Angriffsflächen
Das Feature ist rein lesend, verwendet einen bestehenden Endpunkt, fügt keine Eingaben hinzu und schreibt keine Daten. Aus Security-Perspektive ist das ein Low-Risk-Feature.
Recommendations
encodeURIComponent(tag.name)explizit in den Navigations-Spec schreiben — nicht als Impl-Detail dem Agenten überlassenGET /api/tags/treeerfordert Authentifizierung (Standard-Auth-Guard), kein separates@RequirePermissionnötigOpen Decisions (omit this section entirely if none)
Keine. Alle Security-Aspekte sind durch bestehende Muster abgedeckt.
🧪 Sara Holt — QA Engineer
Observations
1. Fehlende Gherkin-Szenarien für spezifizierte Edge Cases
Die Edge-Case-Tabelle enthält „API-Fehler → Seite zeigt Fehlertext mit Retry-Hinweis" — aber kein einziges Gherkin-Szenario deckt diesen Fall ab. Das ist ein Widerspruch: Edge Case definiert, Test fehlt.
Außerdem fehlen Szenarien für:
documentCount = 0aber Kind-Docs > 0: Karte ist sichtbar2. Rekursive Filter-Logik braucht Unit-Test
Die Filterregel „zeige Tag wenn er oder ein Nachfahre > 0 hat" ist Business-Logik, die separat testbar sein muss — als TypeScript-Funktion mit klaren Eingaben:
Diese Funktion in einem
$derived.by()zu verstecken macht sie untestbar. Sie sollte als exportierte Hilfsfunktion in einer eigenen Datei leben.3.
load-Funktion für/themen/+page.server.tsbraucht Integration-TestDas bestehende Muster (
import { load } from './+page.server'+vi.fn()als Mock-Fetch) deckt folgende Fälle ab:Ohne diesen Test ist das API-Fehler-Edge-Case nur dokumentiert, nicht verifiziert.
4. Touch-Target-Szenario ist nicht automatisierbar wie geschrieben
Then hat jede interaktive Zeile eine Mindesthöhe von 44 px— Playwright kann keine berechneten CSS-Höhen direkt assertieren. Das Szenario sollte entweder:getBoundingClientRect()-Assertion in Playwright:Ohne
data-testidoder eine klare Selektor-Strategie ist dieses Szenario für den Agenten nicht umsetzbar.Recommendations
$derived)data-testid="themen-child-row"auf Kind-Zeilen und Karten-HeaderOpen Decisions
data-testid-Attribute auf interaktiven Elementen oder Playwright-Rollen-Selektoren? Die bestehende Codebase nutzt keinedata-testid-Konvention — wenn wir sie hier einführen, sollte das bewusst entschieden werden.🚢 Tobias Wendt — DevOps & Platform Engineer
Observations
Rein Frontend — keine Infrastruktur-Änderungen nötig
Dieses Feature berührt ausschließlich SvelteKit-Routen und Dashboard-Komponenten. Keine neuen Docker-Services, keine Compose-Änderungen, keine CI-Pipeline-Anpassungen, keine neuen Umgebungsvariablen.
GET /api/tags/tree— kein Performance-ProblemDer Endpunkt gibt den vollständigen Tag-Baum zurück. Für ein Familienarchiv (10–50 Tags, kleine JSON-Antwort) ist das kein Problem. Kein Caching, keine Pagination nötig.
CI-Pipeline läuft automatisch durch
Die Gitea CI-Pipeline baut und testet das Frontend bei jedem Commit. Die neuen Komponenten und die
/themen-Route fallen automatisch unter die bestehenden Build- und Test-Stages.Eine Anmerkung zur Beobachtbarkeit
Die
/themen-Route ist eine neue SSR-Seite mit einem Backend-Aufruf. Wenn der Endpunkt langsam wird oder fehlschlägt, sieht man das in Loki über die bestehende SvelteKit-Request-Logging-Middleware. Kein separates Monitoring nötig.Keine Bedenken aus Infrastruktur-Perspektive
Alles abgedeckt durch bestehende Infrastruktur. Grünes Licht von meiner Seite.
🗳️ Decision Queue — Action Required
1 Entscheidung braucht dein Input, bevor die Implementierung startet.
Testing
data-testid-Konvention. Zwei Optionen: (A)data-testid="themen-child-row"einführen — explizit, stabil, aber neue Konvention; (B) Playwright-Rollen-Selektoren (getByRole('link', { name: ... })) — kein neues Attribut nötig, aber fragiler bei mehreren gleichnamigen Links. (Raised by: Sara)we skip E2E for now
✅ Implementation complete — branch
worktree-feat+issue-662-themen-inhaltsverzeichnisWhat was built
8 commits, pure frontend — no backend changes needed.
aad8382bhasAnyDocuments(node)recursive helper + 4 unit tests in$lib/shared/utils/tagUtils.ts94d5e696themen_widget_title,themen_alle,themen_leer,themen_weitere,themen_dokumentef376fae6/themen/+page.server.ts— loadsGET /api/tags/tree, throws 500 on failure + 3 server tests41754fc0+page.server.ts— tag tree added to both reader and editor fetch batches1d032f52ThemenWidget.svelte— card grid with 4 px color bars,compactprop for sidebar, empty state + browser tests9d8e9c45+page.svelte— widget wired into reader layout (after PersonChips) and editor sidebar (between FamilyPulse and ActivityFeed,compact={true})9084c8dd/themen/+page.svelte— root-tag cards with 6 px top color bar, up to 5 child rows,+ N weitere →overflow + browser testsfc53c69aKey decisions applied from review comments
/?tag={encodeURIComponent(tag.name)}(tag names, not IDs)hasAnyDocuments()is an exported function in$lib/shared/utils/tagUtils.ts(not inline$derived)documentCount = 0but children with docs: count is omitted (not shown as 0)compact: boolean = falseprop onThemenWidgetcontrols sidebar vs. full-width layoutTests
tagUtils.test.ts— 4 unit tests ✅themen/page.server.spec.ts— 3 server tests ✅ThemenWidget.svelte.spec.ts— 6 browser tests (CI)themen/page.svelte.spec.ts— 6 browser tests (CI)