17 Commits
Author SHA1 Message Date
ki 94509e6836 Merge fix/BUG-05-save-indicator: Speichern-Button weg, Save-Status-Indikator 2026-08-29 17:50:41 +04:00
ki 5cf1d743eb Merge fix/BUG-06-autopack-undo: Auto-Pack Undo via Snapshot 2026-08-29 17:50:22 +04:00
ki 7885b100b4 Merge fix/BUG-03-span-geometry: CSS-Grid-Span-Geometrie im Initial-Render
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.
2026-08-29 17:50:07 +04:00
ki 67e1b465d5 Merge fix/BUG-01-02-resize-pointer: Pointer-Events + rAF
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.
2026-08-29 17:49:11 +04:00
ki d0cc69410f BUG-05: Speichern-Button weg, Save-Status-Indikator mit States
Der 'Speichern'-Button war redundant — debouncedSave() lief schon
bei jedem Drop/Resize/Add/Delete. Klick auf den Button machte nur
einen sofortigen POST statt 500ms-debounce. Verwirrend statt hilfreich.

Fix:
- Speichern-Button aus dem Toolbar entfernt.
- Stattdessen: <div id='saveIndicator' data-state='idle'> mit .save-dot
  und .save-label — rechtsbündig im Toolbar (margin-left:auto).
- States: idle | pending | saving | error (CSS-Data-Attr-basiert).
  - idle: grüner Punkt, 'Gespeichert'
  - pending: warn-gelber Punkt pulsiert, 'Ungespeichert…' (debouncedSave-Start)
  - saving: info-blauer Punkt pulsiert schneller, 'Speichern…' (saveLayout-Start)
  - error: alert-roter Punkt, 'Fehler: ...' (j.ok=false)
- setSaveState(state, label?) als zentraler Helper.
- saveLayout() macht jetzt success-toast weg — der Indicator reicht.
- Fehlerfall: error-toast bleibt (kritisch, separat sichtbar).

Vorteile:
- Toolbar ist aufgeräumter, +Button ist eindeutig der primäre Action.
- User sieht Save-Status ohne Klick auf irgendwas — visuelles Feedback
  für Auto-Save.
- Fehler-State zeigt sofort 'Fehler: ...' mit Details.

Beweis: tests/test_save_indicator.js (11/11 grün)
  1. Speichern-Button entfernt
  2. Save-Indicator #saveIndicator vorhanden
  3. setSaveState() Funktion definiert
  4. setSaveState setzt dataset.state
  5. debouncedSave setzt pending-Status
  6. saveLayout setzt saving am Anfang
  7. saveLayout setzt idle bei Erfolg
  8. saveLayout setzt error bei j.ok=false
  9. CSS für alle 4 Indicator-States
 10. Kein success-toast in saveLayout mehr
 11. Save-Indicator rechtsbündig (margin-left:auto)

Closes #7
2026-08-29 17:47:23 +04:00
ki a465912b55 BUG-06: Auto-Pack Undo via Snapshot + actionable Toast
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
2026-08-29 17:44:58 +04:00
ki 9f705ad7e4 BUG-03: Span-Geometrie im Initial-Render (grid-column/grid-row direkt setzen)
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
2026-08-29 17:42:49 +04:00
ki c2a44b80ea BUG-01+02: Resize via Pointer-Events + rAF (kein renderGrid-per-Move)
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
2026-08-29 17:31:40 +04:00
ki c6919c5104 BUG-04: Add-Item ohne Re-Pack (first-fit)
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
2026-08-29 17:20:22 +04:00
epaper-dashboard 5f74238e0e fix: grid items position:relative to restore multi-cell spans 2026-08-29 17:06:26 +04:00
epaper-dashboard 31d27ffa92 FEAT: 4x4 grid UI fixes, smooth drag-drop ghost preview, WordClock plugin
- Grid items now correctly placed inside .grid-cell divs (CSS Grid children)
- Visual occupied-state feedback during drag (CSS classes, no full re-render)
- Drag ghost preview (green=ok, red=overlap) replaces flicker on dragend
- New clock_wordclock plugin: German/English word clock (QWATCH layout)
2026-08-29 16:52:55 +04:00
epaper-dashboardandHermes 81e46a2c60 Cleanup: Helfer-Altlasten aus main entfernen
24 Files geloescht, die nicht zur aktiven Codebasis gehoeren:
- 3 konkurrierende Renderer (renderer.py/2/3.py) - wir nutzen nur dashboard.py
- 4 alternative Admin-Templates (design_a/b/c, preview_all) - nur index.html aktiv
- 9 alte Test-Skripte (test_gradient, test_grid_v2, etc.)
- 5 alte Boot-Skripte (start_admin.sh, install_*.sh, etc.) - durch systemd-units ersetzt
- 2 Misc (50-allow-networkmanager.rules, waveshare_epd_init.py, idx.html, socktest.py)

