Files
tessera-ctl/.planning/quick/260922-hk4-bilderrahmen-bilder-auf-die-festplatte/260922-hk4-SUMMARY.md
T
schalli ee2b0256b5
Tessera CI/CD / Lint & Type Check (push) Successful in 50s
Tessera CI/CD / Tests (push) Successful in 1m16s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 18s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m4s
docs(quick-260922-hk4): Akte - Bilder im Dateibereich, Rundgang mit Selbstheilungs-Befund
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 15:36:04 +02:00

12 KiB

phase, plan, subsystem, tags, status, requires, provides, affects, tech-stack, key-files, decisions, metrics, actuals, plan_head_before
phase plan subsystem tags status requires provides affects tech-stack key-files decisions metrics actuals plan_head_before
quick-260922-hk4 01 apps/api/src/dashboard
bilderrahmen
dashboard
user-files
prisma-migration
rls
tdd
complete
quick-260921-pi9
DashboardImage.storagePath — Bilder im Dateibereich statt als bytea
DashboardImagesService.onApplicationBootstrap() — automatischer Umzug beim Start
Migration 20260922120000_dashboard_image_to_disk (Stufe 1 von 2)
apps/api/src/dashboard/dashboard-images.service.ts
apps/api/prisma/schema.prisma
apps/api/src/prisma/rls-access-inventory.spec.ts
docs/mandantentrennung-zugriffsklassifikation.md
docs/anleitung-betrieb.md
added patterns
Servergenerierter Dateiname (UUID + Endung aus dem erkannten Mime-Typ), Muster dkv-export.service.ts (T-07-09)
Relativer Pfad in der Zeile, Muster User.avatarPath (user.controller.ts)
Einmal systemgebunden lesen, je Zeile mandantengebunden schreiben (Muster DkvService.loadActiveConfigsForScheduler)
Zweistufige Spaltenablösung: ADD + NULLbar jetzt, DROP nach nachgewiesenem Lauf
Dateitests gegen ein echtes Temp-Verzeichnis statt fs-Mock (Muster desktop.service.spec.ts)
created modified
apps/api/prisma/migrations/20260922120000_dashboard_image_to_disk/migration.sql
.planning/todos/pending/2026-09-22-dashboard-image-data-spalte-entfernen.md
apps/api/prisma/schema.prisma
apps/api/src/dashboard/dashboard-images.service.ts
apps/api/src/dashboard/dashboard-images.service.spec.ts
apps/api/src/prisma/rls-access-inventory.spec.ts
docs/mandantentrennung-zugriffsklassifikation.md
docs/anleitung-betrieb.md
CHANGELOG.md
data Bytes? bleibt im Prisma-Modell (optional) statt $queryRaw — der Bootstrap-Umzug bleibt dadurch typisiert und ohne rohes SQL; Spalte und Feld fallen gemeinsam in Stufe 2
Systemkontext (forSystem) nur im Startpfad; die vier Anfragewege bleiben ausnahmslos mandantengebunden, auch das Schreiben des Umzugs
Neue Regel system_read_policy auf DashboardImage, damit der Umzug nach dem Scharfschalten der Datenbankrolle nicht stumm nichts findet
Testschalter DASHBOARD_IMAGES_DIR (Muster DESKTOP_DIST_DIR) statt fs-Mock — die Tests schreiben und lesen wirklich
duration completed
~35 min 2026-09-22
tokens tasks commits
21000 2 2
441854a

Quick-Aufgabe 260922-hk4: Bilderrahmen-Bilder auf die Festplatte — Summary

Die Bilder des Bilderrahmen-Widgets liegen jetzt unter user-files/dashboard-images/<userId>/<id>.<ext>; die Datenbankzeile hält nur noch den relativen Pfad, und vorhandene Bilder ziehen beim ersten Start automatisch um — nachgewiesen gegen die lokale Datenbank.

Was gebaut wurde

Aufgabe 1 — Schema, Migration, Dienst, Bootstrap-Umzug, Tests (9039cea)

  • schema.prisma: data Bytes → data Bytes?, neu storagePath String?.
  • Migration 20260922120000_dashboard_image_to_disk: ADD COLUMN "storagePath", ALTER COLUMN "data" DROP NOT NULL, dazu system_read_policy … FOR SELECT auf "DashboardImage". Kein DROP — die Begründung steht im Migrationskopf (Stufe 1 von 2, T-HK4-03).
  • DashboardImagesService:
    • upload legt die Zeile an (erst danach steht die UUID fest), schreibt die Datei, trägt storagePath nach; scheitert das Schreiben, wird die Zeile zurückgenommen und 500 geworfen.
    • getBytes liest die Datei; fehlender Pfad oder fehlende Datei → 404.
    • remove löscht Zeile und Datei (Dateifehler wird protokolliert, nicht geworfen).
    • onApplicationBootstrap() zieht Altbestand um: einmal systemgebunden lesen (forSystem, Zeilen ohne storagePath über alle Mandanten), je Zeile mandantengebunden schreiben (forTenant(prisma, row.tenantId, row.userId)), Log „N Bilderrahmen-Bilder auf die Festplatte umgezogen", still bei 0, wiederholbar.
    • Dateiname IMMER servergeneriert; absoluteImagePath() weist jeden Pfad zurück, der nicht im Bilderverzeichnis liegt (T-HK4-01).
  • Tests: 23 Fälle (11 neu), echtes Temp-Verzeichnis statt fs-Mock.
  • RLS-Wächter und Klassifikationsdokument nachgezogen (siehe Abweichungen).

