Konflikt in renderGrid() zwischen HEAD (BUG-01: kein HTML5-draggable,
touch-action:none) und BUG-03 (alter draggable=true). Resolution:
HEAD-Variante behalten — BUG-01 hat den HTML5-Drag komplett entfernt,
BUG-03 nur die Span-Geometrie gefixt. Kompatibel, kein Re-Konflikt.
Tests: 5/5 Python + 7/7 Pointer-Events + 8/8 Span-Geometry = alle grün.
BUG-01: Resize-Handle griff nicht — HTML5-draggable auf Items feuerte
dragstart bevor mousedown auf dem Handle ankam. Komplett raus, eigene
Pointer-Events für Move+Resize mit stopPropagation auf dem Resize-Handle.
BUG-02: Resize-Drag war zäh — renderGrid() pro mousemove. Jetzt rAF +
nur CSS-Geometrie-Update während Drag.
Tests: 7/7 grün.
Auto-Pack rief vorher layout_mod.pack() auf und schickte einen
simplen success-Toast. Wer aus Versehen klickte, hatte alle
manuellen Positionen verloren — kein Undo, kein Recovery.
Fix:
- packAll() speichert layoutItems als JSON-Snapshot in einer
local closure (vor dem pack-Call).
- Nach erfolgreichem Pack: toast() bekommt einen 4. Parameter
action={label, onClick}.
- Neues toast()-Feature: action-Button im Toast, clickbar trotz
dismiss-on-click. Klick ruft onClick und dismissed den Toast.
- Bei Undo-Klick: POST /api/layout mit den snapshot-Items →
Server restored → renderGrid() → 'Layout wiederhergestellt'.
- Modal-Text weist auf die 10s Undo-Möglichkeit hin.
Vorteile:
- Versehentlicher Klick auf Auto-Pack ist recoverable.
- Kein sessionStorage-Bloat, kein Multi-Tab-Konflikt
(Closure-Variable statt global Storage).
- Toast-Pattern ist jetzt generisch — andere Aktionen
(z.B. 'Snapshot wiederherstellen' aus Sidebar) können
denselben Mechanismus nutzen.
Beweis: tests/test_autopack_undo.js (8/8 grün)
1. Snapshot (JSON.stringify(layoutItems)) wird erstellt
2. Toast mit action={label:'Rückgängig'}
3. Undo ruft POST /api/layout mit snapshot-Items
4. Modal-Body erwähnt 'rückgängig'
5. CSS .toast-action Klasse vorhanden
6. toast() unterstützt 4-Args (action-Parameter)
7. Action-Button Click ruft onClick + dismiss
8. .toast-action hat pointer-events:auto
Live-Test: GET /?demo=1 → 200, 90847 bytes
toast-action: 6 occurrences (CSS + JS)
Rückgängig: 1 mention
JSON.stringify(layoutItems): 1
Closes#8
Initial-Render hatte keine Span-Geometrie:
renderGrid() hing Items in die Origin-Cell mit width:100% height:100%.
Ein 2x2-Item sah damit aus wie eine 1x1-Box mit Mini-Inhalt.
Resize-Code (in BUG-01/02-Branch, applySize) setzte zwar korrekt
grid-column/row per JS — aber nur WÄHREND Resize. Initial war's kaputt.
Fix:
- renderGrid() hängt Items jetzt direkt in den Grid-Container (cont),
nicht mehr in die Origin-Cell.
- style.gridColumn = '${it.x + 1} / span ${it.w}' setzt die CSS-Span-Geometrie
direkt im Inline-Style.
- Origin-Cell bekommt nur noch die 'occupied'-Klasse (für die Optik).
- DOM-Baum: Items sind Geschwister der Cells → keine DOM-Kollision mehr,
Drag-Events auf Nachbar-Cells werden nicht vom Item verschluckt.
Vorteile:
- 2x2-Item rendert visuell über 2x2 Cells (richtige Größe beim ersten Laden)
- Drag auf JEDE Zelle innerhalb der Item-Bbox funktioniert
- applySize (Resize) kann den Span nahtlos aktualisieren ohne DOM-Wechsel
- Kein Flicker beim Resize (initial state ist schon korrekt)
Beweis: tests/test_span_geometry.js (8/8 grün)
1. gridColumn wird per JS gesetzt
2. gridRow wird per JS gesetzt
3. Item wird in Container (cont) gehängt
4. Item wird NICHT mehr in Origin-Cell gehängt
5. Origin-Cell bekommt 'occupied' Klasse
6. gridColumn Format: <x+1> / span <w>
7. Keine width:100% im Item-CSS-Block
8. CSS-Kommentar erwähnt BUG-03
Hinweis: Mein ursprüngliches Issue-Statement war zu pessimistisch (Items
verdecken keine Nachbarzellen visuell). Sie saßen nur 1x1 in der
Origin-Cell. Dennoch ist der Fix substantiell: Initial-Render zeigt jetzt
korrekte Größe, und zukünftige Resize-Codes können sich auf den Span
verlassen ohne DOM-Mutation.
Closes#5
BUG-01: Resize-Handle griff nicht
Items hatten `draggable=true` (HTML5-DnD). Der Resize-Handle war Kind des
Items, also fired der Browser `dragstart` am Parent, sobald die Maus
überhaupt auf irgendeinem Kind war. `e.preventDefault()` im
`startResize()` mousedown-Handler kam zu spät — HTML5-DnD hatte den
Move-Drag bereits übernommen.
BUG-02: Resize-Drag war zäh
Pro mousemove rief der alte Code `renderGrid()` auf, das die komplette
Cell- und Item-Struktur neu aufbaute. Bei 60 events/s × 100+ DOM-Ops
pro Render entstand sichtbarer Jank.
Fix:
- HTML5-draggable komplett raus.
- Eigene Pointer-Events (`pointerdown`/`pointermove`/`pointerup`)
für Move (startItemPointer) und Resize (startResizePointer).
- Resize-Handle: `pointer-events:auto` auf Handle,
`pointer-events:none` auf `::before` (Dreieck-Deko), eigene
Listener, `stopPropagation()` damit Item-Handler nicht mitfeuert.
- `touch-action:none` auf Handle und Item-Body → Touch-Devices
scrollen nicht versehentlich beim Drag.
- Beide Aktionen nutzen requestAnimationFrame, und nur die
CSS-Geometrie (grid-column/grid-row) wird aktualisiert — kein
renderGrid() während Drag. Erst beim Drop / Resize-End kommt
der volle re-render + save.
- Resize nutzt `setPointerCapture` damit der Move auch dann
weiterläuft, wenn die Maus den Handle kurz verlässt.
Beweis: tests/test_pointer_events.js (Node-basiert, ohne Browser)
1. Resize stopPropagation wird aufgerufen
2. Item-Body pointerdown erreicht nur Item-Handler (nicht Resize)
3. CSS pointer-events korrekt gesetzt
4. renderGrid() in keiner onMove-Funktion (0/2)
5. requestAnimationFrame wird genutzt (3 calls)
6. draggable=true komplett entfernt (0 assignments)
7. touch-action:none auf Resize-Handle
ALLE 7 grün.
Manuelle Verifikation gegen laufenden Server (?demo=1):
GET /?demo=1 → 200, 91685 bytes
grep draggable=true: 0
grep startItemPointer|startResizePointer: 6
grep pointerdown: 3
grep requestAnimationFrame: 3
grep HTML5-drag handlers: 0 (nur 1 in Kommentar)
grep renderGrid(): 12, davon 0 in onMove-Handlern
Closes#3#4
Beim Hinzufügen eines neuen Widgets via /api/layout/add rief der Server
layout_mod.pack() auf alle Items auf — pack() sortiert nach Fläche
absteigend und platziert scan-line greedy. Ein 1x1 hello konnte dabei
einen 2x1 spotify aus seiner Position drängen, weil die Sort-Reihenfolge
sich ändert sobald ein neues Item im Mix ist.
Reproduktion vor dem Fix:
Bestehende config: c1@(0,0) w1@(2,0) st1@(0,2) sp1@(2,2) sv1@(2,3)
Add hello (1x1) → w1 wurde nach (0,2) verschoben, hello landete bei (2,0).
Siehe RED-Test in tests/test_add_route_no_repack.py.
Fix:
- layout.py: neue Funktion first_fit(item, others) — platziert ein Item in
der ersten freien scan-line-Zelle OHNE andere Items zu verändern.
- admin.py /api/layout/add nutzt first_fit. Wenn kein Platz: HTTP 409 mit
{ok:false, error:'kein Platz für WxH-Item', hint:'use_auto_pack'}.
- templates/index.html addItem(): 409 als 'warn'-Toast mit Auto-Pack-Hinweis.
Akzeptanzkriterien (BUG-04):
- bestehende Items bleiben bei Add unverändert an (x,y)
- neues Item landet in erster freier Zelle
- voller Grid → 409, kein bestehendes Item verschoben
- Auto-Pack bleibt als expliziter User-Wunsch erhalten (BUG-06)
Tests:
- tests/test_layout_firstfit.py: 3 unit tests (empty, gappy, full grid)
- tests/test_add_route_no_repack.py: 2 integration tests gegen /api/layout/add
mit gemocktem dashboard-Modul + Flask test_client
Closes#6