Files
tessera-ctl/.planning/quick/260917-jdd-favoriten-widget-favicon-ersatzweg-bei-u/260917-jdd-SUMMARY.md
T
schalli efbd6e8974
Tessera CI/CD / Lint & Type Check (push) Successful in 53s
Tessera CI/CD / Tests (push) Successful in 1m5s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 18s
Tessera CI/CD / Build & Publish Images (push) Successful in 4m22s
docs(quick-260917-kgc): Aktenstand — Update in der Desktop-App, alle Nachweise erbracht, Wiedereinstieg bereinigt
Quick 260917-kgc (Plan/Recherche/Bericht/Verifikation) und Schnellfix a6d1a64
in der Quick-Task-Tabelle; Nachweise in allen sechs Berichten nachgetragen
(Playwright lokal, CI-Laeufe 382-384, Windows-Test-VM: In-App-Update
7479cb4 -> a6d1a64). Ueberholte .continue-here-Dateien entfernt,
Desktop-Client-Todo geschlossen.

Dieser Push aendert nichts unter apps/desktop -- er ist zugleich der
Beweisfall 2 des CI-Desktop-Skips (Pakete aus dem Zwischenspeicher).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g2npLxzH5gZpg8s2S6vKh
2026-09-18 11:29:14 +02:00