Aufgabe 2 — Changelog, Betriebsanleitung, Todo (8cbfb8b)

  • CHANGELOG „Unveröffentlicht → Geändert" mit der Nutzerzeile.
  • docs/anleitung-betrieb.md Kap. 6: user-files nennt die Bilderrahmen-Bilder und hält fest, dass pg_dump sie nicht mehr enthält.
  • Todo .planning/todos/pending/2026-09-22-dashboard-image-data-spalte-entfernen.md mit Vorbedingung (storagePath IS NULL = 0 auf alpha UND live), Migrationsname 20260922120100_dashboard_image_drop_data und allen Nacharbeiten an Spec und Klassifikation.

TDD-Nachweis (RED → GREEN)

  • RED (vor der Umsetzung, vitest run src/dashboard/dashboard-images.service.spec.ts): Tests 12 failed | 11 passed (23), u. a. TypeError: makeService(...).onApplicationBootstrap is not a function und Erwartungen an storagePath, die noch niemand setzte. Die 11 grünen Fälle sind die unveränderten Besitz-/Magic-Byte-Prüfungen aus pi9.
  • GREEN nach dem Dienst: Tests 23 passed (23).
  • Ein RED war ein Testfehler, kein Dienstfehler: Test 17 („Datei fehlt") nutzte die Kennung img-1, für die Test 8/10 im geteilten Temp-Verzeichnis schon eine Datei angelegt hatten — Kennung auf datei-fehlt geändert.

Nachweis am laufenden System (lokal, kein Testserver)

  • prisma migrate deploy gegen die lokale Container-Datenbank: Migration 20260922120000_dashboard_image_to_disk angewendet, danach prisma generate.
  • \d "DashboardImage": data ist jetzt NULLbar, storagePath text, Policies tenant_isolation_policy + system_read_policy (FOR SELECT).
  • Bootstrap-Umzug gegen die echte Datenbank ausgeführt (Wegwerf-Spec, danach gelöscht):
    • vorher: 1 Zeile, storagePath = null, 502 Byte in data
    • Log: 1 Bilderrahmen-Bilder auf die Festplatte umgezogen
    • nachher: storagePath = user-files/dashboard-images/1166431d-…/f43be914-….png
    • Datei auf der Platte: 502 Byte, PNG image data, 320 x 200 (file)
    • zweiter Lauf: keine Zeile mehr offen, Datei unverändert (wiederholbar)

Tore

Tor Ergebnis
vitest run src/dashboard src/prisma 147 Tests, alle grün
pnpm type-check (4 Pakete) grün
pnpm lint (5 Pakete) grün (74 API-/53 Web-Warnungen, alle vorbestehend, keine in den geänderten Dateien)
pnpm --filter @tessera/api test 75 Dateien, 1186 Tests grün
pnpm --filter @tessera/web test 81 Dateien, 640 Tests grün
as unknown as in apps/api 27 (unverändert)
neue any / ! / biome-ignore keine

Abweichungen vom Plan

[Regel 3 — blockierend] Das Klassifikationsdokument brauchte doch eine Änderung

Der Plan sagte, das Dokument brauche keine neue Zeile. Richtig — eine neue ZEILE nicht, aber der forSystem()-Aufruf im Startpfad ändert den gemessenen Stand des Paars dashboard-images.service.ts/dashboardImage von gebunden auf system-gebunden, und rls-access-inventory.spec.ts prüft genau diesen Wert. Zwei Tests waren rot, bis nachgezogen war:

  • FORSYSTEM_ALLOWED_CALL_SITES (die Liste ist ein „genau", kein „mindestens") um apps/api/src/dashboard/dashboard-images.service.ts = 1 erweitert, mit Begründung im Kopfkommentar: Startpfad, kein Anfrageweg; geschrieben wird auch dort mandantengebunden. Präzedenz: ldap-config.service.ts, dessen Nachverschlüsselung in onApplicationBootstrap() genauso gebaut ist.
  • Klassifikationsdokument: Stand system-gebunden mit Begründung, Zahlen der Bereichszeile dashboard mit derselben Gate-Schleife nachgemessen (1/18/0 → 1/21/1; +3 gebunden = Nachtragen von storagePath, Rücknahme bei Schreibfehler, Nachtragen im Umzug), Summe 187/5 → 190/6.

Beides ist im Todo für Stufe 2 als Rückbau vermerkt.

[Regel 2 — fehlende kritische Funktionalität] ALTER COLUMN "data" DROP NOT NULL

Der Plan nannte nur ADD COLUMN "storagePath". Ohne das Lockern der NOT-NULL-Bedingung wäre jeder neue Upload an der Datenbank gescheitert, weil er keine Bytes mehr in die Zeile schreibt.

[Regel 2 — fehlende kritische Funktionalität] system_read_policy auf "DashboardImage"

Nicht im Plan. Ohne diese Regel sähe der systemgebundene Umzug nach dem Scharfschalten der Datenbankrolle NULL Zeilen und stellte die Arbeit stumm ein — genau die Falle, die Migration 20260914120000 für die fünf Hintergrunddienst-Tabellen geschlossen hat. Permissiv, nur FOR SELECT; Schreiben bleibt allein der Mandantenregel unterstellt.

[Entscheidung] Testschalter DASHBOARD_IMAGES_DIR

Der Plan ließ die Wahl zwischen memfs und einem Temp-Verzeichnis. Gewählt: Temp-Verzeichnis (keine neue Abhängigkeit), erreichbar über die Umgebungsvariable DASHBOARD_IMAGES_DIR — dasselbe Muster, das desktop.service.ts mit DESKTOP_DIST_DIR schon nutzt. Im Betrieb nie gesetzt; ohne sie gilt der Pfad unter der Monorepo-Wurzel.

Known Stubs

Keine.

Threat Flags

Keine neue Angriffsfläche über den <threat_model> des Plans hinaus. Der einzige neue Dateipfad-Umgang ist vollständig servergeneriert und zusätzlich containment-geprüft (absoluteImagePath).

Von Hand zu prüfen (nach dem nächsten --build-Deploy)

  1. Bild im Bilderrahmen-Widget hochladen → erscheint in der Kachel und in der Verwaltung unter Einstellungen → Dashboard.
  2. Auf dem Server nachsehen: docker compose exec api ls -R /app/user-files/dashboard-images — je Benutzer ein Ordner, Dateiname eine UUID mit .png/.jpg/.gif/.webp, nie der Originalname.
  3. Bild löschen → verschwindet aus der Kachel UND die Datei ist weg (ls wie oben).
  4. Nach dem ersten Start mit dieser Version: docker compose logs api | grep umgezogen — die Zeile „N Bilderrahmen-Bilder auf die Festplatte umgezogen" steht genau einmal; ein zweiter Neustart schweigt.
  5. docker compose exec db psql -U tessera -d tessera -c 'SELECT count(*) FROM "DashboardImage" WHERE "storagePath" IS NULL;' → muss 0 sein (Vorbedingung für Stufe 2, siehe Todo).

Self-Check: PASSED

  • apps/api/prisma/migrations/20260922120000_dashboard_image_to_disk/migration.sql — vorhanden
  • apps/api/src/dashboard/dashboard-images.service.ts — vorhanden
  • .planning/todos/pending/2026-09-22-dashboard-image-data-spalte-entfernen.md — vorhanden
  • Commit 9039cea — vorhanden
  • Commit 8cbfb8b — vorhanden

Rundgang durch den Orchestrator (22.09.2026, lokaler Stack, Abbilder aus dem Commit danach)

Bestanden, und dabei EIN Befund gefunden und behoben (eigener Commit):

  • Hochladen ueber die Oberflaeche legt die Datei unter user-files/dashboard-images/<userId>/<uuid>.png im Container-Volume an; die Liste zeigt sie, die Kachel rendert sie.
  • Loeschen entfernt Zeile UND Datei (3 Dateien/3 Zeilen → 2/2, gemessen im Container und in der Datenbank).
  • Befund: eine Zeile zeigte auf eine Datei, die es im Container nicht gibt — der Bootstrap-Umzug war beim Bauen auf dem HOST gelaufen (Repo-Verzeichnis), der Container hat aber das Volume user-files. Lokal ein Artefakt, im Betrieb aber real: wer einen pg_dump von VOR dem Umzug zurueckspielt, waehrend das getrennt gesicherte Volume leer ist, haette Zeilen ohne Datei, obwohl die Bytes im Abzug noch stecken.
  • Behoben: getBytes schreibt die Datei in diesem Fall aus der noch vorhandenen Spalte data neu und liefert sie aus (Protokoll „… aus der Datenbank wiederhergestellt"); fehlt beides, bleibt es bei 404. Nachgewiesen: Abruf lieferte 200/image/png/502 Byte, danach lag die Datei im Container. Zwei Tests (10b, 10c), api 1186 → 1188.