docs(quick-260922-hk4): Akte - Bilder im Dateibereich, Rundgang mit Selbstheilungs-Befund
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
---
|
||||
phase: quick-260922-hk4
|
||||
plan: 01
|
||||
type: tdd
|
||||
autonomous: true
|
||||
subsystem: apps/api/src/dashboard
|
||||
requirements: []
|
||||
---
|
||||
|
||||
# Quick-Aufgabe 260922-hk4: Bilderrahmen-Bilder auf die Festplatte statt in die Datenbank
|
||||
|
||||
## Warum (Entscheidung des Nutzers, 22.09.2026)
|
||||
|
||||
Der Bilderrahmen legte die Bilddaten als `bytea` in der Datenbank ab
|
||||
(quick-260921-pi9). Der Nutzer hat nach der Freigabe 1.3.0 gefragt, ob das auf
|
||||
Dauer sinnvoll ist. Befund und Entscheidung:
|
||||
|
||||
- **Geschwindigkeit ist NICHT das Argument.** Ein Bild wird je Browser einmal
|
||||
taeglich geladen (`Cache-Control: private, max-age=86400`); ein paar hundert
|
||||
Kilobyte aus Postgres kosten nichts gegen die uebrige Last.
|
||||
- **Die Sicherung ist das Argument.** Gesichert wird von Hand per `pg_dump`
|
||||
(docs/anleitung-betrieb.md Kap. 6). Jedes Bild waechst in diesen Abzug hinein:
|
||||
30 Bilder à 5 MiB je Benutzer sind im Extremfall 150 MB **pro Benutzer**. Die
|
||||
alpha-Datenbank ist heute 18 MB gross (gemessen 22.09.), da faellt das sofort auf.
|
||||
- **Einheitlichkeit.** Tessera speichert Dateien laengst im Volume `user-files`:
|
||||
Profilbilder unter `user-files/avatars/<userId>.<ext>` mit `User.avatarPath` in
|
||||
der Datenbank (`user.controller.ts`), DKV-Exporte daneben mit ausschliesslich
|
||||
servergenerierten Dateinamen (`dkv-export.service.ts`). Der Bilderrahmen war der
|
||||
Ausreisser.
|
||||
- **Ein eigener Ordner je Benutzer ist KEIN Schutz.** Wer welches Bild sehen darf,
|
||||
entscheidet weiterhin der Server (Besitzpruefung + RLS-Regel). Getrennte Ordner
|
||||
bringen zusaetzlich die Gefahr von Dateinamen, die aus dem Ordner herausfuehren —
|
||||
dagegen hilft nur, was DKV schon macht: der Server vergibt den Dateinamen, nie
|
||||
der Client.
|
||||
|
||||
Bestand: alpha 3 Bilder / 1,8 MB, Live 0 (noch nicht gezogen), lokal 1–2. Der
|
||||
Umzug ist jetzt praktisch kostenlos.
|
||||
|
||||
## Gebundene Entscheidungen (Orchestrator)
|
||||
|
||||
1. **Ablage:** `user-files/dashboard-images/<userId>/<imageId>.<ext>` — ein Ordner
|
||||
je Benutzer, Dateiname ist die UUID der Datenbankzeile plus Endung aus dem
|
||||
ERKANNTEN Mime-Typ (`png|jpg|gif|webp`). Kein Byte aus der Anfrage geht in den
|
||||
Pfad. Verzeichnis-Aufloesung nach dem Muster `resolveAvatarsDir()`
|
||||
(`path.resolve(__dirname, '..', '..', '..', '..', 'user-files', ...)`), als
|
||||
eigene Funktion `resolveDashboardImagesDir()` im Dienst.
|
||||
2. **Datenbank:** Spalte `data Bytes` entfaellt, neu `storagePath String` (relativ
|
||||
zur Monorepo-Wurzel, wie `User.avatarPath`: `user-files/dashboard-images/...`).
|
||||
Rest der Zeile unveraendert (id, userId, tenantId, originalName, mimeType, size,
|
||||
createdAt), RLS-Regel und Indizes bleiben.
|
||||
3. **Migration `20260922120000_dashboard_image_to_disk`** in zwei Schritten, weil
|
||||
die vorhandenen Bytes nicht verloren gehen duerfen:
|
||||
- SQL-Migration: `ALTER TABLE "DashboardImage" ADD COLUMN "storagePath" TEXT;`
|
||||
(erst NULLbar), **nicht** sofort `DROP COLUMN "data"`.
|
||||
- Einmal-Skript `apps/api/scripts/migrate-dashboard-images-to-disk.ts`
|
||||
(ausfuehrbar per `pnpm --filter @tessera/api exec tsx scripts/...`, tsx ist
|
||||
vorhanden — sonst `ts-node`/kompiliertes JS; pruefen): liest alle Zeilen mit
|
||||
`data IS NOT NULL`, schreibt die Datei, setzt `storagePath`, laesst `data`
|
||||
stehen. Idempotent (vorhandene Datei + gesetzter `storagePath` = ueberspringen).
|
||||
- Zweite SQL-Migration `20260922120100_dashboard_image_drop_data`:
|
||||
`ALTER TABLE "DashboardImage" ALTER COLUMN "storagePath" SET NOT NULL;` und
|
||||
`ALTER TABLE "DashboardImage" DROP COLUMN "data";`.
|
||||
**Reihenfolge fuer den Betrieb dokumentieren:** beide Migrationen laufen beim
|
||||
Start automatisch (`migrate deploy`), das Umzugs-Skript liegt DAZWISCHEN. Damit
|
||||
das ohne Handarbeit klappt, macht der Dienst den Umzug selbst: siehe Punkt 4.
|
||||
4. **Automatischer Umzug beim Start statt Handarbeit** (der Nutzer soll nichts
|
||||
ausfuehren muessen): `DashboardImagesService` bekommt `onApplicationBootstrap()`,
|
||||
das alle Zeilen ohne `storagePath` einsammelt, die Bytes per rohem SQL liest
|
||||
(`$queryRaw` auf `data`, weil die Spalte dann nicht mehr im Prisma-Modell steht —
|
||||
deshalb liegt der DROP in einer SPAETEREN Migration, die erst in der naechsten
|
||||
Freigabe scharf geschaltet wird), die Datei schreibt und `storagePath` setzt.
|
||||
**Konsequenz fuer diese Aufgabe: die DROP-Migration wird NICHT mitgeliefert.**
|
||||
Sie bekommt einen Platzhalter-Eintrag in `.planning/todos/pending/` und kommt,
|
||||
wenn alle Server einmal mit dieser Version gelaufen sind. Begruendung im
|
||||
Migrations-Kommentar festhalten (Muster: zweistufige Umstellung).
|
||||
Der Bootstrap laeuft ueber den Systemkontext (`forSystem()`, Muster
|
||||
`dkv`-Scheduler), nicht ueber einen Mandantenklienten, und protokolliert
|
||||
„N Bilder auf die Festplatte umgezogen" bzw. schweigt bei 0.
|
||||
5. **Dienst:** `upload` schreibt die Datei (`fs.promises.mkdir(..., {recursive:true})`
|
||||
+ `writeFile`) NACH dem erfolgreichen `create` (Reihenfolge: Zeile zuerst, damit
|
||||
die UUID feststeht; schlaegt das Schreiben fehl, Zeile wieder loeschen und
|
||||
`InternalServerErrorException`). `getBytes` liest die Datei und liefert
|
||||
`{ mimeType, data }` wie bisher; fehlt die Datei, `NotFoundException` (Kachel
|
||||
zeigt dann „Bild nicht verfügbar", schon gebaut). `remove` loescht Zeile und
|
||||
Datei (Datei-Fehler werden geschluckt und protokolliert — eine Dateileiche ist
|
||||
harmloser als eine haengende Loeschung). `list` unveraendert.
|
||||
Der Controller bleibt unveraendert (gleiche Routen, gleiche fuenf Header).
|
||||
6. **Betriebsanleitung:** in Kapitel 6 den Satz zu `user-files` um die
|
||||
Bilderrahmen-Bilder ergaenzen (dort steht schon, wie das Volume gesichert wird);
|
||||
im Anwenderhandbuch nichts aendern (fuer Anwender aendert sich nichts).
|
||||
CHANGELOG unter „Unveröffentlicht → Geändert": „Bilderrahmen: hochgeladene
|
||||
Bilder liegen jetzt im Dateibereich des Servers statt in der Datenbank — die
|
||||
Datenbanksicherung bleibt dadurch klein; vorhandene Bilder ziehen beim ersten
|
||||
Start automatisch um" (kein Fliesstext).
|
||||
7. **Tests:** Dienst-Tests mit `memfs` ODER einem temporaeren Verzeichnis
|
||||
(`fs.mkdtempSync(os.tmpdir())`) — pruefen, was im Repo schon genutzt wird
|
||||
(`user.controller.spec.ts` fuer Avatare ansehen und demselben Muster folgen).
|
||||
Mindestens: Upload legt Datei unter `<dir>/<userId>/<id>.png` an und speichert
|
||||
`storagePath`; Upload mit fehlschlagendem Schreiben loescht die Zeile wieder;
|
||||
`getBytes` liefert den Dateiinhalt; fehlende Datei → 404; fremder Benutzer → 404
|
||||
(unveraendert); `remove` loescht Zeile und Datei; Dateiname enthaelt NIE
|
||||
`originalName`; Bootstrap-Umzug schreibt Datei und setzt `storagePath`,
|
||||
ueberspringt bereits umgezogene Zeilen.
|
||||
|
||||
## Aufgaben
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer" tdd="true">
|
||||
<name>Aufgabe 1: Schema, Migration, Dienst auf Dateiablage umstellen, Bootstrap-Umzug, Tests</name>
|
||||
<files>apps/api/prisma/schema.prisma, apps/api/prisma/migrations/20260922120000_dashboard_image_to_disk/migration.sql, apps/api/src/dashboard/dashboard-images.service.ts, apps/api/src/dashboard/dashboard-images.service.spec.ts, apps/api/src/dashboard/dashboard-images.controller.spec.ts, docs/mandantentrennung-zugriffsklassifikation.md</files>
|
||||
<action>
|
||||
Entscheidungen 1–5 umsetzen. Reihenfolge: Schema + Migration, `prisma migrate deploy` + `generate` gegen die lokale Container-DB ([BLOCKING], Befehle in den Executor-Hinweisen), dann Tests rot, dann Dienst.
|
||||
Das Klassifikationsdokument braucht keine neue Zeile (Modell unveraendert gebunden), aber die Begruendungsspalte erwaehnt jetzt, dass die Bytes auf der Platte liegen und die Zeile den Pfad haelt — Zahlen nachmessen wie dort beschrieben.
|
||||
Commit: `refactor(quick-260922-hk4): Bilderrahmen-Bilder in user-files statt in der Datenbank`
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api exec vitest run src/dashboard src/prisma && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/api lint</automated>
|
||||
</verify>
|
||||
<done>Migration angewendet, `storagePath` gefuellt fuer die vorhandenen lokalen Zeilen (Bootstrap nachgewiesen), Dateien liegen unter `user-files/dashboard-images/<userId>/`. Spalte `data` bleibt vorerst bestehen (zweistufig, siehe Plan). API-Tests ≥ 8 neue Faelle, RLS-Waechter unveraendert gruen. Keine `any`, Zaehler unveraendert.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Aufgabe 2: Changelog, Betriebsanleitung, Todo fuer die DROP-Migration, Voll-Tore</name>
|
||||
<files>CHANGELOG.md, docs/anleitung-betrieb.md, .planning/todos/pending/2026-09-22-dashboard-image-data-spalte-entfernen.md</files>
|
||||
<action>
|
||||
Entscheidung 6 umsetzen. Das Todo nennt: DROP der Spalte `data` erst, wenn alpha UND live einmal mit einer Version ≥ dieser gelaufen sind (Bootstrap-Umzug erledigt), Migrationsname `20260922120100_dashboard_image_drop_data`, plus `ALTER COLUMN "storagePath" SET NOT NULL`.
|
||||
Volle Tore: `pnpm type-check`, `pnpm lint`, `pnpm --filter @tessera/api test`, `pnpm --filter @tessera/web test`.
|
||||
Commit: `docs(quick-260922-hk4): Changelog, Betriebsanleitung und Todo zur data-Spalte`
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && grep -q 'Dateibereich' CHANGELOG.md && pnpm type-check && pnpm lint && pnpm --filter @tessera/api test</automated>
|
||||
</verify>
|
||||
<done>Changelog-Zeile steht unter „Unveröffentlicht → Geändert"; Betriebsanleitung Kap. 6 nennt die Bilderrahmen-Bilder beim `user-files`-Volume; Todo angelegt; alle Tore gruen; genau zwei Commits mit Scope `quick-260922-hk4`.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
## Hinweise fuer den Executor
|
||||
|
||||
- Lokale Migration: `IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tessera-ctl-db-1)`, dann
|
||||
`DATABASE_URL="postgresql://tessera:tessera_dev@$IP:5432/tessera" pnpm --filter @tessera/api exec prisma migrate deploy` und `prisma generate`.
|
||||
- Testserver NICHT anfassen.
|
||||
- Commits: Conventional Commits, Scope `quick-260922-hk4`, deutscher Betreff im Stil von `git log --oneline -15`, jede Commit-Nachricht endet mit
|
||||
`Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>`
|
||||
- `.planning/**` NICHT committen ausser der Todo-Datei in Aufgabe 2.
|
||||
- Qualitaetsregeln wie bisher: keine neue `any`, `as unknown as` api bleibt 27, keine `!`, kein `biome-ignore`.
|
||||
|
||||
<threat_model>
|
||||
ASVS 1, block on high.
|
||||
|
||||
| ID | Bedrohung | Schwere | Disposition |
|
||||
|---|---|---|---|
|
||||
| T-HK4-01 | Pfad-Ausbruch ueber `originalName` oder Kennung aus der Anfrage | high | Dateiname = UUID der Zeile + Endung aus dem ERKANNTEN Mime-Typ; `originalName` geht nie in den Pfad (Muster DKV T-07-09). Mitigiert. |
|
||||
| T-HK4-02 | Fremdzugriff auf Bilder ueber geratene Pfade | high | Die Datei wird nie direkt ausgeliefert; nur ueber `GET /dashboard/images/:id` mit Besitzpruefung (Mandant + Benutzer) und 404 fuer Fremde. Das Volume ist nicht im Webserver eingehaengt. Mitigiert. |
|
||||
| T-HK4-03 | Datenverlust beim Umzug | high | Zweistufig: `data` bleibt vorerst stehen, Umzug ist idempotent, DROP erst nach nachgewiesenem Lauf auf beiden Servern (Todo). Mitigiert. |
|
||||
| T-HK4-04 | Halbe Zustaende (Zeile ohne Datei / Datei ohne Zeile) | medium | Upload: Zeile zuerst, bei Schreibfehler Zeile loeschen; Loeschen: Zeile zuerst, Dateifehler wird protokolliert (Dateileiche statt haengender Loeschung); fehlende Datei = 404, die Kachel zeigt „Bild nicht verfügbar". Akzeptiert und benannt. |
|
||||
| T-HK4-05 | Volume geht verloren, Datenbank ueberlebt | low | Bewusst akzeptiert (Entscheidung des Nutzers); Betriebsanleitung nennt die Sicherung des Volumes. |
|
||||
</threat_model>
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
---
|
||||
phase: quick-260922-hk4
|
||||
plan: 01
|
||||
subsystem: apps/api/src/dashboard
|
||||
tags: [bilderrahmen, dashboard, user-files, prisma-migration, rls, tdd]
|
||||
status: complete
|
||||
requires: [quick-260921-pi9]
|
||||
provides:
|
||||
- "DashboardImage.storagePath — Bilder im Dateibereich statt als bytea"
|
||||
- "DashboardImagesService.onApplicationBootstrap() — automatischer Umzug beim Start"
|
||||
- "Migration 20260922120000_dashboard_image_to_disk (Stufe 1 von 2)"
|
||||
affects:
|
||||
- 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
|
||||
tech-stack:
|
||||
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)"
|
||||
key-files:
|
||||
created:
|
||||
- apps/api/prisma/migrations/20260922120000_dashboard_image_to_disk/migration.sql
|
||||
- .planning/todos/pending/2026-09-22-dashboard-image-data-spalte-entfernen.md
|
||||
modified:
|
||||
- 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
|
||||
decisions:
|
||||
- "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"
|
||||
metrics:
|
||||
duration: "~35 min"
|
||||
completed: 2026-09-22
|
||||
actuals:
|
||||
tokens: 21000
|
||||
tasks: 2
|
||||
commits: 2
|
||||
plan_head_before: 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.
|
||||
Reference in New Issue
Block a user