17 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions requirements-completed coverage duration completed status
quick-260917-jdd 01 dashboard-favorites
nestjs
undici
prisma
rls
nextjs
react
vitest
ssrf
favicon
LENIENT_TLS_AGENT (icon-discovery.service.ts) — Modul-Singleton undici-Agent, toleriert Zertifikatsfehler des Zielhosts in fetchWithRedirectGuard (HTML-Ermittlung und Icon-Byte-Holen)
PUT /favorites/order + FavoritesService.reorder() — transaktionale Sortierung der Favoriten eines Widgets ueber withTenantTransaction()
FavoriteIcon (favorites-widget.tsx) — dreistufiger Browser-Ersatzweg proxy -> direct -> Buchstabe
reorderFavorites (favorites-api.ts) — Web-Client fuer PUT /favorites/order
favorites
dashboard-widgets
tokens tasks commits plan_head_before
58000 3 3 e7633e15de
added patterns
undici@7.28.0 (@tessera/api, direkte Abhaengigkeit — bereits im Lockfile aufgeloest ueber cheerio/jsdom, kein neuer Download)
Dispatcher-Option pro Aufruf (undicis eigenes fetch) statt prozessweiter NODE_TLS_REJECT_UNAUTHORIZED-Abschaltung — Nodes globales fetch ignoriert einen undici-Agent, deshalb der Modulimport von undici statt des globalen fetch
withTenantTransaction() als atomare Mehrschritt-Form fuer transaktionale Schreibzugriffe ohne Benutzerdimension in der Sitzung — jede Bedingung im Callback traegt userId UND widgetId selbst (zweites Netz)
Dreistufiger Browser-Ersatzweg fuer Bilder, die der Server nicht liefern kann (SSRF-Schutz lehnt interne Hosts bewusst ab): Server-Proxy -> Direktbild aus dem Browser des Nutzers (referrerPolicy no-referrer) -> Buchstaben-Platzhalter, React-key setzt die Stufe bei URL-Wechsel zurueck
created modified
apps/api/src/favorites/dto/reorder-favorites.dto.ts
apps/api/package.json
pnpm-lock.yaml
apps/api/src/favorites/icon-discovery.service.ts
apps/api/src/favorites/icon-discovery.service.spec.ts
apps/api/src/favorites/favorites.service.ts
apps/api/src/favorites/favorites.service.spec.ts
apps/api/src/favorites/favorites.controller.ts
apps/api/src/prisma/prisma-tenant.extension.ts
apps/web/src/lib/favorites-api.ts
apps/web/src/components/dashboard/widgets/favorites-widget.tsx
apps/web/src/components/dashboard/widgets/favorites-widget.test.tsx
apps/web/src/messages/de.json
apps/web/src/messages/en.json
CHANGELOG.md
docs/anleitung-anwender.md
docs/mandantentrennung-zugriffsklassifikation.md
undici als direkte Abhaengigkeit statt eines neuen Downloads: 7.28.0 lag bereits im Lockfile ueber cheerio@1.2.0/jsdom aufgeloest; `pnpm add undici@7.28.0 --offline` macht daraus eine direkte Abhaengigkeit ohne neues Major und ohne Netzabruf (Plan-Vorgabe, uebernommen).
Test-Extraktion der Link-Reihenfolge ueber `a.querySelector('.truncate')` statt `a.textContent` (Abweichung vom Plan-Wortlaut, siehe Deviations): der Buchstaben-Platzhalter liegt IMMER im selben `<a>` wie der Titel-Span, `a.textContent` haette deshalb den Buchstaben vor dem Titel mitgezaehlt (z. B. "GGitHub" statt "GitHub") und die im Plan geforderte exakte Array-Gleichheit waere nie gruen geworden.
QUICK-260917-JDD
id description requirement verification human_judgment
D1 icon-discovery.service.ts holt HTML und Icon-Bytes ueber undicis eigenes fetch mit LENIENT_TLS_AGENT als dispatcher in der einzigen Ausgangsstelle fetchWithRedirectGuard; SSRF-Schutz (isPublicHttpUrl je Hop, MAX_REDIRECTS, Timeout, Groessendeckel) unveraendert QUICK-260917-JDD
kind ref status
unit apps/api/src/favorites/icon-discovery.service.spec.ts — 3 neue Dispatcher-Tests (dispatcher-Instanz+options, redirect:manual, Singleton), 16 bestehende SSRF/Discovery-Tests unveraendert gruen pass
false
id description requirement verification human_judgment
D2 PUT /favorites/order (vor den :id-Routen) + FavoritesService.reorder() setzt position=index fuer exakt die Favoriten eines Widgets in EINER withTenantTransaction; fremde/unbekannte/fehlende/doppelte ids -> BadRequestException ohne Teilschreibung QUICK-260917-JDD
kind ref status
unit apps/api/src/favorites/favorites.service.spec.ts — 7 neue reorder-Tests (Happy Path, fremde id, unbekannte id, Teilmenge, Duplikat, fremder Mandant, Wachhund forTenant=0/withTenantTransaction=1) pass
false
id description requirement verification human_judgment rationale
D3 Widget zeigt bei fehlendem Server-Symbol das Direktbild aus dem Browser (referrerPolicy no-referrer, nur http/https) und danach den Buchstaben; Pfeile im Bearbeitungsmodus sortieren optimistisch und persistieren ueber PUT /favorites/order, Fehler laedt neu QUICK-260917-JDD
kind ref status
unit apps/web/src/components/dashboard/widgets/favorites-widget.test.tsx — 5 neue Tests (Ersatzbild bei iconUrl null, Kette Proxy->direkt->Buchstabe, kein Direktbild bei ftp://, Pfeilzustand+Klick, Fehlerpfad); 11 bestehende Tests unveraendert gruen pass
true Der tatsaechliche Beweis ueber die Netzgrenze (echter Host mit Zertifikatsfehler, echter interner Host, Sortierung ueber Reload hinweg) ist laut Plan Aufgabe des Orchestrators im Browser — siehe Abschnitt unten.
~40min 2026-09-17 complete

Quick Task 260917-jdd: Favoriten-Widget — Symbol-Ersatzweg bei Zertifikatsfehler/interner Adresse, manuelle Sortierung Summary

Favoriten holen ihr Symbol jetzt trotz Zertifikatsfehlern (undici-Dispatcher mit toleranter TLS-Pruefung serverseitig) oder ueber einen Browser-Ersatzweg bei internen Adressen, und lassen sich im Bearbeitungsmodus per Pfeilen in eine gewuenschte Reihenfolge bringen (PUT /favorites/order, transaktional, mit Existenzorakel-Vermeidung).

Performance

  • Duration: ~40 min
  • Completed: 2026-09-17
  • Tasks: 3/3
  • Files modified: 16 (1 neu, 15 geändert)

Accomplishments

Teil A — Symbol trotz Zertifikatsfehler / interner Adresse

  • icon-discovery.service.ts: LENIENT_TLS_AGENT = new Agent({ connect: { rejectUnauthorized: false } }) als Modul-Singleton; fetchWithRedirectGuard (die einzige Ausgangsstelle fuer HTML-Ermittlung UND Icon-Byte-Holen) ruft jetzt undiciFetch(url, { dispatcher: LENIENT_TLS_AGENT, redirect: 'manual', signal, headers }) statt des globalen fetch — Nodes globales fetch ignoriert einen undici-Agent (gemessen: self-signed.badssl.com liefert ueber undici 200, ueber global fetch DEPTH_ZERO_SELF_SIGNED_CERT). DNS-Pruefung, Redirect-Limit, Timeout, Groessendeckel, HTML-Zeichenbegrenzung bleiben unangetastet
  • undici@7.28.0 als direkte Abhaengigkeit von @tessera/api (bereits im Lockfile aufgeloest, pnpm add --offline, kein neuer Download); pnpm install --frozen-lockfile --offline gruen
  • favorites-widget.tsx: neue Unterkomponente FavoriteIcon mit den Stufen proxy (Server-Proxy /api-proxy/favorites/:id/icon) → direct (Browser-Direktbild {origin}/favicon.ico, referrerPolicy="no-referrer", nur http/https ueber getDirectFaviconSrc) → none (Buchstaben-Platzhalter, liegt immer darunter); key={iconUrl|url} setzt die Stufe bei Aenderung zurueck; kein style.display-Hack mehr
  • GET /favorites/:id/icon sendet zusaetzlich X-Content-Type-Options: nosniff und eine restriktive Content-Security-Policy (T-JDD-02, Haertung fuer den Fall eines direkt im Tab geoeffneten SVG)

Teil B — manuelle Sortierung mit Pfeilen

  • ReorderFavoritesDto (neu): widgetId (@IsUUID), ids (@IsArray @ArrayMinSize(1) @ArrayMaxSize(500) @ArrayUnique @IsUUID('all', {each:true}))
  • @Put('order') im Controller VOR den :id-Routen (NestJS-Route-Order)
  • FavoritesService.reorder(): EINE withTenantTransaction()-Transaktion — findMany prueft EXAKTE Uebereinstimmung der ids mit den Favoriten des Widgets, dann je id updateMany({ where: { id, userId, widgetId }, data: { position: index } }) mit count === 1-Pruefung; jede Abweichung (fremde/unbekannte/fehlende id, fremdes Widget, fremder Mandant) wirft DIESELBE BadRequestException (Existenzorakel-Vermeidung, T-JDD-06); doppelte ids scheitern VOR der Transaktion
  • favorites-widget.tsx: handleMove tauscht optimistisch in sortedFavorites, setzt position=index fuer alle, ruft reorderFavorites; Erfolg uebernimmt die Server-Antwort, Fehler zeigt favorites.error und laedt per fetchFavorites neu; Pfeile (nur bei nicht-inline-Bearbeitung) mit disabled am ersten/letzten Eintrag, sichtbar in Listen- und Kachelansicht
  • reorderFavorites(widgetId, ids) im Web-Client (PUT /favorites/order)
  • i18n: widgets.favorites.moveUpButton/moveDownButton (de/en)

Dokumentation

  • CHANGELOG.md: je ein Stichpunkt unter ### Neu (Sortierpfeile) und ### Behoben (Symbol trotz Zertifikatsfehler)
  • docs/anleitung-anwender.md: Tabellenzeile „Favoriten" erweitert um Pfeile und Browser-Ersatzweg (eine Zeile geblieben)
  • docs/mandantentrennung-zugriffsklassifikation.md: Nachtrag zu favoriteLink — reorder() laeuft ueber withTenantTransaction() ohne Benutzerdimension in der Sitzung, Stand bleibt gebunden
  • prisma-tenant.extension.ts: Kopfkommentar-Nachtrag — favorites.service.ts (reorder) ist der erste Nutzer-CRUD-Aufrufer von withTenantTransaction(); NUR Kommentartext, Funktionscode unveraendert

Befund am Code (uebernommen aus dem Plan, wichtig fuer die Browser-Nachweise unten)

discoverFavoriteIconUrl liefert NIE null, sondern bei jedem Fehler den Origin-Rueckfall https://host/favicon.ico. Fuer einen internen Host steht also https://intern/favicon.ico in iconUrl, das Widget rendert zunaechst das Proxy-Bild, der Proxy antwortet 502 (SSRF-Schutz lehnt ab), onError schaltet auf die Direktbild-Stufe. Die Browser-Stufe haengt deshalb korrekt an onError des Proxy-Bildes UND an iconUrl === null — nicht nur an letzterem, wie eine naive Lesart nahelegen wuerde.

Task Commits

Each task was committed atomically:

  1. Task 1: API — undici-Dispatcher fuer beide Icon-Pfade, PUT /favorites/order mit transaktionalem reorder(), Specs - 2a562d0 (feat)
  2. Task 2: Web — reorderFavorites, FavoriteIcon mit Browser-Ersatzweg, Sortierpfeile, i18n, Tests - b18ac25 (feat)
  3. Task 3: CHANGELOG, Anwenderhandbuch, zwei Nachtraege - b023d6f (docs)

Plan metadata: wird vom Orchestrator nach diesem SUMMARY committet.

Beide Task-1- und Task-2-Aenderungen (tdd="true") folgten RED→GREEN: Tests wurden vor der Implementierung geschrieben und liefen zunaechst rot (Task 1: 9 fehlschlagende Tests — service.reorder is not a function, init.dispatcher undefined; Task 2: alle 5 neuen Tests haetten ohne FavoriteIcon/handleMove/reorderFavorites fehlgeschlagen), dann gruen nach Implementierung. Task 2 traegt die Tracer-Rolle (einzige lokal Ende-zu-Ende pruefbare Kette: Klick → optimistische Reihenfolge → reorderFavorites → bei Fehler Neuladen; Proxy-Bild → onError → Direktbild → onError → Buchstabe) — der automatisierte <verify>-Block wurde nach dem Commit erneut vollstaendig gruen ausgefuehrt (Tracer-Feedback-Gate, automatisiert, kein Checkpoint noetig).

Files Created/Modified

  • apps/api/package.json, pnpm-lock.yaml — undici 7.28.0 als direkte Abhaengigkeit von @tessera/api
  • apps/api/src/favorites/icon-discovery.service.ts — LENIENT_TLS_AGENT, undiciFetch in fetchWithRedirectGuard, Rueckgabetyp UndiciResponse
  • apps/api/src/favorites/icon-discovery.service.spec.ts — vi.mock('undici'), 3 neue Dispatcher-Tests
  • apps/api/src/favorites/dto/reorder-favorites.dto.ts — neu
  • apps/api/src/favorites/favorites.controller.ts — @Put('order') vor den :id-Routen, zwei Header am Icon-Proxy
  • apps/api/src/favorites/favorites.service.ts — reorder() ueber withTenantTransaction
  • apps/api/src/favorites/favorites.service.spec.ts — Mock/Fake um withTenantTransaction/updateMany erweitert, 7 neue reorder-Tests
  • apps/api/src/prisma/prisma-tenant.extension.ts — ein Kommentar-Nachtrag (Task 3)
  • apps/web/src/lib/favorites-api.ts — reorderFavorites
  • apps/web/src/components/dashboard/widgets/favorites-widget.tsx — getDirectFaviconSrc, FavoriteIcon, handleMove, Sortierpfeile
  • apps/web/src/components/dashboard/widgets/favorites-widget.test.tsx — Mock um reorderFavorites erweitert, 5 neue Tests
  • apps/web/src/messages/de.json, en.json — zwei Schluessel
  • CHANGELOG.md, docs/anleitung-anwender.md, docs/mandantentrennung-zugriffsklassifikation.md — Stichpunkte/Saetze (Task 3)

Decisions Made

  • undici als direkte Abhaengigkeit statt neuem Download — bereits im Lockfile aufgeloest, exakt gepinnt auf 7.28.0 wie im Plan vorgegeben.
  • Test-Extraktion der Link-Reihenfolge ueber a.querySelector('.truncate') statt a.textContent — siehe Deviations unten.
  • Keine weiteren Abweichungen von der im Plan vorgegebenen Architektur (Dispatcher-Ort, Transaktionsform, dreistufiger Ersatzweg).

Deviations from Plan

1. [Rule 1 - Test-Bug im Plan-Wortlaut] Link-Reihenfolge im Test nicht ueber a.textContent, sondern a.querySelector('.truncate')?.textContent

  • Found during: Task 2 (Test-Implementierung, vor dem ersten Testlauf)
  • Issue: Der Plan-Text schlug within(...).getAllByRole('link').map(a => a.textContent) vor. Der Buchstaben-Platzhalter (letter-fallback-{id}) liegt aber IMMER im selben <a>-Element wie der Titel-Span (Bestandscode, unveraendert) — a.textContent haette deshalb Buchstabe+Titel konkateniert geliefert (z. B. "GGitHub" statt "GitHub"), und die im Plan geforderte exakte Array-Gleichheit ['Notion', 'GitHub'] waere mit keiner Implementierung gruen geworden.
  • Fix: Test extrahiert stattdessen a.querySelector('.truncate')?.textContent — die CSS-Klasse des Titel-Spans, unveraendert seit Bestand. Keine Aenderung an der Produktionsdatei noetig; nur die Testauswahl wurde praeziser.
  • Files modified: apps/web/src/components/dashboard/widgets/favorites-widget.test.tsx
  • Commit: b18ac25

Issues Encountered

None ueber die dokumentierte Deviation hinaus. pnpm add undici@7.28.0 --offline erzeugte eine bereits bekannte, vorbestehende Peer-Warnung (http-cookie-agent erwartet undici@^5.11.0, findet 7.28.0) — unveraendert seit vorher moeglich (jetzt sichtbar, weil undici erstmals eine direkte statt nur transitive Abhaengigkeit ist), keine Auswirkung auf Build oder Tests, nicht behoben (ausserhalb des Aufgabenbereichs).

User Setup Required

None — keine externe Konfiguration noetig.

Nachweis durch Orchestrator (offen)

Folgende Punkte sind NICHT lokal pruefbar (kein echter Netzzugriff/Browser in dieser Umgebung) und folgen laut Plan durch den Orchestrator:

  • Host mit Zertifikatsfehler: ein Favorit auf https://self-signed.badssl.com/ (oder vergleichbar) zeigt das Symbol ueber den Server-Proxy (icon-proxy-*), nicht ueber das Direktbild — der Server toleriert den Zertifikatsfehler jetzt (undici-Dispatcher), der Browser des Nutzers muesste es sonst gar nicht erst versuchen.
  • Interner Host: ein Favorit auf eine Adresse im Firmennetz (die der SSRF-Schutz des Servers absichtlich ablehnt, Proxy antwortet 502) zeigt das Symbol ueber das Direktbild aus dem Browser des Nutzers (icon-direct-*), sofern der Host per http/https erreichbar ist und (bei https) ein vom Browser vertrautes Zertifikat traegt. Ein http://-Favorit auf einem https://-Tessera ist Mischinhalt und wird vom Browser hochgestuft/blockiert; ein selbstsigniertes Zertifikat ohne Vertrauen im Browser des Nutzers klappt ueber die Direktbild-Stufe NICHT (der Browser laesst sich nicht wie der Server ueberreden).
  • Sortierung ueber Reload hinweg: nach einem Klick auf „Nach oben"/„Nach unten" bleibt die neue Reihenfolge nach einem Neuladen der Seite erhalten (Server-persistiert).
  • Altbestand-Normalisierung: Favoriten mit position = 0 (vor diesem Plan angelegt) ordnen sich beim ERSTEN Sortierklick zu 0..n-1, ohne Datenverlust oder Fehlermeldung.

Kein Blocker fuer weitere Arbeit — API-Suite 68 Dateien/1101 Tests, Web-Suite 64 Dateien/429 Tests, beide type-checks gruen; drei atomare Commits ohne Push, ohne Docker-Build, ohne Schema-Aenderung; .planning/ nicht committet.


Quick Task: 260917-jdd Completed: 2026-09-17

Self-Check: PASSED

All 11 claimed files found on disk; all three task commits (2a562d0, b18ac25, b023d6f) found in git history.

Nachweis durch Orchestrator (2026-09-17, Playwright gegen lokale Container) — erbracht

  • https://self-signed.badssl.com: Symbol ueber den Server-Proxy geladen (180 px) — vorher Buchstabe.
  • http://192.168.13.11:3002 (interner Host): Proxy antwortet 502, Widget laedt http://192.168.13.11:3002/favicon.ico direkt (referrerPolicy no-referrer) — Symbol da.
  • Sortierung: „Nach unten"/„Nach oben" aendern die Reihenfolge sofort; nach Reload bleibt sie; DB-Positionen nach dem ersten Klick 0..3 (Altbestand mit 0 normalisiert).
  • Testfavoriten danach aus der lokalen DB entfernt.