Commit Graph
8 Commits
Author SHA1 Message Date
tfaour fcf3aec4c0 Move button actions to per-widget config, add hold-for-global-action
Build and push server image / test (push) Successful in 36s
Firmware build check / build-check (push) Successful in 2m4s
Build and push server image / build-and-push (push) Successful in 3m12s
Build and push server image / deploy (push) Successful in 58s
Next/back button assignment moves from a frame-level "Button
assignments" card into each widget's own gear-icon dialog, prefilled
with a sane default at creation (photos/calendar -> advance/back,
whiteboard/weather -> check_now, others -> none). At most one binding
per (widget, button) now -- cross-widget execution order never
mattered since each widget's action only touches its own state.

New firmware capability: holding NEXT or BACK past a configurable
duration (min 3s, server-side default) triggers a frame-wide action
instead of the per-widget short-press one -- cycling saved layouts,
refreshing all widgets, or freezing/unfreezing every photo widget (see
app/global_actions.py). Firmware next/back checks gain the same
hold-duration polling the combo button already had; the threshold
comes from the previous wake's /frame/config fetch (persisted in NVS),
since this wake's button decision happens before that request.

Not done here: firmware/version.txt is intentionally left unbumped --
this hasn't been built or hardware-tested (no ESP-IDF toolchain in this
environment), so no firmware release build should be triggered yet.
2026-07-27 22:09:33 +00:00
tfaour eb7127718b Add battery widget (device's own last-reported level, no live upstream)
Build and push server image / test (push) Successful in 30s
Build and push server image / build-and-push (push) Successful in 2m4s
Build and push server image / deploy (push) Successful in 50s
Shows Frame.battery_percent/battery_as_of, already set by every device
wake-on-battery report, plus routers/common.py's existing
battery_estimate_s time-remaining estimate -- nothing new to fetch or
cache. Compact (icon + percent) or detailed (+ estimate, last report
age) display mode. No button actions.
2026-07-27 19:02:21 +00:00
tfaour 52ebafab78 Add standalone weather widget (current/hourly/daily/multi-city, pluggable providers)
Build and push server image / test (push) Successful in 1m11s
Build and push server image / build-and-push (push) Successful in 2m3s
Build and push server image / deploy (push) Successful in 52s
New widget type with four display modes -- current conditions, an
hourly forecast strip, a multi-day forecast, and several cities' current
day side by side -- backed by a pluggable provider registry (app/weather/,
mirroring the app/widgets/ dispatch pattern): Open-Meteo (worldwide) and
NWS (US-only) both wired up now, Environment Canada documented as the
next one to add given its more involved station/grid-lookup API.

The calendar widget's existing embedded weather strip is untouched and
still Open-Meteo-only; this lifts the same underlying icon-drawing
primitives (now shared via app/weather_render.py, calendar_render.py
still imports draw_weather_row unchanged) into a widget that can be
placed and sized on its own. Icons are redrawn in the panel's actual ink
colors (yellow sun/bolt, blue rain/snow) instead of flat black, and
build_multi_city's icon/font sizing now scales with how many cities need
to fit rather than the box's height alone -- both fixed after catching
them via live browser verification, along with a mode-switch cache-shape
crash and a mobile-width dialog overflow.

New WeatherWidgetConfig table (migration 24), grid footprint, widget
module, common.py fetch/cache helper, router endpoints (location set/
clear, city add/remove, preview), dialog template + JS, and full test
coverage (providers, widget render, HTTP endpoints, migration replay).
docs/widgets.md and CLAUDE.md's TODO updated accordingly.
2026-07-27 16:16:43 +00:00
Thomas Faour 3735c5bfa7 Add a text widget (rich text: bold/italic/underline, per-run color/highlight)
Build and push server image / test (push) Successful in 27s
Build and push server image / build-and-push (push) Successful in 1m59s
Build and push server image / deploy (push) Successful in 1m9s
A new self-contained widget type showing user-authored rich text -- no
live upstream to poll, like the static image widget, just word-wrapped
styled text instead of an uploaded image.

The dialog's contenteditable HTML is never stored or replayed as HTML:
app/text_content.py parses it server-side (on save) into a plain
paragraphs-of-styled-runs structure -- the actual sanitization
boundary, since raw HTML never round-trips back into any browser DOM
(the dialog rebuilds its editor from that same JSON via
createElement/textContent). app/widgets/text.py renders it with a
custom word-wrap/shrink-to-fit layout, using real vendored font weights
(app/fonts/NotoSans-{Regular,Bold,Italic,BoldItalic}.ttf, OFL-licensed
like the emoji fonts already there) rather than every other widget's
single ImageFont.load_default() -- the one widget type where that
distinction matters.
2026-07-25 14:21:52 +00:00
Thomas Faour 35e80c6d1c Add a static image widget (PNG/JPEG/GIF/BMP/WEBP/TIFF/PDF upload)
Build and push server image / test (push) Successful in 26s
Build and push server image / build-and-push (push) Successful in 2m1s
Build and push server image / deploy (push) Successful in 59s
A new widget type showing a single user-uploaded image with no live
upstream to poll -- decoded once at upload time (PDF's first page via
pypdfium2, BSD-3-Clause/Apache-2.0, no copyleft exposure) into plain RGB
PNG bytes, then composed per a crop/stretch/letterbox display mode like
the photos widget.
2026-07-25 08:39:09 +00:00
Thomas Faour b5c52004c8 Split the tasks feature out of the calendar widget into its own widget type
Build and push server image / test (push) Successful in 24s
Build and push server image / build-and-push (push) Successful in 2m1s
Build and push server image / deploy (push) Successful in 56s
Task lists used to be a week-view-only sub-feature bolted onto calendar
widgets (CalendarWidgetConfig.tasks_*), so a task list could only exist
tied to a calendar's view and only inside its footprint. Tasks are now
a standalone widget type (TaskWidgetConfig, app/widgets/tasks.py) that
can be placed and sized independently, same as photos/calendar/
whiteboard -- no separate "enabled" flag either, since being on the
grid at all is the on/off switch, matching every other widget type.

Migration 17 creates task_widget_configs, extracts any existing
calendar widget's configured task source into a new sibling tasks
widget (auto-placed in open grid space, source dropped+logged if truly
none is left), then drops calendar_widget_configs' now-dead tasks_*
columns in the same migration -- this project's usual same-migration-
drop convention. Also handles the rarer case of a database jumping
straight from before the widget system existed to after this split in
one boot, via the legacy Frame.calendar_tasks_* columns.

Verified live in the browser at desktop and mobile widths: adding a
Tasks widget, its own dialog (task-list source picker + preview), and
confirming the calendar widget's dialog no longer mentions tasks at
all. Full test suite (180 tests, including new coverage for the widget
render/actions, the migration's data-extraction path, and the
permission-boundary shape for tasks-source) passes.
2026-07-25 02:11:45 +00:00
tfaour 37bd657299 Widget system Phase 2: full cutover to widget-based rendering
Build and push server image / test (push) Successful in 49s
Build and push server image / build-and-push (push) Successful in 1m56s
device.py's mode-keyed dispatch is replaced by a real compositor:
load a frame's widgets, compute pixel rects via app/grid.py, render
each through its widget module, and composite with render_panel.
Physical NEXT/BACK buttons now execute each frame's assigned
FrameButtonAction rows instead of one hardcoded per-mode action.

api_frames.py, manage.py, and common.py's build_manage_content are
repointed to read/write the frame's widget config rows instead of
the old Frame columns, and every settings page (Photos/Calendar/
Whiteboard tabs) now pre-fills its form from the same widget config
the write endpoints actually save to -- previously the read and
write sides would have silently diverged. The old mode selector and
photo-inlay checkbox are removed along with their now-inert wiring;
arbitrary widget placement subsumes what the fixed inlay split did.

Ships together with Phase 1 (per-type render/action modules) since
splitting the read/write cutover across deploys would have left
settings changes with no visible effect.
2026-07-24 09:26:28 -04:00
tfaour f48daa71c8 Widget system Phase 1: per-type render/action modules
Build and push server image / test (push) Successful in 17s
Build and push server image / build-and-push (push) Successful in 2m2s
New app/widgets/ package (photos.py, calendar.py, whiteboard.py, plus
the WIDGET_TYPES registry) -- the widget-system analogue of
routers/device.py's old RENDERERS/ADVANCE_RENDERERS/BACK_RENDERERS,
generalized from "one mode owns the whole panel" to "each widget renders
into its own region and responds to named button actions." Each module
exposes render(db, frame, widget, target_w, target_h) -> Image.Image
(never raises -- a widget's own fetch hiccup falls back to a small
placeholder rather than taking the whole panel's render down) and an
ACTIONS registry for NEXT/BACK button assignment.

Supporting changes needed to give the widget modules something to call,
all mechanical/behavior-preserving for every existing caller:

- image_pipeline.py: render_panel(regions, ...) generalizes render_frame's
  tail (paste, enhance once, overlay once, quantize once, pack once) from
  one photo to N regions -- not a restructuring, since calendar mode's
  photo-inlay feature already pastes a second composed image onto the
  canvas before that single shared pipeline runs.
- photo_queue.py: advance_forced/back_forced/remove_from_rotation/
  get_current take an explicit `frame` param now that `cfg` won't always
  be the Frame itself once photo-queue state moves to PhotoWidgetConfig
  -- caught a real latent bug while doing this: get_current was reading
  refresh_interval_s off `cfg`, but that's a frame-level wake-cadence
  setting, not something that becomes per-widget, so it now reads that
  off `frame` explicitly instead.
- routers/common.py: list_assets/fetch_source_and_faces take album_id/
  display_mode directly instead of a whole Frame (both only ever read
  that one attribute off it); new get_or_refresh_*_for_widget siblings
  of the existing calendar/weather/tasks/whiteboard cache helpers, read/
  writing the new per-widget config tables -- the Frame-scoped originals
  are untouched and still what routers/device.py's actual dispatch calls
  until the Phase 2 cutover.

26 new tests (95 total): render_panel size/placement/orientation
coverage, and per-widget-type render/action tests (unconfigured ->
placeholder, a fetch failure -> placeholder not a crash, actions mutate
the right state). Full suite passes; diff-reviewed to confirm
device.py's actual RENDERERS dispatch and the old Frame-scoped
get_or_refresh_* bodies are unchanged, so this is safe to deploy on its
own despite being step 1 of a two-step cutover (see the project plan on
why the *next* step, not this one, has to ship atomically).
2026-07-24 08:44:53 -04:00