Files
tessera-ctl/.planning/quick/260921-qd3-dashboard-widget-xframe-eine-webseite-pe/260921-qd3-SUMMARY.md
T
schalli 31ca115796
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 1m9s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 19s
Tessera CI/CD / Build & Publish Images (push) Successful in 2m46s
docs(quick-260921-qd3): Akte - XFrame-Widget gebaut, Browser-Rundgang ohne Befund; Stand nach beiden Widgets
Plan, Zusammenfassung mit abgehakter Pruefliste und die Zeile in der
Quick-Tabelle von STATE.md; Stopp-Punkt: beide bestellten Widgets fertig,
nichts offen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 19:29:07 +02:00

218 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: quick-260921-qd3
plan: 01
subsystem: apps/web/src/components/dashboard/widgets, apps/web/src/components/settings, apps/api/src/dashboard/dto
tags: [dashboard, widget, xframe, iframe, sandbox, tdd, i18n]
status: complete
requires:
- "STATE.md „NAECHSTER AUFTRAG“: zweites der zwei neuen Dashboard-Widgets, Produktfragen geklaert"
- "quick-260921-pi9 (Bilderrahmen): isHttpsUrl in picture-frame-config.ts, Muster fuer Widget/Formular/Verdrahtung"
provides:
- "Widget-Typ xframe: Webseite per https-Adresse als <iframe> in der Kachel, Sandbox ohne Top-Navigation und ohne Modals"
- "Einstellungen (Adresse, Titel, Neuladen-Intervall) im WidgetSettingsPanel, Formular als eigenes Modul"
- "Uebersetzungs-Namensraum widgets.xframe (15 Schluessel de/en)"
affects:
- "apps/web/src/components/dashboard/widget-registry.tsx (neunter Typ, Zaehler 36)"
- "apps/api/src/dashboard/dto/create-widget.dto.ts (@IsIn)"
- "apps/web/src/messages/umlaut-dictionary.ts (Allowlist: neuem)"
tech-stack:
added: []
patterns:
- "Fremdseite laedt nur der Browser (<iframe referrerPolicy=no-referrer sandbox=…>), die API kennt keinen Proxy und ruft die Adresse nie ab"
- "https-Pruefung zweifach web-seitig (Formular weist ab, Resolver rendert nichts), weil die API Config-Inhalte nicht prueft"
- "Neuladen ueber key-Wechsel des <iframe> (Adresse + Zaehler), Zaehler als data-reload-nonce sichtbar"
- "Transparente Flaeche NACH dem <iframe> im DOM nur im Bearbeitungsmodus, damit die Karte Ziehgriff bleibt"
key-files:
created:
- apps/web/src/components/dashboard/widgets/xframe-config.ts
- apps/web/src/components/dashboard/widgets/xframe-config.test.ts
- apps/web/src/components/dashboard/widgets/xframe-widget.tsx
- apps/web/src/components/dashboard/widgets/xframe-widget.test.tsx
- apps/web/src/components/settings/xframe-config-form.tsx
- apps/web/src/components/settings/xframe-config-form.test.tsx
modified:
- apps/api/src/dashboard/dto/create-widget.dto.ts
- apps/web/src/components/settings/widget-settings-panel.tsx
- apps/web/src/components/settings/widget-settings-panel.test.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:
- "Formular als eigenes Modul xframe-config-form.tsx (Orchestrator-Aenderung 1, statt Funktion im Panel wie der Plan vorsah) — gleiches Muster wie der Bilderrahmen, eigener Test mit dem echten ICU-Uebersetzer"
- "Constraints 12x12 statt 12x8 (Orchestrator-Aenderung 2): 8 Zeilen sind nur 216 px, zu flach fuer eine Webseite; Bilderrahmen wurde in 8bf3601 aus demselben Grund auf 8x12 gehoben"
- "Link „In neuem Tab öffnen“ traegt zusaetzlich zum aria-label einen sr-only-Text: Biome useAnchorContent zaehlt aria-label nicht als Inhalt, ein biome-ignore ist verboten — der sichtbare Name bleibt identisch"
- "Re-Export von isHttpsUrl aus picture-frame-config.ts, keine Kopie: eine Regel fuer „https-Adresse“ im ganzen Dashboard"
metrics:
duration: "ca. 8 min (19:12 bis 19:20 Uhr, 21.09.2026)"
completed: 2026-09-21
actuals:
tokens: 14400
tasks: 2
commits: 2
plan_head_before: 8bf3601
---
# Quick-Aufgabe 260921-qd3: Dashboard-Widget „XFrame“ Summary
Ein neues Dashboard-Widget zeigt eine Webseite per https-Adresse als
eingebetteten Rahmen in der Kachel. Die Sandbox laesst der Fremdseite
Skripte, Formulare, Popups und ihren eigenen Origin, verbietet aber die
Navigation des obersten Fensters und Modaldialoge — der Tessera-Tab bleibt
Tessera. Adresse, optionaler Titel (Kopfleiste) und Neuladen-Intervall
(nie / 1 / 5 / 10 / 30 Minuten / 1 Stunde) stehen unter Einstellungen ->
Dashboard; „In neuem Tab öffnen“ ist immer da, sobald eine Adresse gesetzt
ist. Der Server ruft die Adresse nie ab. Alle Tore sind gruen.
## Was gebaut wurde
**Resolver (`xframe-config.ts`).** Reines Modul ohne React: `url` nur wenn
String, getrimmt und vom echten `URL`-Parser als `https:` erkannt (Re-Export
`isHttpsUrl` aus dem Bilderrahmen, nicht dupliziert) — sonst `null`; `title`
getrimmt und auf 100 Zeichen gekuerzt; `reloadSeconds` nicht endlich oder
unter 60 -> 0 (nie), ab 3600 -> 3600, sonst die groesste Auswahlstufe <= n,
damit das `<select>` immer eine passende Option zeigt. `XFRAME_SANDBOX =
'allow-scripts allow-same-origin allow-forms allow-popups
allow-popups-to-escape-sandbox'`; der Dateikopf erklaert, warum
`allow-same-origin` bleibt (Origin der Fremdseite, nicht Tesseras; ohne das
Token brechen Cookies/localStorage der meisten Seiten) und welche Tokens
bewusst fehlen (Top-Navigation, Modals).
**Widget (`xframe-widget.tsx`).** Leerzustand als zentrierter grauer Text
(auch bei http/javascript/data — der Resolver liefert `null`). Sonst genau
ein `<iframe>` mit `sandbox`, `allow=""`, `referrerPolicy="no-referrer"`,
`loading="lazy"`, `title` = Titel oder Adresse, `key` aus Adresse + Zaehler,
Zaehler als `data-reload-nonce`. Mit Titel eine schmale Kopfleiste (`h2` +
Link rechts, Muster Favoriten), ohne Titel der Link als Ecksymbol
(`absolute right-1`, `top-1` im Ansichts-, `top-6` im Bearbeitungsmodus
unter der 20-px-Griffleiste). Der Link ist ein echtes `<a target="_blank"
rel="noopener noreferrer">` mit `aria-label`, `title` und sr-only-Text.
Timer nur bei `url !== null && reloadSeconds > 0 && !isEditMode`,
Aufraeumfunktion `clearInterval`. Im Bearbeitungsmodus liegt eine
transparente `div` (`absolute inset-0`, `aria-hidden`) NACH dem Rahmen im
DOM, damit `mousedown` zur Karte `.widget-drag-handle` aufsteigt.
**Formular (`xframe-config-form.tsx`, eigenes Modul).** Adresse und Titel
als Entwuerfe mit Uebernahme bei Blur/Enter; leere Adresse -> `{ url: '' }`
(Entfernen erlaubt), Nicht-https -> `role="alert"` + `aria-invalid`, kein
`onChange`; unveraenderter Wert -> kein Aufruf; Intervall als `<select>`
mit Beschriftungen Nie / Jede Minute / Alle {n} Minuten / Jede Stunde; der
Einbett-Hinweis steht dauerhaft unter dem Adressfeld. Im Panel nur der
Zweig `widgetType === 'xframe'` und die Kopfzeilen-Bedingung „— Titel“.
**Verdrahtung.** `WidgetType | 'xframe'`, `WIDGET_CONSTRAINTS.xframe =
{ minW 4, minH 4, defaultW 12, defaultH 12 }`, `XframeIcon`
(Browserfenster), Registry-Eintrag, `wireXframeWidget`, Katalog-Liste,
Import + Verdrahtung in `(portal)/page.tsx`, `'xframe'` in
`CreateWidgetDto @IsIn` (Kommentar: neun Typen). de/en: 15 Schluessel unter
`widgets.xframe`, identischer Satz; `neuem` auf der Umlaut-Allowlist.
**Doku (Commit 20a9eb2).** Changelog-Stichpunkt als erster unter
„Unveroeffentlicht -> Neu“, Zeile in der Widget-Tabelle, erweiterter Satz
und Absatz-Zusatz unter „Dashboard > Widgets“ im Anwenderhandbuch.
## Die Tests, und der Beleg dass sie rot waren
| Datei | Faelle | Rot-Lauf (vor der Umsetzung) |
|---|---:|---|
| `xframe-config.test.ts` | 12 | `pnpm --filter @tessera/web exec vitest run src/components/dashboard/widgets/xframe-config.test.ts src/components/dashboard/widgets/xframe-widget.test.tsx` -> `Error: Failed to resolve import "./xframe-config" from "src/components/dashboard/widgets/xframe-config.test.ts"`, Test Files 2 failed (2), Tests no tests |
| `xframe-widget.test.tsx` | 12 | derselbe Lauf -> `Failed to resolve import "./xframe-config" from "src/components/dashboard/widgets/xframe-widget.test.tsx"`; nach der Umsetzung erster Lauf 12/12 |
| `xframe-config-form.test.tsx` | 8 | nach dem Resolver, vor dem Formular geschrieben; erster Lauf nach der Umsetzung 8/8 |
| `widget-settings-panel.test.tsx` (Test X1) | 1 | Zweig rendert das Formular, Kopfzeile „— Board“, Blur -> `updateWidgetConfig('x1', { url })` genau einmal |
Zusammen 33 neue Faelle; Registry-/Katalog-/Seiten-Tests laufen mit dem
neunten Typ (Erwartungstabelle um `xframe`, `counted` 32 -> 36, Attrappen um
`xframe.*` und das neue Widget-Modul ergaenzt). Web-Tests 569 -> 603
(+34: 33 neue Faelle + 1 durch `it.each` in der Registry).
## Messungen (Endstand, HEAD 20a9eb2)
| Groesse | Ausgang (8bf3601) | Jetzt |
|---|---:|---:|
| `pnpm type-check` | 4/4 | 4/4 |
| `pnpm lint` | 5/5 | 5/5 (api 74 Warnungen, web 53, keine Stufe `error`) |
| API-Tests | 1175 | **1175** (75 Dateien) |
| Web-Tests | 569 | **603** (80 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` / `!` / `any` in den neuen Dateien | – | 0 / 0 / 0 |
| de/en-Schluesselgleichheit `widgets.xframe` | – | 15 = 15 |
Keine neue `any`, kein `!`, kein neues Paket, kein Prisma-/Schema-Pfad.
## Pruefliste fuer den Browser-Rundgang (Orchestrator, lokal, Playwright-MCP — NICHT Testserver)
- [x] (a) Dashboard -> Bearbeiten -> „Widget hinzufügen“ zeigt „XFrame“ mit Fenster-Symbol und Beschreibung „Webseite einbetten“; die platzierte Kachel ist 12x12 und zeigt „Keine Adresse eingestellt — über die Einstellungen festlegen“
- [x] (b) Einstellungen -> Dashboard -> „XFrame #1“ aufklappen: Hinweistext „Manche Webseiten lassen sich nicht einbetten …“ steht dauerhaft da; https-Adresse einer einbettbaren Seite (z. B. eine interne Tessera-Seite oder `https://example.com`) eintragen, Feld verlassen -> Kachel zeigt die Seite
- [x] (c) `http://…` eintragen -> rote Meldung „Bitte geben Sie eine vollständige https-Adresse ein.“, Netzwerk-Tab zeigt keinen PATCH
- [x] (d) Titel „Board“ -> Kopfleiste mit Titel und Symbol „In neuem Tab öffnen“ rechts; Titel leeren -> Symbol wandert in die rechte obere Ecke
- [x] (e) Klick auf das Symbol oeffnet die Adresse in einem neuen Tab, der Tessera-Tab bleibt; Tab-Taste erreicht das Symbol
- [x] (f) Intervall „Jede Minute“ -> nach 60 s wird der Rahmen neu geladen (Netzwerk-Tab: zweiter Dokumentabruf; im DOM springt `data-reload-nonce` auf 1)
- [x] (g) Bearbeitungsmodus: Kachel laesst sich an einer Stelle ueber dem Rahmen anfassen und ziehen, Groesse aendern funktioniert, im DOM liegt `xframe-edit-overlay`; Ansichtsmodus: Overlay weg, Seite bedienbar (Scrollen/Klicken im Rahmen)
- [x] (h) Adresse einer Seite, die Einbetten verweigert (z. B. `https://www.google.com`) -> Rahmen bleibt leer (Browser-Konsole meldet `X-Frame-Options`/`frame-ancestors`), „In neuem Tab öffnen“ funktioniert
- [x] (i) API-Log waehrend (b)/(f): kein Abruf der Fremdadresse durch die API — nur der Browser laedt sie
- [x] (j) `pnpm --filter @tessera/api test` (1175) und `pnpm --filter @tessera/web test` (603) gruen, Zaehler wie Ausgangsmessung (siehe Tabelle oben)
**Rundgang durch den Orchestrator am 21.09.2026 (lokaler Stack, Abbilder aus 20a9eb2, Playwright-MCP):** alle Punkte bestanden. Belege: (a) Katalogeintrag „XFrame — Webseite einbetten“ mit Symbol, Kachel 801x328 px (12x12), Leerhinweis; (b) Hinweistext dauerhaft unter dem Adressfeld, `https://example.com/` → Kachel zeigt „Example Domain“ im Rahmen (`sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"`, `referrerpolicy=no-referrer`, `allow=""`, `loading=lazy`); (c) `http://example.com` → Meldung, `aria-invalid`, kein PATCH; https danach genau ein PATCH; (d) Titel „Board“ → Kopfleiste mit `<h2>` und Link in der Leiste (`border-b`-Vorfahr); (e) Klick oeffnet `https://example.com/` in einem neuen Tab, Tessera-Tab bleibt auf `/`, Tab-Taste erreicht den Link; (f) „Jede Minute“: `data-reload-nonce` 0 → 1 nach 62 s, genau ein zweiter Dokumentabruf von example.com; (g) Bearbeitungsmodus: Overlay `aria-hidden` NACH dem iframe, Ziehen ueber den Rahmen verschiebt die Kachel (`translate` 8 → 278 px), Groesse aendern 801x328 → 935x636, nach dem Speichern kein Overlay; (h) `https://www.google.com/` → Konsole „Refused to display … 'X-Frame-Options' to 'sameorigin'“, Rahmen leer, Link fuehrt zur Seite; (i) API-Log ohne Treffer auf `example.com`/`google.com`. Keine Befunde, keine Korrektur noetig.
## Deviations from Plan
1. **Orchestrator-Aenderung 1 umgesetzt:** das Einstellungsformular liegt
als `XframeConfigForm` in `apps/web/src/components/settings/xframe-config-form.tsx`
mit eigenem Test (8 Faelle, `createTranslator`-Attrappe wie beim
Bilderrahmen) statt als Funktion im Panel; das Panel traegt nur den
Zweig, die Kopfzeilen-Bedingung und einen Testfall (X1). Aufgabe 1,
Commit d63d9f5.
2. **Orchestrator-Aenderung 2 umgesetzt:** Constraints `12x12` statt `12x8`
(Registry, Registry-Test, Pruefpunkt (a)). Aufgabe 1, Commit d63d9f5.
3. **[Rule 3 - Blocking] Biome `useAnchorContent`** meldete den Link
„In neuem Tab öffnen“ (nur `aria-hidden`-SVG als Kind, `aria-label`
zaehlt fuer die Regel nicht als Inhalt) als neue Warnung (web 53 -> 54).
Kein `biome-ignore` erlaubt — der Link traegt zusaetzlich einen
`sr-only`-Text mit demselben Namen; `aria-label` und `title` bleiben wie
im Plan. Web-Lint wieder 53. Aufgabe 1, Commit d63d9f5.
4. **Widget-Test 7** prueft `vi.getTimerCount()` direkt nach dem Rendern
(ohne vorherige Interaktion haelt React keinen Scheduler-Timer, gemessen 1
= nur unser Intervall) — der pi9-Vorbehalt (Test 11 dort) trat hier nicht
auf.
5. **Zwei zusaetzliche Widget-Faelle** ueber die geforderten 10 hinaus
(Test 11 Ansichtsmodus ohne Overlay, Test 12 `top-1`/`top-6`); Resolver
genau 12 wie gefordert.
Nicht geaendert: `STATE.md`, `ROADMAP.md`, keine neue Abhaengigkeit, kein
Deploy, kein Zugriff auf den Testserver.
## Known Stubs
Keine. Kette verdrahtet: Katalog -> `POST /dashboard/widgets` (`@IsIn`) ->
Kachel ueber `WIDGET_REGISTRY.xframe.component`; Formular -> `isHttpsUrl`
-> `PATCH /dashboard/widgets/:id/config` -> `resolveXframeConfig` ->
`<iframe>`; Neuladen -> `key`-Wechsel; Bearbeitungsmodus -> Overlay.
## Threat Flags
Keine neue Flaeche ausserhalb des `<threat_model>` des Plans: T-QD3-01
(Sandbox ohne Top-Navigation/Modals), -03 (https zweifach), -04 (kein
Server-Abruf), -05 (no-referrer, noopener noreferrer), -06 (`allow=""`),
-07 (Overlay), -08 (Klemmung 60..3600, kein Timer beim Bearbeiten), -09
(Titel nur als Textknoten) sind je per Test belegt; -04 zusaetzlich
Rundgangspunkt (i).
## Self-Check: PASSED
Alle 6 neu angelegten Dateien liegen auf der Platte, die zwei Commits
d63d9f5 und 20a9eb2 sind in `git log` auffindbar
(`git rev-list --count 8bf3601..HEAD` = 2). Die Zahlen der Tabelle stammen
aus tatsaechlich gelaufenen Befehlen.