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:
+8
-7
@@ -4,10 +4,10 @@ milestone: v1.3
|
||||
current_phase: 18
|
||||
current_phase_name: desktop-client-fertigstellen
|
||||
status: verified
|
||||
stopped_at: "22.09.2026: VERSION 1.3.0 FREIGEGEBEN (Tag v1.3.0, Commit d146234; Abbilder live + v1.3.0 fuer web und api, Gitea-Release Tessera 1.3.0 mit Tessera-Setup-1.3.0.exe und Tessera-1.3.0.AppImage; CI 408/409/410 alle gruen). Inhalt: Bilderrahmen- und XFrame-Widget (inkl. Ausschnitt/Zoom/Nur-anzeigen), Favoriten-Sortierung, Desktop-App mit Server-Anzeige/-Wechsel und Update in der App, Bildmarke in Akzentfarbe, Herkunft in Fehlermeldungen, drei Korrekturen vom 22.09. OFFEN BEIM NUTZER: auf dem Live-Server pullen (docker compose -f docker-compose.prod.yml pull && up -d --force-recreate api web). Der Basic-Auth vor alpha bleibt (Nutzerentscheidung, intern Ausnahme) — nicht ansprechen. Kein weiterer Auftrag benannt."
|
||||
last_updated: "2026-09-22T12:15:00.000Z"
|
||||
stopped_at: "22.09.2026: 1.3.0 freigegeben; danach quick-260922-hk4 — Bilderrahmen-Bilder liegen jetzt im Dateibereich (user-files) statt in der Datenbank, Umzug laeuft automatisch beim Start, Selbstheilung aus der alten data-Spalte eingebaut; im Browser nachgewiesen. NAECHSTER SCHRITT, vom Nutzer noch nicht bestaetigt: (1) einmaliges Aufraeumen, damit ein Modul seine Dashboard-Kachel selbst mitbringt (heute sieben Hartkodierungen je Kachel; Katalog zeigt auch Kacheln gesperrter Module; gesperrte Kachel bleibt leer statt zu erklaeren) — das Geruest WIDGET_MODULE_MAP existiert und ist leer; (2) danach das Proxmox-Modul (PVE/PBS/PMG) und seine Kachel. Offen beim Nutzer: Live-Server auf 1.3.0 ziehen, neuen Client per Browser installieren."
|
||||
last_updated: "2026-09-22T13:40:00.000Z"
|
||||
last_activity: 2026-09-21
|
||||
last_activity_desc: Freigabe 1.3.0 — CHANGELOG abgeschlossen, live auf main vorgezogen, Tag v1.3.0 gepusht; CI 408/409/410 gruen, Abbilder live/v1.3.0 und Gitea-Release mit beiden Desktop-Paketen
|
||||
last_activity_desc: Quick 260922-hk4 — Bilderrahmen-Bilder in den Dateibereich umgezogen (automatisch beim Start, Selbstheilung aus der alten Spalte), im Browser nachgewiesen; davor Freigabe 1.3.0
|
||||
state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2
|
||||
progress:
|
||||
total_phases: 18
|
||||
@@ -461,6 +461,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
|
||||
| 260922-frg | **Desktop-Client: Update-Eintrag im Tray nie mehr stumm ausgegraut.** Befund des Nutzers: „Update installieren“ bleibt grau, obwohl alpha `1.2.0-beta.gc001a08` anbietet und der Client auf `a6d1a64` steht — auch nach App-Neustart. Nachgemessen: Tessera-seitig antwortet `/desktop/update` auf dem alpha-Server selbst (am Proxy vorbei) mit 200 und gueltigem Manifest; DAVOR antwortet der Nginx Proxy Manager auf jede Anfrage an alpha mit `401 Basic` (vom Dev-Host und vom Testserver ueber 217.7.63.32 gemessen). Die Webansicht der App merkt sich das Proxy-Passwort, der Updater (`tauri-plugin-updater`, eigener reqwest) nicht. **Produktfehler:** das Plugin verschluckt Nicht-2xx-Status (`updater.rs` 529-559: `last_error` bleibt leer → `Err(ReleaseNotFound)`), unser `Err(_) => {}` machte daraus stumm denselben grauen Eintrag wie „kein Update“; geprueft wurde nur beim Start. **Fix (d73aad1, nur lib.rs + CHANGELOG):** drei Endzustaende, alle anklickbar — „Auf Beta-Stand … aktualisieren“ (installiert), „Kein Update verfügbar – erneut prüfen“, „Update-Prüfung fehlgeschlagen (HTTP 401) – erneut prüfen“ (Statuscode per eigener Diagnose-Anfrage nachgeliefert, nur Status gelesen); Benachrichtigung mit Erklaerung (Passwortschutz/Zugriffsliste am Proxy), entprellt ueber `LastCheckNotice`; Wiederhol-Thread alle 4 h (`std::thread`, ueberspringt bei abgelegtem Update); http-Server weiterhin „Update nur über https möglich“. Proxy-Zugangsdaten NICHT in den Client (T-FRG-03). `cargo fmt/clippy/test/build` gruen, 37 → 44 Tests, Rot-Nachweis 9x E0425. **Behebung beim Nutzer:** Passwortschutz vor alpha im Proxy Manager entfernen oder `/api-proxy/desktop/*` durchlassen; neuen Client einmal ueber den Browser installieren. | 2026-09-22 | d73aad1 | [260922-frg-desktop-client-update-eintrag-im-tray-ni](./quick/260922-frg-desktop-client-update-eintrag-im-tray-ni/) |
|
||||
| fast-260922-b | **Desktop-App: Download-Knoepfe in der App ohne Funktion (fast, 747a4d4).** Befund des Nutzers: „Herunterladen“ unter Einstellungen → Desktop-App tut in der App nichts (Windows und Linux). Ursache: die Webansicht hatte keinen Download-Handler — webkit2gtk verwirft Downloads dann still, WebView2 zeigte ebenfalls nichts. Fix: Hauptfenster entsteht im Code (`app.windows` in tauri.conf.json leer), weil nur `WebviewWindowBuilder` `on_download` annimmt; der Handler bricht den Download in der App ab und oeffnet die Adresse per Opener im System-Browser (Fortschritt, Speicherort, Passwortfenster fuer den Proxy). Capability `main` unveraendert. cargo fmt/clippy/test gruen. Nicht am laufenden Client geprueft (kein Display auf dem Dev-Host) — CI baut, Nachweis beim Nutzer oder auf der Windows-VM. | 2026-09-22 | 747a4d4 | — |
|
||||
| 260922-ge2 | **XFrame: Ausschnitt der Seite waehlen und einpassen, Zoom, „Nur anzeigen“.** Wunsch des Nutzers: nur einen bestimmten Ausschnitt der eingebetteten Seite zeigen, und die Groesse soll skalieren. Config: `crop {x,y,w,h}` in Seitenpixeln bei fester Layoutbreite 1280 (`XFRAME_PAGE_WIDTH`, keine UI), Klemmung ueber EINE Funktion `clampXframeCrop` (x+w ≤ 1280 verschiebt x; w ≥ 100, h ≥ 60, y+h ≤ 4000); `zoom` (50…150 %, nur Ganzseiten-Modus); `readOnly` (transparente Flaeche ueber dem Rahmen im Ansichtsmodus). Kachel: `computeCropLayout` (contain + Zentrierung, Massstab darf > 1 sein), der `<iframe>` wird selbst verschoben und skaliert (cross-origin — die Seite laesst sich von aussen nicht scrollen), Kachelmass per ResizeObserver. Einstellungen: Vorschau der Seite bei 1280 px (Stage 3000 Seitenpixel hoch, eigener Bildlauf), Rahmen als `<fieldset>` (Biome `useSemanticElements`) mit vier Eckgriffen, Ziehen per Pointer-Events mit lokalem Entwurf und genau einem PATCH beim Loslassen, Zahlenfelder als Tastaturweg; Zoom-Auswahl nur ohne Ausschnitt; Aktivieren setzt `readOnly` mit. **Befund im Browser-Rundgang, behoben (cf70a19):** Kachel und Vorschau hatten verschiedene Rahmenhoehen (max(y+h,720) vs. 3000) — bei vh-relativen Seiten (example.com `margin: 15vh`) lag derselbe Inhalt an verschiedenen Stellen, der gewaehlte Ausschnitt haette in der Kachel daneben gelegen; jetzt dieselbe Layouthoehe. Neun Pruefpunkte bestanden (Verschieben, Ecken mit fester Gegenecke und Mindestbreite, Klemmung der Zahlenfelder, Einpassen und Mitskalieren bei Kachelgroesse, Nur-anzeigen, Zoom 60 %, verweigernde Seite). Playwright kann in einem per `transform` skalierten iframe nicht selbst klicken — per `elementFromPoint` + `mouse.click` umgangen, ist eine Werkzeuggrenze. Test-Helfer `src/test/fake-resize-observer.ts`. **Zahlen:** web 604 → 640, api 1175, type-check 4/4, lint 5/5 (web 53 Warnungen unveraendert), `as unknown as` 27/6, Umlaut-Allowlist + „Ausschnitt“. | 2026-09-22 | 445b1d3,30fdd99,cf70a19 | [260922-ge2-xframe-widget-ausschnitt-der-eingebettet](./quick/260922-ge2-xframe-widget-ausschnitt-der-eingebettet/) |
|
||||
| 260922-hk4 | **Bilderrahmen-Bilder liegen jetzt im Dateibereich statt in der Datenbank.** Frage des Nutzers nach der Freigabe 1.3.0, ob `bytea` auf Dauer sinnvoll ist. Befund: Geschwindigkeit ist NICHT das Argument (ein Bild wird je Browser einmal taeglich geladen), die SICHERUNG ist es — gesichert wird von Hand per `pg_dump`, und 30 Bilder à 5 MiB je Benutzer waeren im Extremfall 150 MB pro Benutzer in jedem Abzug (alpha-DB heute 18 MB). Dazu Einheitlichkeit: Profilbilder (`user-files/avatars`, `User.avatarPath`) und DKV-Exporte liegen laengst im Volume. Umsetzung: Spalte `storagePath`, Ablage `user-files/dashboard-images/<userId>/<uuid>.<ext>` — Dateiname IMMER vom Server (UUID + Endung aus dem erkannten Mime-Typ), `originalName` nie im Pfad; ein eigener Ordner je Benutzer ist ausdruecklich KEIN Schutz, es entscheidet weiterhin die Besitzpruefung im Dienst. Umzug laeuft automatisch beim Start (`onApplicationBootstrap` ueber `forSystem()`), idempotent; die Spalte `data` bleibt bewusst vorerst stehen (Todo fuer den DROP, erst wenn alpha und live einmal gelaufen sind). **Befund im Rundgang, eigener Commit:** eine Zeile zeigte auf eine fehlende Datei (lokal Host vs. Container-Volume; im Betrieb: alter `pg_dump` + leeres Volume) — `getBytes` stellt die Datei jetzt aus der noch vorhandenen Spalte `data` wieder her, statt 404 zu melden. **Zahlen:** api 1175 → 1188, web 640, type-check 4/4, lint 5/5, RLS-Waechter 78/78. | 2026-09-22 | 9039cea,8cbfb8b,82472ee | [260922-hk4-bilderrahmen-bilder-auf-die-festplatte](./quick/260922-hk4-bilderrahmen-bilder-auf-die-festplatte/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
@@ -502,8 +503,8 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-09-22T12:15:00Z
|
||||
Resumed: 2026-09-21 (abends) ueber /gsd-resume-work; danach Bilderrahmen, XFrame (inkl. Ausschnitt), Desktop-Korrekturen, zuletzt die Freigabe 1.3.0.
|
||||
Stopped at: Version 1.3.0 freigegeben und vollstaendig nachgewiesen (Tag, Abbilder live/v1.3.0, Release mit exe + AppImage). Offen beim Nutzer: Live-Server pullen. Kein weiterer Auftrag benannt.
|
||||
Last session: 2026-09-22T13:40:00Z
|
||||
Resumed: 2026-09-21 (abends) ueber /gsd-resume-work; seitdem Bilderrahmen, XFrame (inkl. Ausschnitt), Desktop-Korrekturen, Freigabe 1.3.0, Bilder in den Dateibereich.
|
||||
Stopped at: hk4 fertig und nachgewiesen. Dem Nutzer vorgelegt: erst das Aufraeumen (Modul bringt seine Kachel selbst mit), dann Proxmox-Modul + Kachel — Antwort steht aus.
|
||||
Resume file: None
|
||||
Last activity: 2026-09-22 - Freigabe 1.3.0 (Tag v1.3.0 auf d146234), CI gruen, Release angelegt
|
||||
Last activity: 2026-09-22 - Quick 260922-hk4: Bilderrahmen-Bilder im Dateibereich, Selbstheilung aus der alten Spalte
|
||||
|
||||
@@ -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