Files
tessera-ctl/.planning/quick/260923-lrr-favoriten-eigenes-symbol-hochladen-und-s/260923-lrr-SUMMARY.md
T
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

14 KiB
Raw Blame History

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, requirements-completed, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions requirements-completed duration completed status
quick-260923-lrr 01 api+web (favorites)
nestjs
prisma
nextjs
upload
cache-busting
favorites
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)
dashboard
favorites
tokens tasks commits
31900 2 2
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
created modified
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
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
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
QUICK-260923-lrr
45min 2026-09-23 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.