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
+12
View File
@@ -215,6 +215,18 @@ document.getElementById('palette-reset').addEventListener('click', () => {
savePalette({ palette_reset: 'true', color_boost: '1', contrast_boost: '1', dither_strength: '1' });
});
// Fills the table with a community-measured starting point (see the
// card's own explanatory text) -- doesn't save by itself, same as
// editing the hex/RGB fields by hand; the user still clicks Save (or
// Reset) to commit or discard it.
document.getElementById('palette-load-calibrated').addEventListener('click', () => {
const inputs = paletteHexInputs();
window.CALIBRATED_SPECTRA6_HEX.forEach((hex, i) => {
inputs[i].value = hex;
syncPaletteFromHex(i);
});
});
// ---- Preview: current photo vs. how it renders with saved settings ----
// Scoped to this frame's photo widget (window.PHOTO_WIDGET_PREVIEW_API,
// set by the template) rather than window.FRAME_API -- palette/color/
@@ -14,6 +14,12 @@ function updateWeatherFieldVisibility() {
document.getElementById('weather-daily-days-row').style.display = mode === 'daily' ? '' : 'none';
document.getElementById('weather-location-section').style.display = mode === 'multi_city' ? 'none' : '';
document.getElementById('weather-cities-section').style.display = mode === 'multi_city' ? '' : 'none';
// Modern style is only built for current/daily (see app/html_render.py) --
// hourly/multi_city always render classic server-side regardless of this
// setting, so hide the row entirely rather than offer a choice that's a
// silent no-op.
document.getElementById('weather-render-style-row').style.display =
(mode === 'current' || mode === 'daily') ? '' : 'none';
}
function addWeatherWidgetCityRow(label) {
@@ -74,6 +80,7 @@ function initWeatherDialog() {
weather_units: document.getElementById('weather_units').value,
weather_hourly_interval_hours: document.getElementById('weather_hourly_interval_hours').value,
weather_daily_days: document.getElementById('weather_daily_days').value,
weather_render_style: document.getElementById('weather_render_style').value,
});
try {
const resp = await fetch(`${window.FRAME_API}/config`, {