efbd6e8974
Quick 260917-kgc (Plan/Recherche/Bericht/Verifikation) und Schnellfixa6d1a64in der Quick-Task-Tabelle; Nachweise in allen sechs Berichten nachgetragen (Playwright lokal, CI-Laeufe 382-384, Windows-Test-VM: In-App-Update7479cb4->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
204 lines
17 KiB
Markdown
204 lines
17 KiB
Markdown
---
|
|
phase: quick-260917-jdd
|
|
plan: 01
|
|
subsystem: dashboard-favorites
|
|
tags: [nestjs, undici, prisma, rls, nextjs, react, vitest, ssrf, favicon]
|
|
|
|
requires: []
|
|
provides:
|
|
- "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"
|
|
affects: [favorites, dashboard-widgets]
|
|
|
|
actuals:
|
|
tokens: 58000
|
|
tasks: 3
|
|
commits: 3
|
|
plan_head_before: e7633e15de5ee8c6d1d607275b43ce75a9150e1a
|
|
|
|
tech-stack:
|
|
added:
|
|
- "undici@7.28.0 (@tessera/api, direkte Abhaengigkeit — bereits im Lockfile aufgeloest ueber cheerio/jsdom, kein neuer Download)"
|
|
patterns:
|
|
- "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"
|
|
|
|
key-files:
|
|
created:
|
|
- apps/api/src/favorites/dto/reorder-favorites.dto.ts
|
|
modified:
|
|
- 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
|
|
|
|
key-decisions:
|
|
- "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."
|
|
|
|
requirements-completed: [QUICK-260917-JDD]
|
|
|
|
coverage:
|
|
- id: D1
|
|
description: "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"
|
|
requirement: "QUICK-260917-JDD"
|
|
verification:
|
|
- kind: unit
|
|
ref: "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"
|
|
status: pass
|
|
human_judgment: false
|
|
- id: D2
|
|
description: "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"
|
|
requirement: "QUICK-260917-JDD"
|
|
verification:
|
|
- kind: unit
|
|
ref: "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)"
|
|
status: pass
|
|
human_judgment: false
|
|
- id: D3
|
|
description: "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"
|
|
requirement: "QUICK-260917-JDD"
|
|
verification:
|
|
- kind: unit
|
|
ref: "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"
|
|
status: pass
|
|
human_judgment: true
|
|
rationale: "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."
|
|
|
|
duration: ~40min
|
|
completed: 2026-09-17
|
|
status: 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.
|