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>
15 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-qd3 | 01 | apps/web/src/components/dashboard/widgets, apps/web/src/components/settings, apps/api/src/dashboard/dto |
|
complete |
|
|
|
|
|
|
|
|
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-nonceauf 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 meldetX-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) undpnpm --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
- Orchestrator-Aenderung 1 umgesetzt: das Einstellungsformular liegt
als
XframeConfigForminapps/web/src/components/settings/xframe-config-form.tsxmit 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, Commitd63d9f5. - Orchestrator-Aenderung 2 umgesetzt: Constraints
12x12statt12x8(Registry, Registry-Test, Pruefpunkt (a)). Aufgabe 1, Commitd63d9f5. - [Rule 3 - Blocking] Biome
useAnchorContentmeldete den Link „In neuem Tab öffnen“ (nuraria-hidden-SVG als Kind,aria-labelzaehlt fuer die Regel nicht als Inhalt) als neue Warnung (web 53 -> 54). Keinbiome-ignoreerlaubt — der Link traegt zusaetzlich einensr-only-Text mit demselben Namen;aria-labelundtitlebleiben wie im Plan. Web-Lint wieder 53. Aufgabe 1, Commitd63d9f5. - 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. - 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.