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 |
|
complete |
|
|
|
|
|
|
|
|
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?, neustoragePath String?.- Migration
20260922120000_dashboard_image_to_disk:ADD COLUMN "storagePath",ALTER COLUMN "data" DROP NOT NULL, dazusystem_read_policy … FOR SELECTauf"DashboardImage". Kein DROP — die Begründung steht im Migrationskopf (Stufe 1 von 2, T-HK4-03). DashboardImagesService:uploadlegt die Zeile an (erst danach steht die UUID fest), schreibt die Datei, trägtstoragePathnach; scheitert das Schreiben, wird die Zeile zurückgenommen und 500 geworfen.getBytesliest die Datei; fehlender Pfad oder fehlende Datei → 404.removelöscht Zeile und Datei (Dateifehler wird protokolliert, nicht geworfen).onApplicationBootstrap()zieht Altbestand um: einmal systemgebunden lesen (forSystem, Zeilen ohnestoragePathü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.mdKap. 6:user-filesnennt die Bilderrahmen-Bilder und hält fest, dasspg_dumpsie nicht mehr enthält.- Todo
.planning/todos/pending/2026-09-22-dashboard-image-data-spalte-entfernen.mdmit Vorbedingung (storagePath IS NULL= 0 auf alpha UND live), Migrationsname20260922120100_dashboard_image_drop_dataund 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 functionund Erwartungen anstoragePath, 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 aufdatei-fehltgeändert.
Nachweis am laufenden System (lokal, kein Testserver)
prisma migrate deploygegen die lokale Container-Datenbank: Migration20260922120000_dashboard_image_to_diskangewendet, danachprisma generate.\d "DashboardImage":dataist jetzt NULLbar,storagePath text, Policiestenant_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 indata - 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)
- vorher: 1 Zeile,
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") umapps/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 inonApplicationBootstrap()genauso gebaut ist.- Klassifikationsdokument: Stand
system-gebundenmit Begründung, Zahlen der Bereichszeiledashboardmit derselben Gate-Schleife nachgemessen (1/18/0 → 1/21/1; +3 gebunden = Nachtragen vonstoragePath, 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)
- Bild im Bilderrahmen-Widget hochladen → erscheint in der Kachel und in der Verwaltung unter Einstellungen → Dashboard.
- 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. - Bild löschen → verschwindet aus der Kachel UND die Datei ist weg
(
lswie oben). - 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. docker compose exec db psql -U tessera -d tessera -c 'SELECT count(*) FROM "DashboardImage" WHERE "storagePath" IS NULL;'→ muss0sein (Vorbedingung für Stufe 2, siehe Todo).
Self-Check: PASSED
apps/api/prisma/migrations/20260922120000_dashboard_image_to_disk/migration.sql— vorhandenapps/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>.pngim 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 einenpg_dumpvon VOR dem Umzug zurueckspielt, waehrend das getrennt gesicherte Volume leer ist, haette Zeilen ohne Datei, obwohl die Bytes im Abzug noch stecken. - Behoben:
getBytesschreibt die Datei in diesem Fall aus der noch vorhandenen Spaltedataneu 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.