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>
This commit is contained in:
epaper-dashboard
2026-08-26 22:11:36 +04:00
co-authored by Hermes
parent af9a99a379
commit 1f142f5245
23 changed files with 6250 additions and 950 deletions
+25 -5
View File
@@ -15,7 +15,6 @@ from PIL import Image, ImageDraw, ImageFont
from palette import BG, FG, OK
from plugins.base import all_widget_classes, Widget
from layout import Item, GRID_COLS, GRID_ROWS, CELL_W, CELL_H, DISPLAY_W as LAYOUT_DISPLAY_W, pack, find_overlaps, find_out_of_bounds
import renderer
import importlib, pkgutil
import plugins as _plugins_pkg # ensure package is importable
@@ -208,8 +207,32 @@ def render_full(items: list, widgets: list, fonts: dict) -> Image.Image:
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), BG)
draw = ImageDraw.Draw(img)
# Grid-Trennlinien NUR im freien Hintergrund (zwischen leeren Zellen).
# Niemals durch Widgets durchgehen — Widgets sind klar abgegrenzt.
GRID_LINE = (215, 215, 215)
item_cells = set()
for item in items:
for dx in range(item.w):
for dy in range(item.h):
item_cells.add((item.x + dx, item.y + dy))
# Vertikale Linien: an jeder Spaltengrenze nur dort zeichnen, wo
# die LINKE und RECHTE Zelle beide LEER sind (kein Item).
for c in range(1, 4):
for r in range(4):
if (c - 1, r) not in item_cells and (c, r) not in item_cells:
y0 = r * 120
draw.line((c * 200, y0, c * 200, y0 + 120), fill=GRID_LINE, width=1)
# Horizontale Linien: analog.
for r in range(1, 4):
for c in range(4):
if (c, r - 1) not in item_cells and (c, r) not in item_cells:
x0 = c * 200
draw.line((x0, r * 120, x0 + 200, r * 120), fill=GRID_LINE, width=1)
for item, widget in zip(items, widgets):
px, py, pw, ph = item.pixels()
# Cell border
draw.rectangle((px, py, px + pw - 1, py + ph - 1), outline=FG, width=2)
if widget is None:
draw.text((px + 8, py + 8), f"#{item.id[:6]} (no plugin)",
font=fonts.get("16", fonts.get("default")), fill=FG)
@@ -261,8 +284,6 @@ class Dashboard:
self.items = items
self.widgets = widgets
self.config_mtime = CONFIG_PATH.stat().st_mtime if CONFIG_PATH.exists() else 0
self.display_theme = cfg.get("display_theme", "default")
self.display_design = cfg.get("display_design", "classic")
def maybe_reload_config(self):
if not CONFIG_PATH.exists():
@@ -290,8 +311,7 @@ class Dashboard:
with self._lock:
items = list(self.items)
widgets = list(self.widgets)
design = getattr(self, 'display_design', 'classic')
img = renderer.render_design(items, widgets, self.fonts, design=design)
img = render_full(items, widgets, self.fonts)
return img
def display(self, img: Image.Image):