Commit Graph
7 Commits
Author SHA1 Message Date
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 8b9f636cce Mark whiteboard as (alpha); add the Phase 5 button-assignment UI
Build and push server image / test (push) Successful in 22s
Build and push server image / build-and-push (push) Successful in 1m59s
Build and push server image / deploy (push) Successful in 52s
Whiteboard rendering isn't fully reliable yet -- tag it (alpha)
everywhere it's user-facing (widget label, add-widget button, dialog
title, Settings' WebDAV section) via one shared WIDGET_LABELS map
(moved to common.js so both the Layout canvas and the new Configuration
tab section can use it).

Button assignments: a new "Button assignments" card on the
Configuration tab lets you assign an ordered list of (widget, action)
bindings to each physical NEXT/BACK button -- add/remove/reorder, all
autosaved. Backed by new GET/PUT /api/frames/{id}/buttons endpoints;
PUT replaces a button's whole list in one atomic, fully-validated call
rather than separate add/remove/reorder endpoints. Device-side
consumption already existed (routers/device.py's _run_button_actions);
this is the UI for what was previously only reachable via the default
migration mapping.
2026-07-24 17:13:51 -04:00
tfaour 569bf733e9 Show save/error status inside the open widget dialog, not behind it
Build and push server image / test (push) Successful in 21s
Build and push server image / build-and-push (push) Successful in 2m0s
Build and push server image / deploy (push) Successful in 54s
showStatus() always wrote to the page-level #result div, which sits
behind a <dialog>'s backdrop -- a save inside a widget's gear-icon
dialog produced a message the user couldn't see without closing the
dialog first. It now prefers a .dialog-result element inside whichever
<dialog> is currently open, falling back to #result everywhere else.

The dialog is a flex column with its body scrolling independently so
.dialog-result stays pinned as a visible footer regardless of scroll
position -- otherwise a save message on a long form (e.g. the calendar
dialog) could land off-screen below the fold with no visible feedback
at all.
2026-07-24 14:48:50 -04:00
tfaour 8ac3fc0de3 Redesign phase D: sidebar app shell, per-frame tabs, namespaced API
Build and push server image / build-and-push (push) Successful in 43s
The web UI grows into the multi-frame world: a left sidebar lists the
user's frames (with an online dot driven by the same overdue math as
the Device panel; collapsible off-canvas with a hamburger on mobile),
and each frame gets three tabs -- Photos (album picker, now displaying,
the drag-to-reorder upcoming grid), Configuration (name/order/
orientation/refresh/quiet hours/timezone/smart crop + the firmware
card), and Stats (device telemetry, lifetime counters, battery chart).
Settings and Admin adopt the same shell. / becomes a routing hub:
first frame, empty-state onboarding page, setup/login, or the
manage-QR redirect.

The JSON API moves to /api/frames/{id}/... behind require_frame_view /
require_frame_control: any linked user (admins see all) can view; 404
for frames outside your view so ids aren't confirmed; mutations 409
with the holder's name unless you hold the soft control lock, and
POST take-control always flips it to you. Config saves are now partial
updates -- each tab posts only its own fields (checkboxes always sent
explicitly), so the split forms can't clobber each other.

All CSS moves to static/theme.css and the old 680-line inline script
block splits into static/*.js -- the Pointer Events drag-drop state
machine and the canvas battery chart ported intact, not rewritten. The
CSRF fetch wrapper now reads a <meta> tag. No build step, still vanilla.

Verified end-to-end: page/static/API suites, control-lock handoff in
both directions, partial-save field preservation, non-admin frame
isolation, and the legacy-device curl suite (still byte-identical
responses for the deployed frame).
2026-07-21 23:56:18 -04:00