8686b1a673
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>
261 lines
18 KiB
Markdown
261 lines
18 KiB
Markdown
---
|
||
phase: quick-260921-pi9
|
||
plan: 01
|
||
subsystem: apps/api/src/dashboard, apps/web/src/components/dashboard/widgets, apps/web/src/components/settings
|
||
tags: [dashboard, widget, bilderrahmen, upload, bytea, magic-bytes, rls, tdd, i18n]
|
||
status: complete
|
||
requires:
|
||
- "STATE.md „NAECHSTER AUFTRAG“: erstes der zwei neuen Dashboard-Widgets, Produktfragen geklaert"
|
||
- "Migration 20260911120000 (Regelform mit Benutzerdimension)"
|
||
provides:
|
||
- "Widget-Typ picture-frame: Diashow aus hochgeladenen Bildern und https-Adressen mit Grossansicht"
|
||
- "API dashboard/images: Bilder je Benutzer als bytea, Magic-Byte-Pruefung, 5 MiB / 30 Stueck, Besitz = Mandant UND Benutzer"
|
||
- "Bildverwaltung im WidgetSettingsPanel (Einstellungen -> Dashboard)"
|
||
affects:
|
||
- "apps/api/prisma/schema.prisma (neues Modell DashboardImage)"
|
||
- "apps/api/src/dashboard/*"
|
||
- "apps/web/src/components/dashboard/widget-registry.tsx (achter Typ)"
|
||
- "apps/web/src/messages/de.json, en.json (Namensraum widgets.pictureFrame)"
|
||
- "docs/mandantentrennung-zugriffsklassifikation.md (neues Paar, Zahlen nachgemessen)"
|
||
tech-stack:
|
||
added: []
|
||
patterns:
|
||
- "Magic-Byte-Erkennung als reine Funktion (kein file-type-Paket), entscheidet Annahme UND gespeicherten Typ"
|
||
- "Eintragstyp als Vereinigung mit kind-Unterscheider in EINER geordneten Liste (promote, kein Listenpaar)"
|
||
- "Fremdbilder laedt nur der Browser (<img referrerPolicy=no-referrer>), die API kennt keinen URL-Proxy"
|
||
- "Prisma-Bytes: new Uint8Array(buffer) statt Zusicherung (TS 5.9 verlangt Uint8Array<ArrayBuffer>)"
|
||
key-files:
|
||
created:
|
||
- apps/api/prisma/migrations/20260921120000_dashboard_image/migration.sql
|
||
- apps/api/src/dashboard/dashboard-image-rules.ts
|
||
- apps/api/src/dashboard/dashboard-image-rules.spec.ts
|
||
- apps/api/src/dashboard/dashboard-images.service.ts
|
||
- apps/api/src/dashboard/dashboard-images.service.spec.ts
|
||
- apps/api/src/dashboard/dashboard-images.controller.ts
|
||
- apps/api/src/dashboard/dashboard-images.controller.spec.ts
|
||
- apps/web/src/lib/dashboard-images-api.ts
|
||
- apps/web/src/lib/dashboard-images-api.test.ts
|
||
- apps/web/src/components/dashboard/widgets/picture-frame-config.ts
|
||
- apps/web/src/components/dashboard/widgets/picture-frame-config.test.ts
|
||
- apps/web/src/components/dashboard/widgets/picture-frame-widget.tsx
|
||
- apps/web/src/components/dashboard/widgets/picture-frame-widget.test.tsx
|
||
- apps/web/src/components/dashboard/widgets/picture-frame-lightbox.tsx
|
||
- apps/web/src/components/settings/picture-frame-config-form.tsx
|
||
- apps/web/src/components/settings/picture-frame-config-form.test.tsx
|
||
modified:
|
||
- apps/api/prisma/schema.prisma
|
||
- apps/api/src/dashboard/dashboard.module.ts
|
||
- apps/api/src/dashboard/dto/create-widget.dto.ts
|
||
- docs/mandantentrennung-zugriffsklassifikation.md
|
||
- apps/web/src/components/settings/widget-settings-panel.tsx
|
||
- apps/web/src/components/dashboard/widget-registry.tsx
|
||
- apps/web/src/components/dashboard/widget-registry.test.tsx
|
||
- apps/web/src/components/dashboard/widget-catalog-modal.tsx
|
||
- apps/web/src/components/dashboard/widget-catalog-modal.test.tsx
|
||
- apps/web/src/app/(portal)/page.tsx
|
||
- apps/web/src/app/(portal)/page.test.tsx
|
||
- apps/web/src/messages/de.json
|
||
- apps/web/src/messages/en.json
|
||
- apps/web/src/messages/umlaut-dictionary.ts
|
||
- CHANGELOG.md
|
||
- docs/anleitung-anwender.md
|
||
decisions:
|
||
- "Prisma-Bytes ohne Zusicherung: new Uint8Array(file.buffer) kopiert einmal je Upload (hoechstens 5 MiB) — der Plan-Hinweis „data: file.buffer geht“ stimmt unter TS 5.9 + Prisma 6 nicht, der Compiler lehnt Buffer<ArrayBufferLike> ab"
|
||
- "Widget-Test 11 prueft die Pause des Wechsels ueber das Verhalten (vier Intervalle vergehen, Bild bleibt), nicht ueber vi.getTimerCount(): React haelt nach einer Interaktion selbst einen Scheduler-Timer (gemessen 1), der Zaehler misst also nicht nur unseren Timer"
|
||
- "Klassifikationsdokument: Bereichs- und Summenzeilen nachgemessen statt +6 addiert — settings (3 -> 4) und bug-reports waren seit 260914-m97 in der Summe nie mitgezaehlt, die Paarzahl der Klassen-Verteilung stand auf 72 bei tatsaechlich 73 Zeilen; jetzt 187 gebunden / 74 Paare, beides der Messung entnommen"
|
||
- "Vorschau im Formular aria-hidden (dekorativ, die Unterschrift traegt den Sinn); das Kachelbild behaelt sein alt und damit eine Biome-Warnung der Stufe warn (onError auf <img> gilt der a11y-Regel als Interaktion — Fehlbefund)"
|
||
metrics:
|
||
duration: "ca. 75 min (18:35 bis 19:50 Uhr, 21.09.2026)"
|
||
completed: 2026-09-21
|
||
actuals:
|
||
tokens: 36000
|
||
tasks: 3
|
||
commits: 3
|
||
plan_head_before: 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)
|
||
|
||
- [x] (a) Dashboard -> Bearbeiten -> „Widget hinzufuegen“ zeigt „Bilderrahmen“ mit Rahmen-Symbol; die platzierte Kachel (8x8) zeigt „Noch keine Bilder — ueber die Einstellungen hinzufuegen“
|
||
- [x] (b) Einstellungen -> Dashboard -> „Bilderrahmen #1“ aufklappen: PNG hochladen -> Vorschau erscheint in der Liste, `GET /dashboard/images` enthaelt den Eintrag ohne `data`
|
||
- [x] (c) https-Adresse hinzufuegen -> Eintrag mit Vorschau; http-Adresse -> rote Meldung „Bitte geben Sie eine vollstaendige https-Adresse ein.“, kein Eintrag
|
||
- [x] (d) `.txt` als `.png` umbenannt hochladen -> rote Meldung „Nur Bilder im Format PNG, JPEG, GIF oder WebP sind erlaubt.“
|
||
- [x] (e) Intervall 5 s, zwei Bilder -> Kachel wechselt; Zufall mit drei Bildern -> nie dasselbe zweimal hintereinander; Bildausschnitt umschalten -> `object-cover` / `object-contain` sichtbar anders
|
||
- [x] (f) Klick auf das Bild -> Grossansicht mit Unterschrift; Escape schliesst, Hintergrund-Klick schliesst, Schliessen-Knopf schliesst; waehrend geoeffnet kein Wechsel
|
||
- [x] (g) Bearbeitungsmodus: Klick auf das Bild oeffnet nichts, die Kachel laesst sich an jeder Stelle ziehen
|
||
- [x] (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)
|
||
- [x] (i) Eintrag entfernen -> Bild verschwindet aus `GET /dashboard/images` und aus der Kachel
|
||
- [x] (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
|
||
|
||
1. **[Rule 1 - Bug] `data: file.buffer` kompiliert nicht.** Der Executor-Hinweis
|
||
„`data: file.buffer` beim Anlegen geht“ stimmt unter TS 5.9 + Prisma 6.19
|
||
nicht: `Bytes` verlangt `Uint8Array<ArrayBuffer>`, multers `Buffer` ist
|
||
ueber `ArrayBufferLike` getypt und wird abgelehnt (TS2322). Statt einer
|
||
Zusicherung kopiert `new Uint8Array(file.buffer)` einmal je Upload
|
||
(hoechstens 5 MiB). Aufgabe 1, Commit 737974b.
|
||
2. **[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_ALLOWLIST` in `apps/web/src/messages/umlaut-dictionary.ts` (Datei
|
||
nicht im Plan). Aufgabe 3, Commit c3b4597.
|
||
3. **Verify-Skript Aufgabe 1:** `git show --stat` kuerzt den Migrationspfad
|
||
auf `.../20260921120000_dashboard_image/migration.sql`, der `grep` des
|
||
Plans auf den vollen Pfad schlaegt deshalb fehl; mit `--stat=200` ist die
|
||
Migration im Commit eindeutig nachgewiesen. Kein Code-Befund.
|
||
4. **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-inventory` prueft
|
||
nur die Paartabelle und war davon nicht betroffen.
|
||
5. **Widget-Test 11** misst die Pause des Wechsels ueber das Verhalten statt
|
||
ueber `vi.getTimerCount()` (siehe decisions).
|
||
6. **Zusatz des Orchestrators umgesetzt:** Zeile und Absatz in
|
||
`docs/anleitung-anwender.md` im 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.
|