20 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-260922-ge2 | 01 | apps/web/src/components/dashboard/widgets, apps/web/src/components/settings, apps/web/src/test |
|
complete |
|
|
|
|
|
|
|
|
5aa577a |
Quick-Aufgabe 260922-ge2: XFrame-Widget — Ausschnitt, Zoom, „Nur anzeigen“ Summary
Das XFrame-Widget zeigt auf Wunsch nur einen Ausschnitt der eingebetteten
Seite: in den Einstellungen erscheint eine Vorschau der Seite bei fester
Breite 1280 px, darueber ein Rahmen, den der Benutzer verschiebt und an
den vier Ecken zieht (oder Links/Oben/Breite/Hoehe eintippt); die Kachel
zeigt genau diesen Ausschnitt, eingepasst und zentriert, und skaliert ihn
mit der Kachelgroesse. Ohne Ausschnitt gibt es eine Vergroesserung
50–150 % fuer die ganze Seite; „Nur anzeigen“ sperrt Klicken und Scrollen
im Rahmen und wird beim Einschalten des Ausschnitts automatisch mit
gesetzt. Sandbox, no-referrer, allow="", Neuladen, Kopfleiste und
Bearbeitungsflaeche sind unveraendert; der Server ruft die Adresse
weiterhin nie ab. Alle Tore gruen.
Was gebaut wurde
Resolver (xframe-config.ts, Commit 445b1d3). Neue Exporte
XFRAME_PAGE_WIDTH 1280, XFRAME_CROP_MIN_W 100, XFRAME_CROP_MIN_H 60,
XFRAME_CROP_MAX_H 4000, XFRAME_CROP_DEFAULT {0,0,1280,720},
XFRAME_ZOOM_OPTIONS [50,60,75,90,100,125,150], XFRAME_ZOOM_DEFAULT 100,
interface XframeCrop, clampXframeCrop (runden; w auf [100,1280], h auf
[60,4000], x/y >= 0, dann x + w > 1280 -> x = 1280 - w — verschieben, nie
abweisen). XframeConfig um crop: XframeCrop | null, zoom, readOnly
erweitert; resolveCrop nimmt nur ein Nicht-null-Objekt mit vier endlichen
Zahlen (Typwaechter isRecord, kein Cast), resolveZoom liefert die
groesste Stufe <= n (unter 50 -> 50, nicht endlich -> 100), resolveReadOnly
nur bei === true. Der Dateikopf erklaert, warum geklemmt und nie
abgewiesen wird (T-GE2-05) und dass zoom nur ohne Ausschnitt wirkt.
Geometrie (xframe-crop.ts, NEU, ohne React). XFRAME_PREVIEW_PAGE_HEIGHT
3000; computeCropLayout(crop, tileW, tileH) -> { scale, left, top, frameHeight } mit scale = min(tileW/w, tileH/h) (contain, darf > 1 sein),
Zentrierung im Rest, frameHeight = max(y + h, 720); Nullkachel ->
scale 0 (nichts rendern, keine Division durch 0). applyCropDrag(mode, start, dxPage, dyPage): move verschiebt bei fester Groesse und klemmt an
den Seitenraendern; nw/ne/sw/se bewegen nur die zwei Kanten ihrer Ecke,
die Gegenecke bleibt, Mindestgroesse an der bewegten Kante; Ergebnis durch
clampXframeCrop.
Widget (xframe-widget.tsx). Kachelkoerper mit ref, overflow-hidden
und — nur bei Ausschnitt — data-tile-size="WxH"; ein useEffect haengt
nur bei crop !== null einen ResizeObserver an (Muster dashboard-grid),
Aufraeumfunktion disconnect(). Drei Render-Zweige mit identischen
frameAttrs (src, title, sandbox, allow, referrerPolicy, loading,
data-testid, data-reload-nonce) und key: (1) Ausschnitt — Clip-div
(xframe-crop-clip, absolut, overflow: hidden, Groesse w·scale × h·scale, Position left/top aus dem Layout) mit dem <iframe> darin
(left = -x·scale, top = -y·scale, width 1280, height frameHeight,
transform: scale(s), transformOrigin 0 0, Klasse ohne h-full w-full),
gerendert erst ab scale > 0; (2) Zoom ≠ 100 ohne Ausschnitt — absolut
positionierter Rahmen mit width/height = 10000/z % (60 % -> 166.67 %) und
transform: scale(z); (3) 100 % — exakt wie bisher, kein style. Nach dem
Rahmen: isEditMode -> xframe-edit-overlay; !isEditMode && readOnly ->
xframe-readonly-overlay (absolute inset-0, aria-hidden); nie beide.
Ecksymbol-Link mit z-10 bleibt ueber beiden. Dateikopf: warum der Rahmen
selbst verschoben und skaliert wird (cross-origin kein Scrollen von
aussen), warum die Layoutbreite fest 1280 ist, und dass readOnly
Bedienkomfort und keine Sicherheitsmassnahme ist (T-GE2-02).
Formular (xframe-config-form.tsx). Unter dem Intervall-Feld in dieser
Reihenfolge: Checkbox xframe-crop-enable (Einschalten -> EIN onChange({ crop: XFRAME_CROP_DEFAULT, readOnly: true }), Ausschalten -> { crop: null }); bei Ausschnitt Hinweis cropPreviewHint, CropPreview und
CropNumberFields (mit key aus den vier Werten -> State-Reset nach
Ziehen/Speichern); ohne Ausschnitt die Zoom-Auswahl xframe-zoom; Checkbox
xframe-readonly; dauerhaft cropHint. CropPreview: aeusserer Behaelter
420 px hoch mit eigenem Bildlauf, Breite per ResizeObserver, p = width / 1280 (vor der Messung 0.5); Buehne 1280·p × 3000·p mit dem Vorschau-
<iframe> (dieselbe XFRAME_SANDBOX, allow="", no-referrer, lazy,
width 1280, height 3000, transform: scale(p), pointer-events: none,
T-GE2-01/-03) und dem Rahmen (<fieldset aria-label>, xframe-crop-rect,
cursor-move touch-none border-2 border-primary) mit vier Griffen
(xframe-crop-handle-{nw,ne,sw,se}, h-3 w-3 bg-primary, Ecken-Cursor).
Pointer-Logik als Fabrik dragProps(mode): onPointerDown nur Haupttaste,
Griffe stopPropagation, Zugzustand in dragRef, Capture per
capturePointer (typeof-Waechter); onPointerMove -> Entwurf aus
applyCropDrag mit Deltas / p; onPointerUp -> Capture loesen, onCommit
nur bei Aenderung gegenueber crop (sameCrop); onPointerCancel ->
verwerfen ohne Aufruf; alle drei pruefen drag.mode === mode, damit das
Bubbling vom Griff zum Rahmen (jsdom ohne Capture) nichts doppelt
ausloest. Ohne Adresse statt der Buehne xframe-crop-preview-empty.
CropNumberFields: vier type="number"-Felder xframe-crop-{x,y,w,h} im
4er-Raster, Uebernahme bei Blur/Enter, Number(draft) endlich ->
clampXframeCrop({ ...crop, [k]: n }), leer/abc/unveraendert -> nichts;
darunter cropUnitHint.
Test-Helfer (apps/web/src/test/fake-resize-observer.ts, NEU).
stubResizeObserver({ width, height }) stubbt ResizeObserver dateiweise
per vi.stubGlobal mit DOMRectReadOnly-contentRect — ein einziger Cast
as ResizeObserverEntry; Aufraeumen per vi.unstubAllGlobals(). Kein
measuredSize-Prop im Produktionscode.
Uebersetzungen. 13 neue Schluessel unter widgets.xframe in de UND en
(28 = 28 Schluessel gesamt, Gleichheit per Skript geprueft), Deutsch mit
„Sie“; Ausschnitt auf der Umlaut-Allowlist nach neuem.
Doku (Commit 30fdd99). Changelog-Stichpunkt direkt nach dem
XFrame-Stichpunkt unter „Unveroeffentlicht -> Neu“; Anwenderhandbuch: ein
Satz in der Tabellenzeile „XFrame“ und ein Satz am Ende des Absatzes
„Dashboard > Widgets“.
Die Tests, und der Beleg dass sie rot waren
| Datei | Faelle | Rot-Lauf (vor der Umsetzung) |
|---|---|---|
xframe-crop.test.ts |
11 (neu) | pnpm --filter @tessera/web exec vitest run src/components/dashboard/widgets/xframe-config.test.ts src/components/dashboard/widgets/xframe-crop.test.ts src/components/dashboard/widgets/xframe-widget.test.tsx -> Error: Failed to resolve import "./xframe-crop" from "src/components/dashboard/widgets/xframe-crop.test.ts". Does the file exist? (0 test) |
xframe-config.test.ts |
21 (12 + 9) | derselbe Lauf -> 21 tests | 11 failed (Tests 1 und 8 mit erweitertem toEqual, Tests 13–21 neu; Fehlerstellen Zeilen 26/78/97/106/111/118/128/139/146/161/172) |
xframe-widget.test.tsx |
19 (12 + 7) | derselbe Lauf -> 19 tests | 6 failed (Tests 13, 14, 15, 16, 17, 19: Unable to find an element by: [data-testid="xframe-crop-clip"] bzw. xframe-readonly-overlay; Test 18 war schon gruen, weil er nur Abwesenheit prueft) — gesamt Test Files 3 failed (3), Tests 17 failed | 23 passed (40) |
xframe-config-form.test.tsx |
17 (8 + 9) | nach den Uebersetzungs-Schluesseln, vor dem Formular geschrieben: pnpm --filter @tessera/web exec vitest run src/components/settings/xframe-config-form.test.tsx -> Test Files 1 failed (1), Tests 17 failed (17) (der erweiterte renderForm-Helfer sucht die Labels cropEnable/readOnly, die das alte Formular nicht rendert — dadurch auch Tests 1–8 rot); erster Lauf nach der Umsetzung 17/17 |
Nach der Umsetzung: Resolver 21/21, Geometrie 11/11, Widget 19/19, Formular
17/17, src/messages 6/6, widget-settings-panel.test.tsx unveraendert
gruen (7 Dateien, 86 Faelle im <verify>-Lauf). Zusammen 36 neue Faelle;
Web-Tests 604 -> 640 (Plan: >= 634).
Messungen (Endstand, HEAD 30fdd99)
| Groesse | Ausgang (5aa577a) |
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 | 604 | 640 (81 Dateien) |
as unknown as in apps/api/src |
27 | 27 |
as unknown as in apps/web/src |
6 | 6 (davon 1 in test/setup.ts) |
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 angefassten Web-Dateien |
– | 0 / 0 / 0 |
de/en-Schluesselgleichheit widgets.xframe |
15 = 15 | 28 = 28 |
Keine neue any, kein !, kein neues Paket (T-GE2-SC nicht ausgeloest),
kein Prisma-/Schema-Pfad, keine Aenderung an Panel, Registry, Katalog oder
API.
Pruefliste fuer den Browser-Rundgang (Orchestrator, lokal, Playwright-MCP — NICHT Testserver)
- (a) Einstellungen -> Dashboard -> XFrame mit
https://example.com: „Nur einen Ausschnitt der Seite anzeigen“ anhaken -> genau EIN PATCH mitcrop: {x:0,y:0,w:1280,h:720}UNDreadOnly: true; Vorschau (420 px hoch, scrollbar) zeigt die Seite bei 1280 px Breite mit blauem Rahmen ueber der ganzen Breite; Zahlenfelder zeigen 0/0/1280/720; die Zoom-Auswahl ist verschwunden - (b) Rahmen mit der Maus verschieben -> folgt fluessig, beim Loslassen genau EIN PATCH, Zahlenfelder springen auf die neuen Werte
- (c) Ecke unten rechts ziehen -> Groesse aendert sich, obere linke Ecke bleibt, ein PATCH; Ecke oben links bis unter die Mindestbreite ziehen -> Rahmen bleibt 100 px breit, rechte Kante steht
- (d) Zahlenfeld Breite auf 2000 + Feld verlassen -> Feld zeigt 1280, Links springt auf 0; Hoehe 10 -> 60
- (e) Dashboard: die Kachel zeigt genau den Ausschnitt, eingepasst und zentriert (DOM:
xframe-crop-clip,data-tile-sizeam Koerper,<iframe>mittransform: scale(…));sandbox,referrerpolicy=no-referrer,allow=""unveraendert - (f) Kachel im Bearbeitungsmodus vergroessern/verkleinern -> nach dem Speichern skaliert der Ausschnitt mit und bleibt ganz sichtbar; Ziehen ueber dem Rahmen funktioniert weiterhin (
xframe-edit-overlay) - (g) „Nur anzeigen“ an: Klick und Mausrad im Rahmen bewirken nichts (DOM:
xframe-readonly-overlay), „In neuem Tab öffnen“ klickt weiterhin; aus: Seite bedienbar - (h) Ausschnitt abhaken -> PATCH
crop: null, Zoom-Auswahl erscheint; Zoom 60 % -> Kachel zeigt die Seite verkleinert (DOM:width: 166.67%,transform: scale(0.6)), 100 % -> keinstyle - (i) verweigernde Seite (
https://www.google.com) -> Vorschau und Kachel bleiben leer, Formular-Hinweise stehen; API-Log ohne Abruf der Fremdadresse
Rundgang durch den Orchestrator am 22.09.2026 (lokaler Stack, Abbilder aus cf70a19, Playwright-MCP): alle neun Punkte bestanden. Belege: (a) Anhaken → genau ein PATCH {crop:{0,0,1280,720}, readOnly:true}, Vorschau 403 px hoch mit Bildlaufleiste, Rahmen ueber die ganze Breite (Akzentfarbe gelb, nicht blau — die Kachelfarbe des Nutzers), Felder 0/0/1280/720, Zoom-Auswahl weg; (b) Verschieben folgt der Maus (200 px waehrend des Ziehens), beim Loslassen ein PATCH {x:194,y:97,…} (= 200/1,03), Felder springen mit; (c) Ecke unten rechts: obere linke Ecke bleibt (714/413), Groesse 620x310 → 739x389; Ecke oben links weit nach rechts: Breite bleibt 100 px (103 px Bildschirm), rechte Kante steht; (d) Breite 2000 → 1280 und Links → 0; Hoehe 10 → 60; (e) Kachel 933x601 zeigt genau den Ausschnitt 256/430/768/200 (xframe-crop-clip 933x243 bei top 179, <iframe> left -311px; top -522px; scale(1.21)), Sandbox/no-referrer/allow="" unveraendert; (f) Kachel auf 663x321 verkleinert → Ausschnitt skaliert auf 0,86 und bleibt ganz sichtbar, xframe-edit-overlay beim Ziehen da; (g) „Nur anzeigen“: Klick auf „Learn more“ im Rahmen bewirkt nichts (Frame bleibt example.com), xframe-readonly-overlay vorhanden, Link „In neuem Tab öffnen“ da; aus → Klick an der auf die Skalierung umgerechneten Stelle navigiert den Rahmen (Playwright selbst kann in einem per transform skalierten iframe nicht klicken — Werkzeuggrenze, per elementFromPoint + mouse.click umgangen); (h) Abhaken → PATCH {crop:null}, Zoom-Auswahl erscheint, 60 % → width: 166.67%; height: 166.67%; transform: scale(0.6); (i) google.com → Vorschau-Rahmen und Kachel leer, beide Hinweise stehen, API-Log ohne Treffer.
Ein Befund aus dem Rundgang, behoben in cf70a19: die Kachel nutzte als Layouthoehe des Rahmens max(crop.y + crop.h, 720), die Vorschau 3000 px. example.com setzt margin: 15vh — die Ueberschrift lag in der Vorschau bei y 450, in der Kachel mit 720 px Rahmen bei y 108; der in der Vorschau gewaehlte Ausschnitt haette in der Kachel etwas anderes gezeigt. Jetzt nutzt die Kachel dieselbe Layouthoehe wie die Vorschau (XFRAME_PREVIEW_PAGE_HEIGHT), damit vh-relative Seiten identisch umbrechen (Tests 4/13 angepasst). Zweitens ein 4-px-Querbalken in der Vorschau durch die Eckgriffe am rechten Rand → overflow-x-hidden. Web-Tests 640 unveraendert gruen.
Deviations from Plan
- [Rule 3 - Blocking] Biome
useSemanticElementsmeldete den Rahmen<div role="group">als neue Warnung (web 53 -> 54);biome-ignoreist verboten. Der Rahmen ist jetzt ein<fieldset aria-label=…>(implizite Gruppen-Rolle;getByRole('group', { name })findet ihn; Klassen zusaetzlichm-0 p-0 min-w-0gegen Browser-Vorgaben). Formular-Test 10 pruefttagName === 'FIELDSET'stattgetAttribute('role'). Die Pointer-Handler nehmenPointerEvent<HTMLElement>. Web-Lint wieder 53. Aufgabe 1, Commit445b1d3. - [Rule 3 - Blocking]
resolveCrop: die im Plan skizzierte Zuweisungconst record: Record<string, unknown> = rawkompiliert unterstrictnicht (objectohne Index-Signatur, TS2322). Statt eines Casts ein TypwaechterisRecord(raw): raw is Record<string, unknown>. Aufgabe 1, Commit445b1d3. - Zaehler-Falle: der Kopfkommentar des Test-Helfers enthielt die
Phrase „No
as unknown as“ und liess den grep-Zaehler auf 7 springen; umformuliert („no double cast throughunknown“). Kein Code betroffen. onPointerCancelprueft wie Move/Up den Zugmodus (drag.mode === mode), damit ein Abbruch am Griff nicht zusaetzlich den Rahmen-Handler durchlaeuft (Bubbling ohne Capture in jsdom). Verhalten im Test 14 unveraendert.- Commit-Nachricht von Aufgabe 1 einmal per
--amendkorrigiert (vor jedem weiteren Commit, nichts baute darauf): die Testzahl stand zunaechst als 638, gemessen sind 640. Der endgueltige Hash ist445b1d3. - Zwei Widget-Faelle mehr als die geforderten sechs (Test 18 und 19
getrennt), ein Resolver-Fall mehr (Test 21
clampXframeCrop).
Nicht geaendert: STATE.md, ROADMAP.md, .planning/** (ausser dieser
Akte), keine neue Abhaengigkeit, kein Deploy, kein Zugriff auf den
Testserver. Ein lokaler Docker-Stack wurde nicht angefasst.
Known Stubs
Keine. Kette verdrahtet: Checkbox -> onChange({ crop, readOnly }) ->
PATCH /dashboard/widgets/:id/config (flache Zusammenfuehrung, bestehend)
-> resolveXframeConfig -> crop !== null -> ResizeObserver ->
computeCropLayout -> Clip + <iframe style>; Rahmen pointerdown/move/up
-> applyCropDrag -> Entwurf -> clampXframeCrop -> EIN onChange;
Zahlenfelder -> clampXframeCrop -> onChange; zoom -> Prozentmasse +
scale; readOnly -> Flaeche nur im Ansichtsmodus.
Threat Flags
Keine neue Flaeche ausserhalb des <threat_model> des Plans: T-GE2-01
(beide Rahmen mit exakt XFRAME_SANDBOX, allow="", no-referrer —
Widget-Test 13/15, Formular-Test 10), T-GE2-03 (pointer-events: none der
Vorschau — Formular-Test 10), T-GE2-04 (keine API-Aenderung — Rundgang i),
T-GE2-05 (Klemmung — Resolver-Tests 13–17, Geometrie-Test 5), T-GE2-06
(nur Zahlen/Template-Strings in Styles, Texte aus Sprachdateien), T-GE2-02
und -07 im Dateikopf des Widgets bzw. Formulars ausdruecklich als
Bedienkomfort/Capture ohne Rechte benannt.
Self-Check: PASSED
Alle 3 neu angelegten Dateien liegen auf der Platte, die zwei Commits
445b1d3 und 30fdd99 sind in git log auffindbar
(git rev-list --count 5aa577a..HEAD = 2). Die Zahlen der Tabelle stammen
aus tatsaechlich gelaufenen Befehlen.