Add experimental HTML/CSS "modern" render style for weather widget
Build and push server image / test (push) Successful in 39s
Build and push server image / build-and-push (push) Failing after 2m34s
Build and push server image / deploy (push) Has been skipped

The weather widget's icons/layout are hand-drawn PIL primitives -- clean
under quantization but flat, no gradients/shadows. Adds an opt-in
render_style="modern" (current/daily modes only) that instead renders a
Jinja2 template through a persistent headless-Chromium browser
(app/html_render.py), following the approach of Tesserae, an open-source
e-ink dashboard targeting this same panel family.

Key design points:
- The Chromium dependency (Playwright) is lazily imported only when a
  weather widget actually uses "modern" style, and the background browser
  itself only launches on first use -- every other widget type, and this
  one's own classic/hourly/multi_city paths, never pay for it.
- No Frame-level dithering setting needed: html_render dithers its own
  rendered widget to exact palette colors (Bayer/ordered, not
  Floyd-Steinberg) before compositing, so the shared whole-canvas
  Floyd-Steinberg pass sees zero quantization error there and leaves it
  untouched -- same trick draw_text/hand-drawn icons already use. Floyd-
  Steinberg keeps working unchanged for photos and every other widget.
- A "Load calibrated Spectra 6 preset" button in Advanced configuration
  offers a community-measured palette (data ported from
  paperlesspaper/epdoptimize, Apache 2.0) as an alternative starting
  point to the existing idealized DEFAULT_PALETTE_RGB -- fills the
  existing palette table, doesn't save by itself.

Known open risk, not resolved here: a headless Chromium binary is far
larger than the ~100MB single-layer limit that already forced this
project's pip/npm installs into split layers, and (unlike those) is a
single ~180MB file that can't be split across layers by ordinary
Dockerfile restructuring. Flagged prominently in server/Dockerfile and
docs/widgets.md -- treat this render style as experimental/local-only
until that's resolved.
This commit is contained in:
2026-07-30 22:18:43 +00:00
parent d34eb1bf45
commit 8ea1c53ec3
22 changed files with 717 additions and 22 deletions
+17
View File
@@ -798,6 +798,22 @@ def _migration_30(conn) -> None:
conn.execute(text("ALTER TABLE frames ADD COLUMN last_displayed_at REAL NOT NULL DEFAULT 0.0"))
def _migration_31(conn) -> None:
"""Weather widget render style (models.WeatherWidgetConfig.
render_style): "classic" (existing hand-drawn PIL renderer,
unchanged) or "modern" (app/html_render.py's headless-Chromium/CSS
renderer). Every existing weather widget defaults to "classic" --
no behavior change until a widget's dialog switches it.
Guarded per-column, same reasoning as migration 30's own comment:
weather_widget_configs is a table some replay tests may re-create
fresh via create_all() (which already has this column) rather than
replaying migration 24's raw CREATE TABLE."""
existing = {c["name"] for c in inspect(conn).get_columns("weather_widget_configs")}
if "render_style" not in existing:
conn.execute(text("ALTER TABLE weather_widget_configs ADD COLUMN render_style TEXT NOT NULL DEFAULT 'classic'"))
MIGRATIONS = [
(1, _migration_1),
(2, _migration_2),
@@ -829,6 +845,7 @@ MIGRATIONS = [
(28, _migration_28),
(29, _migration_29),
(30, _migration_30),
(31, _migration_31),
]