Aktive Codebasis (was bleibt):
- admin.py, dashboard.py, network_watchdog.py, palette.py, layout.py
- plugins/{base,clock,hello,system,weather,minimax,spotify,strava,gmail}.py
- templates/index.html
- config.example.json, .env.example, README.md, RECOVERY.md
- systemd service units + deploy_dnd.sh

Co-Authored-By: Hermes <noreply@hermes.local>
2026-08-26 22:13:47 +04:00
epaper-dashboardandHermes 1f142f5245 Sync to Pi: alle Features die live deployed sind
Aus dem Backup und Live-Pull vom Pi (10.11.3.144):
- dashboard.py: Grid-Linien nur im freien Hintergrund (nicht durch Widgets)
- templates/index.html: komplett redesigned mit Sidebar + Topbar + Toast + Modal
- plugins/clock.py: responsive Layout (1x1 bis 4x4)
- plugins/system.py, weather.py, minimax.py: mit Threshold-Bars und Color-Variants
- plugins/base.py: NEU — fetch_with_retry Helper (3x retry mit backoff)
  + render_error_banner für fehlgeschlagene API-Plugins
  (grosses rotes "!" Icon mit Plugin-Name und Fehler statt Crash)

Cleanup: Helfer-Chaos (renderer.py/2/3, design_a/b/c.html, clock_classic.py,
23x clock_*.png, alte test_*.py) wurde bereits im vorherigen Commit entfernt.

Co-Authored-By: Hermes <noreply@hermes.local>
2026-08-26 22:11:36 +04:00
ki 49de61bb26 Linear design system: new admin UI + display design switcher (8 designs)
Admin UI: Linear.app dark theme — near-black canvas, indigo-violet accent, Inter font
Display: Minimal, Minimal Color, Bold, Analog Clock, Magazine, Cards, Kiosk, Classic
2026-08-26 15:08:17 +04:00
ki a2c4158ba8 Add theme switchers: Admin UI (A/B/C) + Display (5 themes)
Admin UI: Dark Minimal / Retro Terminal / Warm Editorial via header buttons
Display: Classic White / Dark / Sepia / Nord / Terminal via Settings tab
Both switchers persist via localStorage / config.json
2026-08-26 14:41:34 +04:00
ki 99fb5692ef Add 3 alternative admin UI designs: Dark Minimal, Retro Terminal, Warm Editorial 2026-08-26 14:35:39 +04:00
ki 28124c5617 Initial commit: epaper-dashboard for 7.3" ACeP 7-Color display
- dashboard.py: plugin-based renderer with 4x4 grid layout
- admin.py: web UI with layout editor + plugin configs
- layout.py: pack algorithm, item placement, grid system
- plugins/: clock, weather, system, spotify, strava, gmail, minimax, hello
- network_watchdog.py: WiFi AP/client mode management
- waveshare_epd_init.py: vendor driver stub
2026-08-26 14:12:43 +04:00