Files
schalli 3d266418fc
Tessera CI/CD / Lint & Type Check (push) Successful in 51s
Tessera CI/CD / Tests (push) Successful in 1m17s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 18s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m11s
docs(quick-260923-lrr): Favoriten-Symbol hochladen, CHANGELOG, Zugriffsklassifikation nachgetragen
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 16:20:29 +02:00

175 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: quick-260923-lrr
plan: 01
subsystem: api+web (favorites)
tags: [nestjs, prisma, nextjs, upload, cache-busting, favorites]
requires: []
provides:
- "FavoriteLink.uploadedIconMime/iconVersion (Migration 20260923160000)"
- "favorite-icon-files.ts: Erkennung/Pfadbildung/Best-effort-Loeschung fuer hochgeladene Favoriten-Symbole"
- "FavoritesService.uploadIcon/removeUploadedIcon, Vorrang in getIconBytes, Abrufprobe in create/update"
- "POST/DELETE /favorites/:id/icon"
- "Web: uploadFavoriteIcon/removeFavoriteIcon/FavoriteRequestError in favorites-api.ts"
- "FavoritesWidget: versionierte Symbol-Adresse, Datei-Auswahl, Entfernen-Knopf, Fehlermeldung im Formular"
- "DashboardService.removeWidget/deleteDashboard raeumen Symboldateien kaskadiert geloeschter Favoriten auf (T-LRR-07)"
affects: [dashboard, favorites]
actuals:
tokens: 31900
tasks: 2
commits: 2
tech-stack:
added: []
patterns:
- "Dateiablage nach Muster dashboard-images.service.ts: user-files/favorite-icons/<userId>/<id>.<ext>, Dateiname immer servergeneriert"
- "Versionszaehler (iconVersion) statt updatedAt fuer Cache-Busting einer Bild-URL"
- "Abrufprobe vor dem Speichern einer externen URL statt stiller Speicherung eines nicht ladbaren Wertes"
key-files:
created:
- apps/api/src/favorites/favorite-icon-files.ts
- apps/api/src/favorites/favorite-icon-files.spec.ts
- apps/api/src/favorites/favorites.controller.spec.ts
- apps/api/prisma/migrations/20260923160000_favorite_icon_upload/migration.sql
- apps/web/src/lib/favorites-api.test.ts
modified:
- apps/api/src/favorites/favorites.service.ts
- apps/api/src/favorites/favorites.controller.ts
- apps/api/src/dashboard/dashboard.service.ts
- apps/web/src/lib/favorites-api.ts
- apps/web/src/components/dashboard/widgets/favorites-widget.tsx
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
key-decisions:
- "iconVersion statt updatedAt als Cache-Bust-Quelle, weil Umsortieren/Titelaenderung sonst jedes Symbol neu laden liessen"
- "Hochgeladenes Symbol hat Vorrang vor iconUrl; fehlt die Datei trotz gesetztem Typ, faellt der Dienst protokolliert auf iconUrl zurueck statt 404"
- "Abrufprobe nur bei neuer/abweichender iconUrl, nicht bei jedem Speichern — vermeidet unnoetige Netzwerkaufrufe"
- "T-LRR-07 (im Plan als Restrisiko akzeptiert) zusaetzlich geschlossen: DashboardService raeumt Symboldateien kaskadiert geloeschter Favoriten jetzt best effort auf"
requirements-completed: [QUICK-260923-lrr]
duration: 45min
completed: 2026-09-23
status: complete
---
# Quick-Aufgabe 260923-lrr: Favoriten — eigenes Symbol hochladen, Zwischenspeicher nach Änderung erneuern (Summary)
**Favoriten-Symbole bekommen eine versionierte Adresse (`?v=<iconVersion>`), ein hochgeladenes eigenes Symbol (PNG/JPEG/GIF/WebP/ICO/SVG, ≤512 KB) hat Vorrang vor der Logo-Adresse, und eine serverseitige Abrufprobe weist eine nicht ladbare Logo-Adresse beim Speichern mit einer deutschen Meldung ab statt sie still zu übernehmen.**
## Ausgangslage
Der Nutzer meldete: Eine eigene Logo-Adresse eintragen „bewirkt nichts“ (Verdacht: Cloudflare-Prüfung vor dem Bild), und selbst eine tatsächlich abrufbare neue Adresse erschien wegen eines 24-Stunden-Zwischenspeichers einen Tag lang nicht. Beide Ursachen wurden am Code bestätigt (siehe `260923-lrr-PLAN.md`, Abschnitt „Ausgangslage“) und in diesem Lauf behoben; zusätzlich lässt sich jetzt ein eigenes Symbol hochladen.
## Performance
- **Dauer:** ca. 45 Minuten
- **Aufgaben:** 2 von 2 geplanten Aufgaben ausgeführt (Aufgabe 3, Browser-Nachweis, ist ein separater Checkpoint und wird vom Orchestrator im Anschluss durchgeführt — siehe unten)
- **Geänderte/neue Dateien:** 19
## Aufgaben-Commits
1. **Aufgabe 1: API — Symbol hochladen/entfernen, Vorrang, Versionszähler, Abrufprobe** — `7704372` (feat)
2. **Aufgabe 2: Web — versionierte Symbol-Adresse, Datei-Auswahl, Entfernen-Knopf, Texte** — `61f95c8` (feat)
Beide Commits liegen direkt auf `main` (Orchestrator-Vorgabe für diesen Lauf: kein Worktree, `git.allow_default_branch_commits: true` in `.planning/config.json`).
## Accomplishments
- **Zwischenspeicher-Fehler behoben:** `GET /favorites/:id/icon` sendet jetzt `Cache-Control: private, max-age=86400` (statt `public`); die Bildadresse im Widget trägt `?v=<iconVersion>`, das bei jeder Änderung der Symbolquelle um eins steigt — ein geändertes Symbol erscheint jetzt ohne Neuladen der Seite.
- **Cloudflare-Fall behoben:** Eine neue, explizit eingetragene Logo-Adresse wird beim Speichern einmal serverseitig abgerufen (`assertIconUrlLoadable`, 4 s Zeitgrenze über den vorhandenen `IconDiscoveryService`); scheitert der Abruf, antwortet die API mit 422 und einer deutschen Meldung im Formular — nichts wird gespeichert. Keine Umgehung von Bot-Sperren.
- **Eigenes Symbol hochladen:** Neue Felder `FavoriteLink.uploadedIconMime`/`iconVersion` (Migration `20260923160000_favorite_icon_upload`), Ablage unter `user-files/favorite-icons/<userId>/<id>.<ext>` (Dateiname immer servergeneriert, Muster `dashboard-images.service.ts`), Erkennung von PNG/JPEG/GIF/WebP/ICO/SVG an den Bytes (`favorite-icon-files.ts`). Vorrang vor `iconUrl` beim Ausliefern. Hochladen und Entfernen im Bearbeitungs- und Hinzufügen-Formular des Widgets.
- **T-LRR-07 zusätzlich geschlossen** (im Plan als Restrisiko mit Disposition „accept“ eingetragen, siehe unten): Löscht ein Nutzer ein ganzes Widget oder einen Dashboard-Reiter, entfernt die Datenbank-Kaskade (`onDelete: Cascade`) die betroffenen `FavoriteLink`-Zeilen, ohne den Favoriten-Dienst zu durchlaufen — dessen Datei-Aufräumung griff dort bisher nicht. `DashboardService.removeWidget`/`deleteDashboard` merken sich jetzt vor der Kaskade, welche Favoriten ein hochgeladenes Symbol tragen, und entfernen deren Dateien danach best effort (nie blockierend für das Löschen selbst).
## Dateien erstellt/geändert
**API:**
- `apps/api/prisma/schema.prisma` — `FavoriteLink.uploadedIconMime`/`iconVersion`
- `apps/api/prisma/migrations/20260923160000_favorite_icon_upload/migration.sql` — neue Migration, lokal angewendet (siehe Verifikation)
- `apps/api/src/favorites/favorite-icon-files.ts` (neu) — Erkennung, Pfadbildung, best-effort Dateientfernung
- `apps/api/src/favorites/favorite-icon-files.spec.ts` (neu) — 15 Tests
- `apps/api/src/favorites/favorites.service.ts` — `uploadIcon`/`removeUploadedIcon`, Abrufprobe, Vorrang in `getIconBytes`
- `apps/api/src/favorites/favorites.service.spec.ts` — 49 Tests (24 neu)
- `apps/api/src/favorites/favorites.controller.ts` — `POST`/`DELETE /favorites/:id/icon`, `Cache-Control: private`
- `apps/api/src/favorites/favorites.controller.spec.ts` (neu) — 7 Tests
- `apps/api/src/dashboard/dashboard.service.ts` — T-LRR-07-Aufräumung in `removeWidget`/`deleteDashboard`
- `apps/api/src/dashboard/dashboard.service.spec.ts` — 66 Tests (5 neu)
- `docs/anleitung-betrieb.md` — `user-files/favorite-icons/` ergänzt
**Web:**
- `apps/web/src/lib/favorites-api.ts` — `FavoriteRequestError`, `uploadFavoriteIcon`/`removeFavoriteIcon`, `FAVORITE_ICON_MAX_BYTES`
- `apps/web/src/lib/favorites-api.test.ts` (neu) — 10 Tests
- `apps/web/src/components/dashboard/widgets/favorites-widget.tsx` — versionierte Symbol-Adresse, Datei-Auswahl, Entfernen-Knopf, Fehlermeldung im Formular
- `apps/web/src/components/dashboard/widgets/favorites-widget.test.tsx` — 27 Tests (10 neu, 1 bestehender Test an `?v=0` angepasst)
- `apps/web/src/messages/de.json`/`en.json` — neun neue Texte unter `widgets.favorites`
- `CHANGELOG.md`, `docs/anleitung-anwender.md` — ergänzt
## Entscheidungen
- **iconVersion statt updatedAt:** `updatedAt` scheidet als Versionsquelle aus, weil Umsortieren oder eine Titeländerung sonst jedes Symbol neu laden ließen. `iconVersion` steigt gezielt nur bei einer Änderung der Symbolquelle.
- **Vorrang und Rückfall:** Ein hochgeladenes Symbol hat Vorrang vor `iconUrl`. Fehlt die Datei trotz gesetztem Typ (praktisch nur bei einer manuellen Änderung am Dateisystem denkbar), protokolliert der Dienst eine Warnung und fällt auf `iconUrl` zurück, statt 404 zu werfen — das entspricht dem im Plan festgelegten Verhalten.
- **Abrufprobe nur bei Änderung:** Die Probe läuft nur, wenn eine neue oder gegenüber der Zeile abweichende `iconUrl` übergeben wird — nicht bei jedem Speichern. Vermeidet unnötige Netzwerkaufrufe beim bloßen Ändern von Titel oder Reihenfolge.
## Abweichungen vom Plan
### Vom Orchestrator angeordnete Zusatzanforderung (kein Regelabweichungsfund, sondern expliziter Auftrag)
**1. T-LRR-07 geschlossen — Datei-Leichen nach Kaskadenlöschung eines Widgets/Reiters**
- **Gefunden während:** vor Aufgabe 1, auf ausdrückliche Anweisung des Orchestrators (Constraint „closes the plan's accepted gap T-LRR-07“)
- **Befund:** `DashboardService.removeWidget` (einzelnes Widget) und `DashboardService.deleteDashboard` (ganzer Reiter) löschen `WidgetInstance`-Zeilen; `FavoriteLink.widgetId` trägt `onDelete: Cascade`, wodurch die Datenbank die zugehörigen Favoriten-Zeilen mitlöscht, OHNE `FavoritesService.remove()` zu durchlaufen — dessen Datei-Aufräumung griff dort also nicht. Der Plan hatte dies als Restrisiko T-LRR-07 mit Disposition „accept“ eingetragen (Dateien bleiben liegen, sind ohne Zeile nie abrufbar, höchstens 512 KB je Favorit).
- **Fix:** `favorite-icon-files.ts` bekam eine zusätzliche, nie werfende Funktion `removeFavoriteIconFileBestEffort(userId, id, mime)`. `DashboardService.removeWidget`/`deleteDashboard` lesen VOR der Löschung die betroffenen Favoriten mit gesetztem `uploadedIconMime` (ein reiner Lesezugriff, außerhalb der Löschtransaktion) und entfernen NACH erfolgreicher Löschung deren Dateien best effort — ein Dateifehler wird protokolliert und geschluckt, er kann das Löschen des Widgets/Reiters nie verhindern oder zurücknehmen (Muster T-HK4-04).
- **Dateien geändert:** `apps/api/src/favorites/favorite-icon-files.ts`, `apps/api/src/dashboard/dashboard.service.ts`, `apps/api/src/dashboard/dashboard.service.spec.ts`
- **Tests:** 5 neue Tests in `dashboard.service.spec.ts` (Datei wird entfernt, fehlende Datei wird geschluckt, Favoriten ohne Symbol lösen keinen Dateizugriff aus — für beide Methoden je Fall bzw. anteilig)
- **Verifikation:** `pnpm --filter @tessera/api exec vitest run src/dashboard src/favorites` grün (218 Tests)
- **Commit:** `7704372` (Teil des Aufgabe-1-Commits, da API-seitig und eng an `favorite-icon-files.ts` gekoppelt)
**Restrisiko nach diesem Fix:** Das Löschen eines EINZELNEN Favoriten über `DELETE /favorites/:id` sowie die beiden neuen Wege raumen die Datei immer auf. Ein denkbarer Rest bleibt nur, wenn ein Dateisystemfehler ausgerechnet beim best-effort-Entfernen auftritt (protokolliert, nie blockierend) — dasselbe Restrisiko, das die Bilderrahmen-Funktion (`dashboard-images.service.ts`, T-HK4-04) für ihre Bilder ebenfalls bewusst trägt.
---
**Gesamt:** 1 Zusatzanforderung umgesetzt (kein Regel-1/2/3-Fund im eigentlichen Sinn, da vom Orchestrator vorgegeben statt während der Ausführung entdeckt — inhaltlich entspricht die Umsetzung Regel 2, fehlende sicherheits-/korrektheitsrelevante Funktionalität).
**Auswirkung auf den Plan:** Kein Scope Creep über die Orchestrator-Vorgabe hinaus; alle übrigen Aufgaben wurden wie im Plan spezifiziert umgesetzt.
## Verifikation (durchgeführt)
```
pnpm --filter @tessera/api exec prisma generate # OK
pnpm --filter @tessera/api exec vitest run src/favorites src/dashboard # 218/218 grün
pnpm --filter @tessera/api exec tsc --noEmit # sauber
pnpm exec biome lint apps/api/src/favorites apps/api/src/dashboard # keine Befunde
pnpm --filter @tessera/web exec vitest run src/components/dashboard \
src/lib/favorites-api.test.ts src/messages # 276/276 grün
pnpm --filter @tessera/web exec tsc --noEmit # sauber
pnpm exec biome lint apps/web/src/lib/favorites-api.ts \
apps/web/src/lib/favorites-api.test.ts \
apps/web/src/components/dashboard/widgets/favorites-widget.test.tsx # keine Befunde
biome lint favorites-widget.tsx # 3 Befunde (alle vorbestehend, wie vom Plan erlaubt)
grep -rc "as unknown as" apps/api/src # ≤ 29 (Plan-Obergrenze eingehalten)
```
Migration `20260923160000_favorite_icon_upload` wurde lokal über die Container-IP der `db` mit `prisma migrate deploy` angewendet (Vorgabe des Orchestrators: `tessera:tessera_dev`, kein Host-Port).
## Bekannte Stubs
Keine — jede neu geschriebene Funktion ist mit echten Daten verdrahtet, keine Platzhalter.
## Aufgabe 3 — Browser-Nachweis (noch offen, Orchestrator)
Aufgabe 3 des Plans (`checkpoint:human-verify`, `gate="blocking"`) ist ein separater Verifikationsschritt am lokalen Docker-Stack (Symbol hochladen/entfernen, Cache-Bust im Browser, Abrufprobe, Dateigrößen-/Typgrenzen, Dateiaufräumung nach Löschen) und wird laut Auftrag NICHT von diesem Ausführungslauf durchgeführt — das übernimmt der Orchestrator im Anschluss per Playwright MCP. Diese SUMMARY dokumentiert ausschließlich die abgeschlossenen Aufgaben 1 und 2.
## Nächste Schritte
- Orchestrator: Aufgabe 3 (Browser-Nachweis) durchführen, siehe `260923-lrr-PLAN.md`.
- Kein Blocker für Aufgabe 3 aus Sicht der API/Web-Implementierung — alle automatisierten Prüfungen sind grün, die Migration ist lokal bereits angewendet.
---
*Quick-Aufgabe: 260923-lrr*
*Abgeschlossen (Aufgaben 1–2): 2026-09-23*
## Self-Check: PASSED
Alle in dieser Summary genannten neuen Dateien sowie beide Commits (`7704372`, `61f95c8`) wurden gegen das Repository geprüft und gefunden.