175 lines
14 KiB
Markdown
175 lines
14 KiB
Markdown
---
|
||
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.
|