--- created: 2026-09-22 title: DashboardImage — Spalte "data" entfernen und "storagePath" auf NOT NULL setzen (Stufe 2 der Umstellung aus quick-260922-hk4) area: apps/api/prisma severity: cleanup trigger: erst wenn alpha UND live je einmal mit einer Version >= der Freigabe nach 1.3.0 gelaufen sind — dann hat der Bootstrap-Umzug auf beiden Servern gearbeitet und die Bytes liegen im Dateibereich. relates_to: quick-260922-hk4 (.planning/quick/260922-hk4-bilderrahmen-bilder-auf-die-festplatte/) --- ## Worum es geht Die Bilder des Bilderrahmen-Widgets sind mit `quick-260922-hk4` aus der Datenbank in den Dateibereich gezogen (`user-files/dashboard-images/ /.`, Pfad in der Spalte `storagePath`). Die Umstellung ist BEWUSST ZWEISTUFIG: - **Stufe 1, ausgeliefert** (Migration `20260922120000_dashboard_image_to_disk`): `storagePath` dazu (NULLbar), `data` wird NULLbar — aber NICHT gelöscht. Der Umzug der vorhandenen Zeilen passiert beim ersten Start automatisch (`DashboardImagesService.onApplicationBootstrap()`). - **Stufe 2, dieser Zettel:** `data` löschen, `storagePath` auf NOT NULL. Warum nicht sofort: `prisma migrate deploy` läuft VOR dem Anwendungsstart. Ein sofortiges `DROP COLUMN "data"` hätte die Bytes vernichtet, bevor der Umzug beim Start sie lesen konnte (T-HK4-03). ## Was zu tun ist 1. **Vorbedingung prüfen** (auf BEIDEN Servern, alpha und live): ```sql SELECT count(*) FROM "DashboardImage" WHERE "storagePath" IS NULL; ``` Muss überall `0` sein. Ist sie es nicht, ist der Umzug dort noch nicht gelaufen (Server noch auf einer älteren Version) — dann NICHT ausliefern. 2. **Neue Migration** `20260922120100_dashboard_image_drop_data`: ```sql ALTER TABLE "DashboardImage" ALTER COLUMN "storagePath" SET NOT NULL; ALTER TABLE "DashboardImage" DROP COLUMN "data"; ``` 3. **Schema** `apps/api/prisma/schema.prisma`: Feld `data Bytes?` entfernen, `storagePath String?` → `storagePath String`. 4. **Dienst** `apps/api/src/dashboard/dashboard-images.service.ts`: `onApplicationBootstrap()` samt `forSystem()`-Aufruf entfernt sich damit — der Umzug hat seine Arbeit getan. Danach: - Eintrag `apps/api/src/dashboard/dashboard-images.service.ts` aus `FORSYSTEM_ALLOWED_CALL_SITES` in `apps/api/src/prisma/rls-access-inventory.spec.ts` wieder ENTFERNEN (die Liste ist ein „genau", ein veralteter Eintrag macht die Spec rot). - In `docs/mandantentrennung-zugriffsklassifikation.md` den Stand der Zeile `dashboard-images.service.ts`/`dashboardImage` von `system-gebunden` zurück auf `gebunden` setzen und die Zahlen der Bereichszeile `dashboard` sowie die Summe neu messen (Gate-Schleife, nicht abschreiben). - Die Tests 18, 21, 22 und 23 der Dienst-Spec (Zeile ohne `storagePath`, Bootstrap-Umzug) entfallen mit dem Umzug. - Die Regel `system_read_policy` auf `"DashboardImage"` (angelegt in 20260922120000) kann bleiben oder mit `DROP POLICY` fallen — bleibt sie, gehört sie in der Klassifikation erwähnt; fällt sie, ist die Aufzählung „fünf/sechs Tabellen" dort nachzuziehen. 5. **Prüfen**, dass die Datenbank kleiner wird: ```sql SELECT pg_size_pretty(pg_total_relation_size('"DashboardImage"')); ``` (Nach dem DROP zusätzlich `VACUUM FULL "DashboardImage";`, sonst gibt PostgreSQL den Platz nicht ans Dateisystem zurück.) ## Was passiert, wenn es liegen bleibt Nichts Schlimmes: die Spalte steht leer herum und kostet je neuer Zeile nichts. Der Gewinn der Umstellung (kleiner `pg_dump`) ist bereits da, weil neue Uploads keine Bytes mehr in die Zeile schreiben. Nur die Bytes der ALTEN Bilder bleiben bis dahin doppelt vorhanden — einmal in der Datei, einmal in der Spalte. ## Erledigt in quick-260924-m4n (24.09.2026) - Migration heißt `20260924120000_dashboard_image_drop_data` (nicht `20260922120100`: sie muss hinter allen vorhandenen Migrationen liegen). Sie prüft zuerst, dass keine Zeile ohne `storagePath` existiert, und bricht sonst mit Meldung ab, bevor sie etwas ändert; `row_security` ist für die Prüfung aus, damit ein Eigentümer ohne BYPASSRLS nicht still 0 Zeilen sieht (lokal nachgewiesen). Danach `storagePath` NOT NULL, `DROP COLUMN "data"`, `DROP POLICY IF EXISTS system_read_policy ON "DashboardImage"`. - Dienst: Bootstrap-Umzug, `forSystem()` und die Selbstheilung aus `data` entfernt; der Upload vergibt die UUID selbst und legt die Zeile gleich mit Pfad an. Erlaubnisliste, Tests (10b, 10c, 18, 21–23 entfallen) und Zugriffsklassifikation nachgezogen (Zahlen mit der Gate-Schleife gemessen). - Wiederherstellungsweg nach einem Abbruch (fehlgeschlagene Migration als zurückgenommen vermerken, 1.3.1 laufen lassen, erneut einspielen) in `docs/anleitung-betrieb.md` Kapitel 4, in einer Wegwerf-Datenbank durchgespielt.