Plan, Zusammenfassung (mit Rot-Nachweis, curl-Rundgang, abgehakter Pruefliste und den drei im Rundgang gefundenen Befunden) sowie die Zeile in der Quick-Tabelle von STATE.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
18 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-260921-pi9 | 01 | apps/api/src/dashboard, apps/web/src/components/dashboard/widgets, apps/web/src/components/settings |
|
complete |
|
|
|
|
|
|
|
|
573d070 |
Quick-Aufgabe 260921-pi9: Dashboard-Widget „Bilderrahmen“ Summary
Ein neues Dashboard-Widget zeigt eigene Bilder als Diashow: hochgeladen (in der Datenbank, dem Benutzer gehoerend, 5 MiB je Datei, 30 je Benutzer) oder per https-Adresse eingebunden (der Browser laedt sie direkt, der Server ruft nie eine Adresse ab). Bildausschnitt, Wechselintervall, Reihenfolge/Zufall und Bildunterschrift stellt der Benutzer unter Einstellungen -> Dashboard ein; ein Klick zeigt das Bild gross. Alle Tore sind gruen, der curl-Rundgang lief gegen die lebende lokale API.
Was gebaut wurde
API (Commit 737974b). Prisma-Modell DashboardImage (data Bytes, keine
Relation) mit handgeschriebener Migration 20260921120000_dashboard_image:
Tabelle, beide Indizes, ENABLE/FORCE ROW LEVEL SECURITY und
tenant_isolation_policy mit Benutzerdimension von Anfang an. Die Migration
ist lokal angewendet (prisma migrate status: keine ausstehende), prisma generate gelaufen. dashboard-image-rules.ts erkennt PNG/JPEG/GIF/WebP an
den Magic Bytes — file.mimetype und Dateiendung werden nie gelesen, der
erkannte Typ ist zugleich der gespeicherte und der spaeter ausgelieferte.
DashboardImagesService (list/upload/getBytes/remove) holt je Methode
const tenantPrisma = forTenant(this.prisma, tenantId, userId); Liste und
Zaehler filtern explizit { tenantId, userId }, getBytes/remove pruefen
Besitz gegen Mandant UND Benutzer und antworten sonst 404 (nie 403).
DashboardImagesController unter dashboard/images: GET -> POST
(FileInterceptor('image', 5 MiB, eine Datei)) -> GET :id (Content-Type
aus dem gespeicherten Typ, Cache-Control: private, max-age=86400,
X-Content-Type-Options: nosniff, Content-Disposition: inline ohne
Dateinamen, CSP default-src 'none'; sandbox) -> DELETE :id. Kein
@Roles. CreateWidgetDto kennt 'picture-frame'.
Web (Commit c080580). picture-frame-config.ts: PictureFrameEntry als
Vereinigung (upload | url) in EINER Liste, resolvePictureFrameConfig
laesst alles weg, was der URL-Parser nicht als https: erkennt (T-PI9-07:
die API prueft Config-Inhalte nicht, deshalb entscheidet allein diese Funktion,
was zum src wird), Intervall 0 oder 5..3600 s (Vorgabe 30), pickNextIndex
(Zufall zieht aus count-1 Kandidaten, nie das aktuelle). dashboard-images-api.ts
schickt die Datei als FormData-Feld image ohne eigenen Content-Type, macht
aus 413 die deutsche Meldung, 400-Meldungen kommen bereits deutsch von der API.
PictureFrameWidget: Leerhinweis im Stil der anderen Widgets, <img referrerPolicy="no-referrer"> (Upload ueber /api-proxy/dashboard/images/:id,
URL direkt), object-contain/object-cover, Unterschrift als Streifen, Timer
nur bei > 1 Bild und Intervall > 0 und geschlossener Grossansicht (Raeumung im
Cleanup), kaputte Bilder verlassen den Umlauf („Bild nicht verfuegbar“, wenn
alle). Ausserhalb des Bearbeitungsmodus liegt das Bild in einem <button>
(Grossansicht PictureFrameLightbox: Dialog fokussiert, Escape/Hintergrund/
Schliessen-Knopf, danach Fokus zurueck am Bild-Knopf); im Bearbeitungsmodus ein
<div> ohne Handler — die Karte bleibt der Ziehgriff. PictureFrameConfigForm
im WidgetSettingsPanel: drei Auswahlfelder (senden nur ihr Feld), Eintragsliste
mit Vorschau/Unterschrift (Entwurf, Uebernahme bei Blur/Enter)/Pfeilen/Entfernen
(Upload wird auch serverseitig geloescht, Fehler verschluckt), Datei hochladen
(deaktiviert ab 30), Webadresse hinzufuegen (http -> role="alert", kein
onChange). Listenaenderungen senden IMMER das ganze images-Array. Registry
(minW 4, minH 4, defaultW 8, defaultH 8), Katalog, Seite, 28 Schluessel je
Sprache unter widgets.pictureFrame.
Doku (Commit c3b4597). Changelog-Stichpunkt als erster unter
„Unveroeffentlicht -> Neu“, Zeile in der Widget-Tabelle und Absatz unter
„Dashboard > Widgets“ im Anwenderhandbuch, zwei Woerter auf der Erlaubnisliste
des Umlaut-Waechters (siehe Deviations).
Die Tests, und der Beleg dass sie rot waren
| Datei | Faelle | Rot-Lauf (vor der Umsetzung) |
|---|---|---|
dashboard-image-rules.spec.ts |
10 | pnpm --filter @tessera/api exec vitest run src/dashboard/dashboard-image-rules.spec.ts -> Error: Cannot find module './dashboard-image-rules', 1 Test File failed, 10 Faelle nicht ausfuehrbar |
dashboard-images.service.spec.ts |
12 | ... vitest run src/dashboard/dashboard-images.service.spec.ts -> Cannot find module './dashboard-images.service', 1 failed |
dashboard-images.controller.spec.ts |
5 | ... vitest run src/dashboard/dashboard-images.controller.spec.ts -> Cannot find module './dashboard-images.controller', 1 failed |
picture-frame-config.test.ts |
11 | pnpm --filter @tessera/web exec vitest run src/components/dashboard/widgets/picture-frame-config.test.ts src/lib/dashboard-images-api.test.ts -> Failed to resolve import "./picture-frame-config", 2 Test Files failed |
dashboard-images-api.test.ts |
5 | derselbe Lauf -> Failed to resolve import "./dashboard-images-api" |
picture-frame-widget.test.tsx |
12 | nach der Umsetzung geschrieben (Plan verlangt Rot nur fuer Regel-/Dienst-/Helfer-Tests); erster Lauf 11/12, Test 11 wegen des React-Scheduler-Timers umgestellt (siehe decisions) |
picture-frame-config-form.test.tsx |
9 | nach der Umsetzung geschrieben; erster Lauf 9/9 |
Zusammen 64 neue Faelle; die bestehenden Registry-/Katalog-/Seiten-Tests laufen
mit dem achten Typ (Constraints-Tabelle, counted 28 -> 32, Attrappen um
pictureFrame.* und das neue Widget-Modul ergaenzt).
curl-Rundgang gegen die lebende API
Die Container api/web lagen mit einem alten Image still; die API lief
deshalb aus dem Quelltext (nest build + node dist/main.js) gegen eine
eigens angelegte, leere Datenbank tessera_pi9 auf dem lokalen db-Container
(Migrationen angewendet, Admin per Erstanlage), danach wieder geloescht. Kein
Zugriff auf den Testserver.
| Schritt | Ergebnis |
|---|---|
POST /dashboard/images mit 4x4-PNG |
201, { id, originalName, mimeType: "image/png", size: 73, createdAt } |
GET /dashboard/images |
200, Liste mit denselben fuenf Feldern, kein data |
GET /dashboard/images/<id> |
200, Content-Type: image/png, Cache-Control: private, max-age=86400, X-Content-Type-Options: nosniff, Content-Disposition: inline, Content-Security-Policy: default-src 'none'; sandbox; Bytes per cmp identisch mit der Quelle |
Textdatei als .png (type=image/png) |
400 Nur Bilder im Format PNG, JPEG, GIF oder WebP sind erlaubt. |
| 6-MiB-Datei | 413 File too large (multer/Nest, im Web-Klienten deutsch) |
| erfundene Kennung | 404 |
| ohne Cookie | 401 |
Kennung mit dem Cookie eines ZWEITEN Benutzers (GET und DELETE) |
404 / 404 (Punkt (h) der Pruefliste bereits erledigt) |
eigener DELETE |
200 { id }, Liste danach [] |
Messungen (Endstand, HEAD c3b4597)
| Groesse | Ausgang (573d070) |
Jetzt |
|---|---|---|
pnpm type-check |
4/4 | 4/4 |
pnpm lint |
5/5 | 5/5 (api 74 Warnungen, web 53, keine Stufe error) |
| API-Tests | 1148 | 1175 (75 Dateien) |
| Web-Tests | 531 | 569 (77 Dateien) |
as unknown as in apps/api/src |
27 | 27 |
as unknown as in apps/web/src |
6 | 6 |
noNonNullAssertion in apps/api/src (biome) |
56 | 56 |
noExplicitAny in apps/api/src (biome) |
13 | 13 |
biome-ignore in apps/api/src |
1 | 1 |
ts-expect-error |
0 | 0 |
dangerouslySetInnerHTML in den drei neuen Komponenten |
– | 0 |
RLS-Waechter src/prisma |
30/30 laut Plan | 78/78 (davon rls-coverage + rls-access-inventory 35/35) |
de/en-Schluesselgleichheit widgets.pictureFrame |
– | 28 = 28 |
Keine neue any, kein !, kein neues Paket.
Pruefliste fuer den Browser-Rundgang (Orchestrator, lokal, Playwright-MCP)
- (a) Dashboard -> Bearbeiten -> „Widget hinzufuegen“ zeigt „Bilderrahmen“ mit Rahmen-Symbol; die platzierte Kachel (8x8) zeigt „Noch keine Bilder — ueber die Einstellungen hinzufuegen“
- (b) Einstellungen -> Dashboard -> „Bilderrahmen #1“ aufklappen: PNG hochladen -> Vorschau erscheint in der Liste,
GET /dashboard/imagesenthaelt den Eintrag ohnedata - (c) https-Adresse hinzufuegen -> Eintrag mit Vorschau; http-Adresse -> rote Meldung „Bitte geben Sie eine vollstaendige https-Adresse ein.“, kein Eintrag
- (d)
.txtals.pngumbenannt hochladen -> rote Meldung „Nur Bilder im Format PNG, JPEG, GIF oder WebP sind erlaubt.“ - (e) Intervall 5 s, zwei Bilder -> Kachel wechselt; Zufall mit drei Bildern -> nie dasselbe zweimal hintereinander; Bildausschnitt umschalten ->
object-cover/object-containsichtbar anders - (f) Klick auf das Bild -> Grossansicht mit Unterschrift; Escape schliesst, Hintergrund-Klick schliesst, Schliessen-Knopf schliesst; waehrend geoeffnet kein Wechsel
- (g) Bearbeitungsmodus: Klick auf das Bild oeffnet nichts, die Kachel laesst sich an jeder Stelle ziehen
- (h)
curl -b <cookie zweiter Benutzer> -o /dev/null -w '%{http_code}' .../dashboard/images/<id>-> 404 (bereits mit curl gegen die lokale API belegt, siehe Rundgang oben; im Browser optional wiederholen) - (i) Eintrag entfernen -> Bild verschwindet aus
GET /dashboard/imagesund aus der Kachel - (j) Netzwerk-Tab: das Fremdbild laedt der Browser selbst (Anfrage an den Fremdhost mit
Referrer Policy: no-referrer), im API-Log kein Aufruf der Fremdadresse
Rundgang durch den Orchestrator am 21.09.2026 (lokaler Stack, Abbilder aus HEAD, Playwright-MCP): alle zehn Punkte bestanden. Belege: (a) Katalog zeigt „Bilderrahmen“ mit Beschreibung, Leerhinweis in der Kachel; (b) rot.png hochgeladen, Vorschau 320 px, GET /dashboard/images liefert {id, originalName, mimeType, size, createdAt} ohne data; (c) http://example.com/bild.png → Meldung, kein Eintrag; https://www.gstatic.com/webp/gallery/1.webp → Eintrag mit Vorschau (eine zuvor eingetragene, serverseitig 400 liefernde Wikimedia-Adresse zeigte korrekt „Bild nicht verfügbar“); (d) Textdatei als .png → „Nur Bilder im Format PNG, JPEG, GIF oder WebP sind erlaubt.“; (e) Intervall 5 s: blau → gstatic → rot → blau im Sekundentakt gemessen; Zufall mit drei Bildern: 8 Wechsel in 42 s, nie dasselbe zweimal hintereinander; object-cover nach Umschalten; (f) Grossansicht nach Portal-Korrektur 8bf3601 ueber den ganzen Viewport (Hintergrund 1905x949), Escape schliesst mit Fokusrueckgabe auf „Bild groß anzeigen“, Hintergrund-Klick schliesst, waehrend geoeffnet 6 s lang kein Wechsel; (g) Bearbeitungsmodus: kein Bild-Knopf im Widget, Ziehen ueber die Bildflaeche verschiebt die Kachel (translate 8 → 480 px), kein Dialog; (h) siehe curl; (i) zweiten Eintrag entfernt → GET /dashboard/images nur noch rot.png, Config nur noch zwei Eintraege; (j) Netzwerk: gstatic-Abruf kommt vom Browser, API-Log ohne Treffer auf gstatic.
Drei Befunde aus dem Rundgang, behoben in 8bf3601: (1) die Grossansicht war auf die Kachelflaeche (531x216) beschraenkt — die Kachel liegt in einem react-grid-item mit CSS-transform, und ein transformierter Vorfahr wird fuer position: fixed zum Bezugsrahmen; jetzt createPortal in document.body wie der Kalender-Tooltip; (2) „1 Minuten“ im Wechselintervall → ICU-Plural in de/en, Formular-Test nutzt dafuer createTranslator von next-intl auf der echten de.json; (3) Standardgroesse 8x8 (216 px hoch) zu flach → 8x12 wie der Kalender. Web-Tests 569 unveraendert in der Zahl (ein Fall um die Singular-Pruefung ergaenzt).
Deviations from Plan
- [Rule 1 - Bug]
data: file.bufferkompiliert nicht. Der Executor-Hinweis „data: file.bufferbeim Anlegen geht“ stimmt unter TS 5.9 + Prisma 6.19 nicht:BytesverlangtUint8Array<ArrayBuffer>, multersBufferist ueberArrayBufferLikegetypt und wird abgelehnt (TS2322). Statt einer Zusicherung kopiertnew Uint8Array(file.buffer)einmal je Upload (hoechstens 5 MiB). Aufgabe 1, Commit737974b. - [Rule 3 - Blocking] Umlaut-Waechter. Der volle Web-Testlauf meldete die
neuen de.json-Woerter „Bildausschnitt“ und „Webadresse“ als unbekannte
ss-Tokens. Beide sind korrektes Deutsch und stehen jetzt auf
UMLAUT_ALLOWLISTinapps/web/src/messages/umlaut-dictionary.ts(Datei nicht im Plan). Aufgabe 3, Commitc3b4597. - Verify-Skript Aufgabe 1:
git show --statkuerzt den Migrationspfad auf.../20260921120000_dashboard_image/migration.sql, dergrepdes Plans auf den vollen Pfad schlaegt deshalb fehl; mit--stat=200ist die Migration im Commit eindeutig nachgewiesen. Kein Code-Befund. - Klassifikationsdokument, mehr als die geplante eine Zeile: die
Nachmessung mit der Gate-Schleife ergab, dass Bereichs- und Summenzeilen
bereits vor dieser Aufgabe um zwei Rohtreffer (settings 3 statt 4,
bug-reports nie summiert) und die Klassen-Verteilung um ein Paar
(bug-reports) hinterherhingen. Beides ist nachgezogen und im Dokument als
Nachtrag 260921-pi9 begruendet; der Waechter
rls-access-inventoryprueft nur die Paartabelle und war davon nicht betroffen. - Widget-Test 11 misst die Pause des Wechsels ueber das Verhalten statt
ueber
vi.getTimerCount()(siehe decisions). - Zusatz des Orchestrators umgesetzt: Zeile und Absatz in
docs/anleitung-anwender.mdim Aufgabe-3-Commit.
Nicht geaendert: STATE.md, ROADMAP.md, keine neue Abhaengigkeit, kein
Deploy, kein Zugriff auf den Testserver.
Known Stubs
Keine. Jede Kette ist verdrahtet: Datei -> Upload -> Config -> Kachel -> Proxy -> API -> Bytes; https-Adresse -> Config -> Kachel -> Browser.
Threat Flags
Keine neue Flaeche ausserhalb des <threat_model> des Plans: die vier Routen
unter dashboard/images und die <img>-Fremdabrufe sind dort als T-PI9-01
bis T-PI9-11 erfasst und mitigiert; Content-Disposition ohne Dateinamen und
Cache-Control: private sind mit curl belegt.
Self-Check: PASSED
Alle 16 neu angelegten Dateien liegen auf der Platte, die drei Commits
737974b, c080580 und c3b4597 sind in git log auffindbar
(git rev-list --count 573d070..HEAD = 3). Die Zahlen der Tabelle stammen
aus tatsaechlich gelaufenen Befehlen.