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

15 KiB
Raw Blame History

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-qd3 01 apps/web/src/components/dashboard/widgets, apps/web/src/components/settings, apps/api/src/dashboard/dto
dashboard
widget
xframe
iframe
sandbox
tdd
i18n
complete
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
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)
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)
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
created modified
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
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
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
duration completed
ca. 8 min (19:12 bis 19:20 Uhr, 21.09.2026) 2026-09-21
tokens tasks commits
14400 2 2
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)

  • (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“
  • (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
  • (c) http://… eintragen -> rote Meldung „Bitte geben Sie eine vollständige https-Adresse ein.“, Netzwerk-Tab zeigt keinen PATCH
  • (d) Titel „Board“ -> Kopfleiste mit Titel und Symbol „In neuem Tab öffnen“ rechts; Titel leeren -> Symbol wandert in die rechte obere Ecke
  • (e) Klick auf das Symbol oeffnet die Adresse in einem neuen Tab, der Tessera-Tab bleibt; Tab-Taste erreicht das Symbol
  • (f) Intervall „Jede Minute“ -> nach 60 s wird der Rahmen neu geladen (Netzwerk-Tab: zweiter Dokumentabruf; im DOM springt data-reload-nonce auf 1)
  • (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)
  • (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
  • (i) API-Log waehrend (b)/(f): kein Abruf der Fremdadresse durch die API — nur der Browser laedt sie
  • (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.