Commit Graph
8 Commits
Author SHA1 Message Date
tfaour 27cd6b3703 Manual per-calendar color choice for calendar mode
Build and push server image / build-and-push (push) Successful in 49s
Each linked person can pin one of the panel's four non-black/white
colors (Yellow/Red/Blue/Green) to their own calendar instead of relying
on calendar_render.py's old auto-cycle-by-owner-name order -- owner-only,
like adding a calendar in the first place. Colors resolve against
whichever palette a frame actually renders with (including a custom
Advanced configuration override), so a pinned "Blue" stays this frame's
actual blue. Event color bars/dots are also bigger and rounded now
across agenda/week/month views, easier to tell apart at a glance.
2026-07-22 22:30:55 -04:00
tfaour acdb929a99 Move mode picker below device status bar; fix invisible calendar lines; add CalDAV calendar support
Build and push server image / build-and-push (push) Successful in 50s
Calendar rule/grid lines were light gray, which dithers away to
near-invisible on the 6-color e-ink palette -- now black.

CalDAV accounts (Nextcloud, Fastmail, iCloud, ...) can now be linked
alongside the existing single ICS subscription, since one account can
expose several calendars. A frame's Calendar tab now lists calendars
per person rather than one opt-in per person: your own row shows every
calendar you have available with a full add/remove toggle, while other
linked users' rows show only calendars they've included, toggleable
off (mute) but not on -- only a calendar's owner can add it to a
shared frame. FrameCalendar replaces the old single-boolean
UserFrame.calendar_included; existing opt-ins are migrated forward.
2026-07-22 22:01:05 -04:00
tfaour 3a0007118c Move frame name/mode into the page header; Calendar gets its own tab
Frame name (pencil-icon inline edit) and the Photos/Calendar mode
selector now live in the page header, shared across all four per-frame
pages instead of being buried in the Configuration form -- so renaming
a frame or flipping its mode no longer requires navigating to a
specific tab first.

Calendar settings move out of a conditionally-hidden card on the
Configuration tab into their own dedicated tab (new /frames/{id}/
calendar route), visually greyed out when the frame is in Photos mode
but still fully usable so calendar settings can be configured ahead of
switching modes. Also wires the week-start setting into the UI for the
first time (the column/backend support landed earlier but had no
control anywhere).
2026-07-22 21:21:00 -04:00
tfaour 60fcfca4a0 Make the device status card always visible, not just on Stats
Build and push server image / build-and-push (push) Successful in 39s
Moved out of the Stats tab's side column into a new horizontal bar
shared by every frame page (Photos/Configuration/Stats), sitting
between the page title and the tabs so it's on screen regardless of
which tab is active.

_device_status_bar.html is a new partial included via a device_status
block in app_base.html; device_status_bar.js is the fetch/render/poll
logic extracted from frame_stats.js and adapted to a wrapping row of
label/value pairs instead of a stacked list. frame_stats.html's Device
card and now-single-card .side-col are gone -- Battery history and
Lifetime stats just stack directly.
2026-07-22 10:46:05 -04:00
tfaour e48ac50ea1 Color/contrast/dithering sliders + before/after render preview
Advanced configuration gains three sliders (PIL ImageEnhance factors
for color/contrast, 0-2, 1=unchanged; a 0-1 dithering strength) applied
to every photo this frame renders. Confirmed the parameter conventions
against a similar project (jwchen119/EPF: ImageEnhance.Color/Contrast,
1.0 baseline) before implementing; dithering strength isn't natively
exposed by PIL's quantize(), so it's implemented by blending the source
toward its own flat/undithered quantization before running Floyd-
Steinberg on the blend -- at 0 there's no quantization error left to
diffuse (exactly the flat result), at 1 it's the original unmodified
behavior, with a smooth continuum between rather than dithering being
an on/off toggle.

image_pipeline.py split into composition (_compose), enhancement
(_enhance), quantization (_quantize), and transpose+pack stages so
render_frame (device bytes) and the new render_preview_png (a normal
viewable PNG, upright logical orientation) share the same pipeline
instead of duplicating it. Named-face overlay label math (face_labels.py)
was already routed through the shared _placement_transform, so it
needed no changes for the new params.

Also added the requested before/after comparison: the Configuration
tab's new Preview card shows the current photo's untouched Immich
preview next to that same photo run through the frame's actual saved
rendering pipeline (two new GET endpoints, /preview/original and
/preview/rendered) -- immediate visual feedback for tuning the palette
and these new sliders. "Refresh preview" re-fetches after saving.

Schema migration v6 adds color_boost/contrast_boost/dither_strength,
defaulting to 1.0/1.0/1.0 -- reproduces the exact previous rendering
until a frame's Configuration tab changes one.

Verified against the live-shaped test database: the migration, sliders
persisting and clamping out-of-range input, both preview endpoints
(real JPEG passthrough / real PNG at correct logical size+orientation),
confirmed dither_strength=0 actually changes the rendered bytes vs.
default, and the standing legacy-device curl suite.
2026-07-22 08:46:22 -04:00
tfaour e802882fc1 Palette calibration: precise hex/RGB inputs instead of a color picker
Build and push server image / build-and-push (push) Successful in 41s
A native <input type="color"> swatch can't be typed into precisely --
no way to enter an exact measured value. Replaced with a small table:
a read-only preview swatch, a hex text field, and three 0-255 number
fields (R/G/B) per ink color, kept in sync live in both directions
(editing hex updates R/G/B and the swatch; editing any of R/G/B updates
hex and the swatch). Hex stays the field actually read at save time --
the server-side validation (#rrggbb via hex_to_rgb) is unchanged, this
is a client-side-only swap of the input widget.
2026-07-22 01:26:03 -04:00
tfaour 5b11f2accb Per-frame palette calibration + sidebar battery indicator
Build and push server image / build-and-push (push) Successful in 40s
Advanced configuration (Configuration tab, collapsed <details> section):
a color picker per ink color (black/white/yellow/red/blue/green),
overriding image_pipeline.DEFAULT_PALETTE_RGB for that frame's actual
panel -- different units can vary enough from the documented
approximations to be worth calibrating once you can compare a rendered
photo against the real hardware. Stored as Frame.palette_rgb (NULL =
default, schema migration v4), threaded through render_frame/
render_placeholder/_quantize_and_pack (which now builds the PIL palette
image per call instead of once at import) so both photos and the
unclaimed/unconfigured placeholder screen respect it. "Reset to
defaults" clears back to NULL. Config-save validates exactly 6 #rrggbb
values, rejecting anything else with a 400.

Also: each frame's sidebar entry now shows its last-reported battery
percent (🔋NN%) next to the name, using the frame_dot's existing
recently-seen indicator conventions -- silent when never reported
(mains-only frames, or before the first report), matching how battery
is hidden everywhere else it's not applicable.

Verified against the same live-shaped database as the SMTP work: the
v3->v4 migration, save/reload/reset round trip through the real HTTP
route, an actual rendered image using a custom palette (confirmed via
its packed panel-code bytes), input validation, and the sidebar badge
against real battery data -- plus the standing legacy-device curl suite.
2026-07-22 01:19:06 -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