First stable release since 0.259.0. Home Assistant users on the stable add-on
channel should take this one: 0.277.1 fixed the community-template catalog,
the Share dialog, template install and the Home Assistant device picker under
ingress, all of which requested root-relative paths and so left the app for
the host root. Also here since 0.259.0: the Companion API grew Lineups
authoring, read and control with per-client permissions and a session
endpoint; dashboards gained data-change refreshes, placement-level refresh
cadences, and cell options that fill from another option; a GTFS departure
board, three TRMNL pixel fonts and BWRY palette calibration landed; and
CircuitPython clients can declare their panel rotation at registration. The
per-version sections below carry the detail.
Fixed
- Switching on the Template marketplace experiment while online features are
off no longer changes nothing without saying why. The community-templates
section on Browse was gated on the experiment and the master online
switch, so an install that enabled the experiment alone saw the page it
already had, with nothing anywhere to explain the omission. Community
widgets are served from a static index on GitHub and stay browsable with the
online switch off, which made the missing templates section read as a bug
rather than a setting. The section now renders on the experiment alone and,
when the online switch is off, says the templates come from api.tesserae.ink
and links straight to the switch. The Settings row for the flag carries the
same note.
[0.295.0], 2026-08-13
Fixed
- A daily or interval Lineup can be played from the Companion API. Records
that came from the schedule store carry no display of their own and fire at
whatever displays their dashboards are bound to, but the action endpoint
resolved targets from the Lineup's own binding, soplay,nextand
previousansweredinvalid_targetfor every one of them from v0.287.0.
Targets are now resolved the way the engine resolves them, and the same
resolved set is reported asresolved_device_idson the projection so a
client targets and labels from the server's answer rather than deriving it.
An explicitdevice_idssubset is bounded by the resolved set. - Adding an unknown dashboard to a Lineup by
PATCHis refused. Creation
already answered 404 for a dashboard id that doesn't exist; an edit built a
step pointing at nothing. Only ids being added are checked, so a member
dashboard deleted since the Lineup was built can still be edited out.
Added
GET /api/app/v1/sessionreturns the token id, the scopes the presented
credential carries now, and the Settings page where they're granted. An
optional scope can be granted or withdrawn long after pairing, and the
pairing response was the only place a client saw its scope list, so it had
no way to tell an ungranted permission from a granted one until a write
answered 403. Advertised as thesession_readcapability.
[0.294.3], 2026-08-12
Fixed
- An unchanged panel is no longer reported as a failed one. Playing a
Lineup step whose frame the panel already shows flashed "Couldn't play step
N: no_change, one or more panels failed to render/publish". Nothing had
failed:no_changemeans the rendered frame matched the panel's current
digest, so the publish was correctly skipped, which on e-ink is the point.
The multi-panel aggregate attached the render/publish error to every status
that wasn'tsent, and the Play button branched onsentalone. Fixed in
both places, plus the Fire now, schedule fire, and onboarding push routes,
which shared the check. - A rotation step no longer re-renders on every tick. Only
sentand
quietrecorded a step as fired, so a step whose content was stable never
recorded itself: each tick inside its dwell re-fired it and paid a full
render to rediscover the same digest, rather than rendering once per dwell.
Most visible on a single-step "keep this page fresh" rotation, where every
tick re-rendered.no_changenow counts as fired, matching the hold-clearing
check directly above it.
[0.294.1], 2026-08-12
Fixed
- A plugin's admin page no longer answers unauthenticated requests.
/plugins/<id>/matched the rule that exempts plugin assets
(/plugins/<id>/client.js) from the auth gate, because its trailing slash
left a non-empty first segment, which was all the check tested for. Every
plugin admin page was therefore reachable without a session from loopback,
contrary to the comment on the rule, which reserves the exemption for the
assets the in-process renderer fetches while composing. Both segments must
now be non-empty; renderer asset fetches are unaffected.
[0.294.0], 2026-08-12
Added
- GTFS / GTFS-RT departure board. A new
gtfswidget shows approaching
vehicles at a stop, from any agency's static GTFS zip plus optional realtime
feeds: live times, delays against the timetable, cancellations, service
alerts, and track numbers where the feed publishes them. Presets cover the
NYC Subway (one per realtime line group) and BART; anything else takes URLs.
A stop finder at/plugins/gtfs/searches a feed for thestop_ida cell
needs and reports which routes call there and what eachdirection_idmeans
in headsign terms, which GTFS itself never states. Two stops can share one
board, merged by time, and a large cell can split into two columns by
direction. Realtime protobuf is decoded directly, including the NYCT track
extension, so no protobuf dependency is added. - A cell option can be filled and locked by another option,
fill_from.
An option names the option that owns its value plus a value map; the editor
fills the field from the controlling option's current value and renders it
read-only, so it stays visible and stays in the POST. Suits presets that
supply a set of endpoints together, where mixing one preset's value with
another's is never valid.
Fixed
choices_fromdropdowns are no longer empty in the widget preview. The
preview read the raw manifest, so every dynamically-populated dropdown
rendered with no choices for every widget that uses one. It now materialises
options through the same helper the page editor uses.
[0.293.0], 2026-08-12
Added
- Placement-level daily widget refreshes. Widgets may declare
updates.on_scheduleso each Grid cell or Canvas element can independently
refresh at the server's local day boundary or an explicitly selected time.
Due refreshes coalesce per dashboard, respect quiet hours, update only
displays already showing the dashboard, and silently re-warm inactive Lineup
pages without selecting or advancing them. The feature is off by default.
[0.292.3], 2026-08-12
Fixed
- Saving a device no longer drops the MQTT session. Adding a device,
saving its card, editing a panel, and applying a calibration profile all
rebuild the transport, and none of them change how Tesserae reaches the
broker. Each one disconnected and redialled anyway, leaving a disconnect /
reconnect cycle in the broker's log that reads as a connection fault while
you are debugging one, and putting in-flight QoS 1 messages at risk. The
rebuild now keeps a live session whose resolved broker settings are
unchanged, and re-registers its callbacks rather than stacking a second set
per rebuild. A genuine broker change still redials. - The log no longer announces a connection the broker refused. "MQTT
connected" was written as soon as the socket opened, before the broker had
answered, and the CONNACK return code was discarded. A rejected login read
as a successful connect followed by a mysterious disconnect. The line now
comes from the broker's acceptance and carries the client id; a refusal
logs an error naming the broker and client id. - The MQTT DISCONNECT packet is now actually sent. Teardown stopped the
network loop before asking the client to disconnect, so the packet that
loop was responsible for writing never went out and the broker held the
session open until keepalive expired.
[0.292.2], 2026-08-11
Fixed
- Dynamic widget pickers no longer look configured when their saved value is
empty or unavailable. Gallery folders, Home Assistant entities, Todo lists,
and other runtime-backed selects now show the actual unset or removed value
until it is deliberately replaced, instead of letting the browser display
and later submit the first available option. - A select whose saved value and choices disagree only on type (a stored
"300"against a numeric300choice) now matches that choice instead of
rendering it twice, once as unavailable.
[0.292.1], 2026-08-11
Changed
- The vendored Companion contract is verbatim again. Six values Tesserae
serves ahead of the published contract had been edited straight into the
copy, which turned refreshing it from a copy into a merge (an overwrite would
have dropped them silently) and left the deviations as comments only findable
by whoever read that region. They now live in onecontract_errata.py, each
with the version that started serving it and why, layered over the spec at
load. A test asserts every entry is still missing from the vendored file, so
the first refresh that includes one fails and names the entry to delete
rather than letting the list rot.
[0.292.0], 2026-08-11
Added
-
Companion API: Lineups authoring (#206).
POST /api/app/v1/lineups
creates one from the same four intents the setup wizard offers, through the
same builder, so a Lineup made from the app and one made from the web are the
same record made the same way.PATCH /lineups/{id}edits one, gated on the
lineups:writepermission an operator grants per client.Three refusals carry the design. A Lineup using anything outside the four
intents is web-only, because a partial write from a client that can't see
those fields would flatten them. A field the app may not set is rejected
rather than ignored, so a client can't believe it wrote something it didn't.
And an edit must present theETagfrom the copy it read: the web editor is
very likely the other writer, and a stale write is refused with412rather
than applied. Every field a request doesn't mention keeps its stored value.Binding an unassigned dashboard to the Lineup's display is opt-in via
bind_unassigned_dashboards, so a dashboard already on another display is
never quietly moved.
Changed
- A valid credential that lacks a permission now answers
403, not401
(#207). 401 tells a client its credential is dead and to pair again, but an
optional permission is an operator toggle in Settings, so re-pairing lands in
exactly the same place. A missing or revoked credential is still401.
[0.291.1], 2026-08-11
Fixed
- A Lineup's "open in Tesserae" link pointed at a page that doesn't exist
(#203). The Companion projection spelled the path out as/decks/{id}, which
was never a route: the only GET a deck id has is its editor at
/decks/{id}/edit. Every such link the app advertised since v0.287.0 was a
404. The URL is resolved from the routing table now, and asserted against it,
so a future rename fails a test rather than shipping a dead link.
[0.291.0], 2026-08-11
Added
-
Lineups read carries everything an advanced Lineup holds (#203). The
projection gained the fields a Lineup can use but the four authoring intents
can't express: entry and home page, home timeout, refresh cadence, end time,
days of week, priority, smart sync and its lead, mode, minimum hold, the
time-of-day window, and the fallback dashboard, plus each dashboard's raw
refresh override, links and conditions. A client that must not edit such a
record can now describe it completely, which is what stops a partial update
flattening the parts it never knew about. Which legacy store a record was
migrated from stays private. -
Lineup step and play report their own job kind.
next,previousand
playreturnkind: lineup_actionrather than riding ondashboard_push, so
a client's activity view can name explicit Lineup control instead of
presenting it as an ordinary push. The History source stayscompanion.
[0.290.0], 2026-08-11
Fixed
-
A compressed RSS feed no longer reports itself as broken XML (#212).
urllib doesn't transparently decompress, and a CDN that gzips regardless of
what was asked for handed the parser a binary blob, which it reported as "not
well-formed (invalid token): line 1, column 0". That reads like a broken feed
rather than a compressed one, so the widget blamed the wrong thing. The
request now asks for no compression and decodes anyway if the server ignores
that.It presented as intermittent because the editor preview falls back to a
Chromium fetch, which decompresses natively, and its result is cached for ten
minutes. A dashboard therefore rendered correctly right after anyone opened
the editor, and showed the error once the cache expired. The same fallback
explains theTimeoutError: widget data fetch exceeded the page-render budgeton the panel preview: it was firing a whole browser fetch on every
preview because the plain fetch looked like a failure. -
The shared plugin HTTP helpers decode compressed responses too.
fetch_textandfetch_jsonhad the same gap, so any widget on a
CDN-fronted API could receive an undecodable body. The decoder is now one
implementation inapp.plugin_http, whichcalendar_core(where this class
of bug was first fixed, #168) now shares instead of keeping its own copy.
[0.289.2], 2026-08-10
Fixed
- A Lineup with the default hold no longer sticks on one dashboard (#167).
The minimum-hold guard measured from the moment of the last paint, and a
paint lands a few seconds into its window (the tick that noticed the
boundary), so the next boundary fell just inside the hold whenever the hold
equalled the dwell.advance_min_hold_minutesdefaults to 5, so a five
minute cycle, the obvious thing to build, swallowed every other window; with
two dashboards the swallowed window is always the same one, and the panel sat
on a single dashboard indefinitely while still firing on the grid at twice
the interval. The hold is now measured window to window for a scheduled
advance, so a hold equal to the dwell swallows nothing while a hold longer
than the dwell still slows the cycle as intended. A condition changing the
step inside one window is unchanged, still held from the last paint.
[0.289.1], 2026-08-10
Changed
- Panel dimension fields are labelled "Logical panel width / height", with a
line under them saying they're the composition orientation dashboards render
at, not the physical size the device reports. The old label read as the
hardware's own dimensions, which is exactly what they aren't on any device
whose mounted orientation differs from its framebuffer: a CircuitPython board
with a built-in display assumes a standard orientation, and the operator may
want another. Renamed across the device card, add-device, onboarding, the
app-level panel block, and the relay pairing form, along with the matching
validation messages, so the wording can't drift between them.
[0.289.0], 2026-08-10
Changed
-
One write path for creating a Lineup (#204). The setup wizard's four
buttons posted to three different routes backed by three different stores, so
what a record ended up being depended on which button made it. All four now
post the same shape to one create, which maps the authoring intent onto the
unified model. The scheduler already ran decks natively, so nothing about how
a Lineup fires changes; the per-store forms behind the Rotations and
Schedules pages are untouched, and existing records are unaffected.A dashboard picked in the wizard that isn't on any display yet now binds to
the Lineup's display for every intent, which the cycle and by-hand paths
already did. Without it a freshly-created Lineup can have steps that never
render.This is what native authoring in the companion app was waiting on (#206):
there is now somewhere forPOST /lineupsto land.
[0.288.0], 2026-08-10
Added
- Per-client permissions for the companion app (#207). Settings → Companion
lists what each paired app can do and offers the abilities that shouldn't
arrive by default. Today that's creating and editing Lineups: granting it is
a decision per client, takes effect on the next request, and can be withdrawn
the same way, all without the user re-pairing since the token doesn't change.
Fixed
- A gallery preview no longer eats a photo (#209). The sequential cursor
advanced on every render, and opening the dashboard editor, hovering a card
on the dashboards list, and probing the widget are all renders. Each one
consumed an image the panel then skipped. Only a render headed for a panel
moves the album on now; a preview shows the photo the panel will paint next. - A stored permission the server doesn't recognise is dropped on load
rather than kept. A scope check is a membership test, so a typo sat in the
credential file looking like a grant while granting nothing.
[0.287.0], 2026-08-10
Added
-
Companion API: Lineups read + control (#205).
GET /api/app/v1/lineups
and/lineups/{id}report each Lineup with its dashboards, which display is
on which page, and whether the app may edit it;
POST /lineups/{id}/actionsenables, disables, steps a display forward or
back, or plays a chosen dashboard. Steps read each display's own position, so
two panels on one Lineup move independently rather than being pulled to a
shared index. Repaints go through the existing job pipeline with an
Idempotency-Key; enable and disable answer immediately, since they touch no
panel.Editability is decided by the server, not the client: a Lineup using
conditions, a fallback dashboard, priority mode, smart sync, day or
time-of-day windows, or a home card reportsnative_editable: falsewith the
reason, and stays fully readable and controllable. That keeps a partial
client update from flattening fields it never knew about. Authoring itself
waits on #204.Pairing now grants
lineups:readandlineups:control. Authoring
(lineups:write) deliberately does not ride along, see #207.
Fixed
- The client protocol no longer claims re-registering changes a declared
rotation (#208). It doesn't:/registerfor an existing device id, and
/discovermatching a known MAC, both return the existing device without
rebuilding its panel. The Rotation dropdown owns the value after the instance
exists, and the page now says so.
[0.286.2], 2026-08-10
Fixed
- A device's Rotation control reads in the client's own terms (#200). The
dropdown labelled its degrees relative to landscape, so a client declaring a
720x1280 framebuffer atrotation: 0(paint my buffer as-is) read back as
90°, and correcting it to 0° transposed the dimensions, which then derived
90° again on the next save. The degrees are now the turn from the panel's own
framebuffer, so the card agrees with what the client declared. Panels whose
framebuffer is landscape, or that never declared one, are unaffected.
Changed
- A deck advance that can't paint says so (#167). When a page failed to
push, the advance was skipped silently and the panel kept showing the
previous page, so a dashboard that consistently fails to render looks like a
deck skipping it rather than a page that won't paint. The failure now records
a deck event and a log line naming the page and the error.
[0.286.1], 2026-08-10
Changed
- A catalog entry's own mark now replaces its card screenshot, rather than
only standing in where no screenshot exists. A widget that renders whatever
the user's own library holds screenshots as one arbitrary photo, which says
nothing about what the widget is; the service's logo is the thing people scan
the grid for. Entries without a mark are unchanged.
[0.286.0], 2026-08-10
Added
- Catalog entries can carry their own icon. A widget fronting a service with
a recognisable mark (Immich, a photo host, a transit agency) can ship that
mark in the catalog and have the Browse card render it in place of a Phosphor
glyph, which no glyph set can match for recognisability. The entry names a
bare filename resolved under the catalog'sicons/directory, never a URL, so
a catalog can't aim every Browse page's<img>at a host of its choosing; an
invalid value falls back to the glyph rather than failing the catalog parse.
The glyph stays the fallback for an install that can't reach the catalog, or
an image that fails to load.
[0.285.0], 2026-08-10
Added
- Clear a duplicate widget from the Widgets page. Plugins load from three
roots (bundled, thendata/authored/, thendata/marketplace/) and the first
to claim an id wins, so a widget pushed from Studio shadows a marketplace
install of the same id and the loser sits on disk erroring forever. The Loader
errors card now names the copy that is actually in use and offers to delete the
shadowed one, which previously needed shell access. The registry re-scans in
place, so no restart. Bundled copies are never removable, and a folder that is
one of several in a catalog bundle points at Browse instead of being removed on
its own.
[0.284.0], 2026-08-10
Added
- Any update cadence for a dashboard. The Updates control gains a 30 minute
preset and a custom… option with a minutes box, so a dashboard can be set
to any interval up to a day. Matching a battery panel's own wake interval
matters: a panel sleeping 30 minutes against a dashboard refreshing every 15
only ever downloads the newest render, so a sequential gallery appears to skip
every second picture.
Fixed
- A cadence outside the preset list renders as itself.
refresh_minutes
always accepted anything up to a day, but a value the dropdown didn't offer
had no matching option and the list drew it as "only when pushed", so opening
the dropdown and picking anything silently rewrote it. - All-day events no longer disappear from calendar_day/week/month/schedule
in the evening. Every calendar widget's shared event-fetching layer
compared an all-day event's date against the requested time window as if
both were full timestamps; once the UTC calendar day rolled past local
midnight — any evening in a timezone behind UTC, such as US timezones —
today's all-day events silently dropped out of the response. - calendar_day's all-day events span the full width of the event column.
They were laid out in a wrapping flex row, so each pill was only as wide as
its own title and stopped short of the column's right edge, reading as a
small tag instead of a bar covering the day it applies to.
[0.283.0], 2026-08-09
Added
- Clients can declare their panel rotation at registration (#200).
rotation
(0 / 90 / 180 / 270) is now accepted on/api/v1/device/discoverand
/api/v1/device/register. Sending it meanspanel_w/panel_hdescribe the
client's framebuffer and the rotation is the turn from that buffer to the
dashboard canvas, so a panel that autodetects as 1200x1920 can drive a
1920x1200 dashboard and still receive a 1200x1920 image. The buffer is stored
as the panel's native dims and echoed back asnative_w/native_hin the
/frameenvelope. Omitting the field keeps the previous reading, where the
reported dims are the canvas itself. - The CircuitPython renderers rotate onto a declared framebuffer.
circuitpython_pngandcircuitpython_bmpemitted at composition dims, which
left a rotated dashboard un-paintable on a client whose buffer is fixed the
other way. They now turn the composition onto the buffer before quantising,
the way the.binrenderers do. Only devices that declared a buffer are
affected; where the native dims were inferred from the preset table, output
keeps its existing shape.
Fixed
- A portrait client self-registering over REST no longer lands inconsistent.
The orientation fix in v0.280.0 covered the admin Register button but not
/api/v1/device/registeror the setup wizard's one-click register, so devices
paired through those paths still stored portrait dims against a landscape
orientation and had them swapped on the next save (#200).
[0.282.0], 2026-08-09
Added
- Palette calibration for 4-colour BWRY panels (#201).
bwry_4was the one
supported gamut with no palette-profile family, so the Calibration tab hid
itself on a PicPak and frames were always dithered against ideal sRGB
primaries the ink cannot reach: its yellow is a dark mustard, so error
diffusion spent yellow on highlights the panel never delivers. The gamut now
has a measured palette and two presets, Nominal BWRY (the default, which
renders identically to no profile, so nothing restyles itself) and PicPak
Calibrated. The palette editor drops the blue and green swatches for these
panels, and the palette-swatch test pattern stops painting two inks a BWRY
panel doesn't have. Calibration measured on a physical PicPak 4.2" panel by
varanu5; see NOTICES.md. - Switching on a PicPak's Calibrated palette + tone mapping toggle now
applies the measured palette. The toggle has been offered on these panels all
along and quietly did nothing, since there was no BWRY calibration data to
apply. It stays off by default.
[0.281.0], 2026-08-09
Fixed
- A dashboard preview is never served from a cache. Compositions went out
with no cache directives at all, which leaves an intermediary free to cache
them heuristically. Behind a caching reverse proxy the editor's preview could
therefore be a composition rendered by an older Tesserae, so anything the
template had gained since (the drag-to-swap overlay) simply wasn't there, with
nothing in the browser console to explain it. Compositions now go out
no-store, and the editor's preview frame is version-stamped so an upgrade
can't reuse a copy cached from the version before it. - Dragging a widget on the preview works with a finger. The drag surface had
notouch-action, so on a touchscreen the browser claimed the gesture for
scrolling and no drag ever started.
[0.280.0], 2026-08-08
Fixed
- A portrait panel's width and height stay where you put them (#200). A
client reports its panel dimensions but never an orientation, so a display
taller than it is wide was stored with portrait dimensions and its device
kind's landscape orientation. Nothing read the contradiction until the next
save of that card, which resolved it by rewriting the dimensions to match the
orientation: 1200×1920 became 1920×1200 after editing something unrelated like
the sleep interval, and typing the dimensions back swapped them again. The
orientation is now derived from the reported dimensions at registration, and a
save prefers whichever of the two the user actually changed, so a stored
mismatch is repaired on the orientation side instead. Moving the rotation
dropdown still drives the dimensions as before, and the 180° half of a
rotation is preserved either way. - Deleting a device with the wipe option no longer leaves its last frame
behind (#199). The wipe cleared dashboards, history, per-device settings and
the calibration image, but not the pointer to the frame most recently rendered
for that device. Renders are content-addressed, so the artifact was still on
disk too: registering the same device id again was handed the dashboard from
before the wipe rather than the 204 that means "nothing here yet". The wipe now
drops that pointer along with any pre-warmed deck or album frames for the
device.
[0.279.1], 2026-08-08
Changed
- Rearrange widgets by dragging them on the live preview. Swapping cells
shipped in 0.279.0 as a drag handle on the editor's form cards, which is not
where anyone looks to move something around a dashboard. The preview itself is
now the drag surface: press a cell, a label follows the pointer, the cell
underneath outlines, and releasing swaps the two widgets. The card handles
remain as a second route in for narrow screens where the preview is scaled
well down. None of this reaches a device render, it lives in the preview-only
overlay.
[0.279.0], 2026-08-08
Added
- Drag a cell onto another to swap the two widgets (discussion #198).
Grid cells carry absolute geometry, so a widget was welded to whatever box it
was first assigned to: rearranging a dashboard meant re-picking every widget
and re-entering its settings. Each cell card now has a drag handle in its
header; drop it on another card and the two widgets trade places, settings
and touch actions included, while both boxes stay exactly where they are. The
auto-managed status bar is pinned and doesn't take part.
Fixed
- Switching to a layout with fewer cells warns before dropping widgets.
Applying a smaller preset pairs each of its slots with a cell in order and
discards the surplus, taking their settings with them. The editor markup had
carried a flag for this warning since the feature shipped, with nothing
reading it, so the confirmation never appeared. It now names the widgets that
won't fit before anything is written.
[0.278.2], 2026-08-08
Changed
- Click a canvas dashboard's name to rename it. Rename already existed
behind the pencil in the switcher menu, but that menu is where you go to open
a different dashboard, so the name in the toolbar is what people reach for
first. It now opens the same rename prompt.
[0.278.1], 2026-08-08
Fixed
- Renders started from the UI show the dashboard, not the setup page, under
Home Assistant. Inside HA's Ingress tab every in-app URL carries HA's
/api/hassio_ingress/<token>prefix, and any render the browser asked for
passed that prefix through to the headless renderer. The renderer fetches
over loopback, which skips HA's proxy, so Tesserae saw the prefix as part of
the path: it matched no route, the loopback bypass that lets the renderer
read/compose/never applied, and what got screenshotted was the password
setup screen. A canvas Send, the panel preview, the render report, the touch
monitor and the template share preview were all affected; scheduled and
device-driven pushes were not, since they build their URLs frombase_url
rather than the request. Templates already submitted with a setup-screen
preview need resubmitting from an updated install.
[0.278.0], 2026-08-08
Fixed
- A rotation advances on the anchor grid the card predicts (#167). The
minimum-hold guard, which exists to stop a flapping condition flipping a
panel back and forth, was also gating advances that the clock had triggered,
and a held advance then fired the instant the hold lapsed. That off-grid fire
became the reference for the next hold, so with the default 5-minute hold and
a 5-minute dwell a single off-grid advance (a restart, an enable, a manual
play) re-paced the rotation permanently: it kept the right interval but ran
minutes behind the times the Lineups card showed under "next advance", and it
never recovered. A clock-driven advance is now held against the dwell
window's start rather than the moment of the last push, so a held advance
resumes on a later boundary and the rotation stays in phase with its anchor.
A condition changing the step inside one window is still held from the last
push, so an urgent step takes over as soon as the hold lapses.
[0.277.1], 2026-08-08
Fixed
- Sharing a community template works under Home Assistant ingress. The
Share dialog, the community-templates catalog, and template install all
requested root-relative paths, so under ingress (or any reverse proxy on a
subpath) they left the app and hit the host root instead. The reply was never
JSON, and Share reported "Couldn't prepare the share dialog; is the server
reachable?" on a perfectly reachable server. All of these URLs now carry the
script root, as the rest of the UI already did. - Share failures say what went wrong. A non-JSON reply is now reported with
its status code, and a redirect to the login page is named as a likely
expired session, instead of both being labelled as an unreachable server. - The Home Assistant device picker in Settings also requested an unprefixed
path, so its device list stayed empty under ingress.
[0.277.0], 2026-08-08
Fixed
- Toggling a dashboard's status bar keeps you in the editor (#197). The
switch posts a native form (it restructures the layout, so the page has to
reload), and the redirect afterwards followed theRefererheader. Behind a
proxy that strips it, Home Assistant's ingress among them, there was nothing
to follow and the browser landed on the dashboard list: the toggle had saved,
but the editing session looked thrown away. Every editor save route now
redirects to the dashboard it just changed. - The history widget shows the Title you gave it (#196). A single-entity
card printed the entity's friendly name in the heading and ignored the cell's
Title option; the option worked only when the card listed several entities. A
blank Title still falls back to the entity name. - The history chart's x-axis carries times instead of sample numbers
(#196). The axis was labelled with the ordinal of each plotted point (1, 10,
19, 26, …), which reads as data but says nothing about when a reading was
taken. It now labels in the app's timezone at a resolution the window can
carry: clock time within a day, weekday plus clock time up to three days, and
the date beyond that. Labels are spaced to the width available, so a wide date
label thins the axis rather than colliding.
[0.276.0], 2026-08-08
Fixed
- A canvas code element no longer picks up an icon stylesheet it never
referenced. The sandbox inlines a vendored library when the element's own
html/css/js mentions it, and the Phosphor regular-weight test matched any bare
phtoken, so a custom property named--phpulled the whole icon font in.
The match now requires the class pair the stylesheet actually defines
(class="ph ph-heart"), and custom-property names are excluded from library
matching altogether. - Injected library CSS can no longer consume an element's first authored
rule. The vendored bundles were joined with a;separator, which is right
for JavaScript and a parse error at the top level of a stylesheet, and the
result was prepended to the element's own CSS inside a single<style>. The
stray token swallowed whichever rule followed, which was always the element's
first: typically a:rootblock, so every variable it declared resolved to
empty while the rest of the sheet applied normally. Injected CSS now carries
no separator and renders in its own<style>block, so a malformed injection
stops at its own stylesheet.
Added
render_reportnames the libraries a code element was given.
injected_libsrides along withoutdebug=1and lists what each sandbox
inlined, each entry carryinginferredand thematchedtoken behind the
choice, so a stylesheet nobody asked for is visible in the report.autolibs: falseon a code element. Opts out of auto-injection entirely,
vendored libraries and bundled fonts alike, for an element that hand-authors
its own markup and wants no ambient styling.?debug=1diagnostics cover the composed stylesheet. Each code sandbox
self-reports any rule missing from the sheet it actually parsed, tagged
authoredorlibrary, so a dropped rule is named instead of pixel-hunted.
[0.275.0], 2026-08-07
Added
- Three pixel fonts: TRMNL12, TRMNL16 and TRMNL21. Core fonts
(fonts_core0.6.0) gains a three-family pixel set drawn by Heavyweight
Digital Type Foundry, Regular and Bold in each, bringing the bundled set to
39 families. Each family is drawn on a pixel grid at its named height, so set
it at 12px, 16px or 21px, or an integer multiple, to keep the strokes on
whole pixels; off-grid sizes resample and soften, which shows up badly once a
panel quantises to 1-bit. Available anywhere the other bundled fonts are,
including by family name inside a code element. Vendored as woff2 under the
SIL Open Font License 1.1, with the licence text alongside each family in
plugins/fonts_core/static/.
[0.274.2], 2026-08-07
Fixed
- The top navigation highlights the section you're in on every settings
page. Firmware, Cloud relay and the new Companion app page aren't
settings_arearoutes, and the nav matched an explicit list of endpoints,
so on those three pages nothing was highlighted at all: the current section
rendered in ordinary body text rather than the accent. The nav now matches
the settings area by path, so a page added later can't drop out of it.
[0.274.1], 2026-08-07
Fixed
- "Return home after" is offered only for lineups that wait for a person.
An auto-advancing lineup reclaims its own panel at the next boundary, so a
return-home timeout shorter than the advance interval parked the panel on
the home page in between, which reads as the lineup having stopped cycling.
The control is now hidden, and disabled so nothing stale is submitted, when
the advance mode is Timer or Both. Existing lineups keep their stored value
until the next save through the editor.
[0.274.0], 2026-08-07
Fixed
- A lineup set to advance automatically no longer reverts to "By hand".
Two separate faults produced that symptom. The management and graph forms
rebuilt the deck from only their own fields, so any save through them reset
every advance setting to its default, turning a timer or both deck back into
a manual one on disk. Those forms now overlay onto the stored deck, which
also means a field added later can't be silently dropped. Separately, the
Lineups card for a navigable deck was hardcoded to the "By hand" badge, so
even a correctly-saved both deck advertised itself as manual; it now shows
the cadence it runs on. Re-saving an interval or daily lineup through the
deck editor also no longer converts it to a cycle. - Widget copy points at Settings → Widgets. Error messages and help text
across the Home Assistant widgets, the gallery, the marketplace setting and
a touch hint still directed people to a "Plugins" section, which was renamed
to Widgets. - A blocked RSS feed explains itself. A feed answering with a challenge or
error page surfaced the raw parser complaint ("not well-formed (invalid
token): line 1, column 0"), and a block page that happened to be valid XML
slipped through to render as an empty widget. Both now report that the feed
returned a web page rather than XML.
[0.273.0], 2026-08-07
Added
- RSS headlines can show an article preview (discussion #194). A new
Article preview option on the RSS widget prints one to three lines of each
item's summary under its headline; off by default, so existing dashboards
are unchanged. The summary comes from the feed itself (descriptionon
RSS,summaryfalling back tocontenton Atom), so no extra requests are
made and no article pages are fetched. Markup is stripped and entities
decoded server-side, the text is capped so a full-text feed can't ship
whole articles into every render, and a feed whose description is a link
dump rather than prose shows no excerpt instead of a row of bare URLs.
[0.272.2], 2026-08-07
Fixed
- The Lineups editor's Save button is reachable on a phone (#192). The
sticky action bar laid its status text and buttons out in a single
non-wrapping row, so on a narrow screen the buttons were pushed past the
right edge and Save could not be tapped at all; the longer the deck's page
chain, the further out they went. The bar now wraps, and the status gives
up space before the buttons do. Desktop layout is unchanged.
[0.272.0], 2026-08-07
Fixed
- Lineups screen cards show the frame the panel was actually sent. The
cards re-rendered the dashboard instead of reading what had been pushed, so
a dashboard whose output moves on its own (a fractal, a clock) showed
something the panel had never displayed. They now serve the pushed
composition, the same source the History page reads, falling back to a
render only for a dashboard that has never been pushed. A dashboard edited
since its last push keeps showing the old frame, which is what its screen
is still displaying. - Preview thumbnails render in the configured timezone. The preview
renderer runs off the request thread and never received the timezone a push
sets, so Chromium fell back to the container clock and clock widgets in a
thumbnail rendered in UTC. The zone is now resolved on the request thread
and passed through, covering the dashboards list, Lineups, the panel-view
preview and the Companion app's dashboard preview.
[0.271.0], 2026-08-06
Added
seeed_reterminal_e1001_gray_legacyhardware kind. The reTerminal
E1001 ships with two glass variants: one carries a built-in 4-gray
waveform, the other needs register LUTs uploaded instead. They need
different firmware images but are identical on the wire, down to the
96000-byte frame, so the new kind exists purely to give the second image
its own identity and OTA lineage. Rendering is unchanged.auto_selecton hardware manifests. A SKU can opt out of being
inferred from a device's self-report. Relay pairing resolves the most
specific catalog kind from the reported protocol + gamut, which is
ambiguous between the two E1001 grayscale variants because their reports
are identical; the legacy variant now sits out that resolution, so an
ambiguous report deterministically pairs as
seeed_reterminal_e1001_gray. The variant is only ever set by the
operator, or by the firmware declaring it at register / discover.
[0.270.0], 2026-08-06
Added
- Settings → Companion app is its own page (#186). Issuing pairing codes,
watching pending codes, and disconnecting paired clients moved out of the
Devices page onto a dedicated tab, so the app surface has room to grow
without crowding the device list. The page also links the public TestFlight
beta, with a QR beside it so a phone can install without typing the URL.
The admin routes moved with it, from/settings/devices/companion/*to
/settings/companion/*.
[0.269.0], 2026-08-06
Fixed
- The Webpage widget no longer crops direct image URLs. A URL that
resolves to an image (PNG, GIF, JPEG, WebP, SVG) is now drawn as an image
and framed to the cell, so a chart taller than its cell keeps its bottom
edge instead of being clipped by the embedded viewport. Pages are
unaffected and still render through the iframe. A new Image fit option
offers the same five modes as the Send tab (fit, fill, stretch, center,
center with a blurred background); Scale continues to apply to pages.
[0.267.0], 2026-08-05
Added
/discoverannounces can suggest a display name (discussion #24).
The Register card in Settings → Devices gains a Display name field
prefilled from the announce's optionalname; the admin can edit or
clear it before registering, and later announces never rename an
already-registered device. The client protocol doc now spells out the
attribute's behaviour on both/discoverand/register.
Added
- Configurable text sizing for calendar_day, calendar_week, and
calendar_month. Per-text-type scale sliders (title, header, axis
label, event title/location/time, 0.01–10.0×), adate_label_style
choice with a full-word option, and ashow_locationtoggle for
the widgets that lacked one. calendar_day also gets configurable
day_start_hour/day_end_hourand ashow_densitytoggle;
calendar_week gets arange_modeoption so its 7-day window can
either stay anchored to the chosen week-start day or roll from
today (rolling_7), and correctly labels/shades each day by its
actual weekday rather than its column position in either mode.
Fixed
- Multi-day events now display correctly in calendar_day and
calendar_week. A multi-day all-day event (e.g. a 3-day conference)
previously appeared only on its first day, or as a separately-labeled
pill repeated on each day instead of one bar spanning them. A
multi-day timed event (an overnight block, a multi-day trip) is a
more common case and had it worse: it vanished entirely after its
first day, could render at the wrong time or run past midnight on
later days, and could overflow the visible timetable when
day_start_hour/day_end_hournarrowed it. All-day pills in both
widgets could also draw shifted left of their true day column. Every
day an event covers now renders correctly clamped to that day. - Personal-data snapshots are isolated per paired Companion installation.
Independently identified household phones can publish Apple Reminders
without replacing each other, each phone sees only its own sync status, and
disabling sync deletes only the publication associated with its pairing.
Existing single-publisher data remains readable and is replaced by the first
authenticated publisher that next syncs that source.
[0.266.0], 2026-08-05
Fixed
- Play and Fire-now push through a button/touch hold. Clicking a
step used to fail with a bare "quiet" message when every panel bound
to the page was still inside a manual page-away hold, even with
quiet hours disabled. Explicit clicks now fire anyway and clear the
holds on the pushed panels (so the rejoin pass doesn't later yank
them off the page), while the timer-driven skip is reported as
heldwith the reason "all devices manually held" in the flash,
status pill, and events log.
[0.265.1], 2026-08-05
Fixed
- Hardware verification records caught up with the bench. The
reTerminal E1001 (mono and 4-level grayscale), XIAO 13.3" ePaper
EE02, TRMNL 7.5" OG DIY Kit, both Inky Impression 4" variants, the
Waveshare 13.3" Spectra 6, and the 7.3" PhotoPainter are now
recorded as confirmed on real hardware in their SKU manifests, the
test matrix, the README tables, and the quickstart banners. The
docs landing page counts thirteen confirmed panels (plus the
community-confirmed PicPak) and the architecture doc's stale
"58 widgets" claim is corrected to the actual 35.
[0.265.0], 2026-08-05
Added
- The setup wizard scopes rotations and decks to a display (#167
feedback). The multi-pick steps gain a Display select; only
dashboards bound to that display (or not yet on any display) are
offered, the created rotation or deck is bound to it, and picked
dashboards without a display adopt it, so the whole set plays on one
panel. Rotations created through/rotations/newaccept
device_idsaccordingly. - Lineups warns about delivery gaps. A card whose dashboards are
bound to different displays (so no single panel plays the set), or
that is bound to a display some member dashboard can't render on,
now says so on the row instead of failing silently.
Fixed
- The Lineups page updates itself. Playing badges, current step,
next-advance times, group headers and thumbnails now refresh when
the scheduler fires (via the events stream) and on a slow fallback
poll; previously everything was frozen at page load. - Screen-card thumbnails no longer freeze at their first render.
Preview images opt into a freshness window (?refresh=<s>on
/compose/<id>/preview.png): a stale cached image is served
immediately while a re-render is queued behind it, so dashboards
whose data moves (clocks, feeds) catch up. Thumbnails that aren't
rendered yet also retry with backoff instead of staying blank.
[0.264.1], 2026-08-05
Fixed
- Hardware claims reconciled with the bench matrix. The README's
supported-hardware tables now use ✅ only for panels confirmed on real
hardware (the reTerminal E1001, both XIAO panels, and the TRMNL rows
move to TBD pending hardware reports), gain an Xteink X-series
section, and correct the Inky Impression 4" variants (600×400
Spectra 6 vs the legacy 640×400 7-colour ACeP). The reTerminal
E1002/E1003/E1004 manifests and the test matrix now record their
real-hardware confirmations, the E1003 including GT911 touch. - The widget gallery shows real stability tiers again. The gallery
generator reads tiers fromdocs/widgets/tiers.md; it was still
parsing a README section that moved there, so most cards rendered
"Tier: -". app/online.py's docstring matches the code again: online
features are opt-in and off by default (the stale docstring said the
opposite; the behaviour never changed).
[0.264.0], 2026-08-05
Added
- Pages can refresh when their data actually changes (#188).
Accepted semantic data changes are quietly debounced for 10 seconds,
then active pages with the policy enabled refresh through the
existing quiet-hours-aware push path while inactive Deck pages are
re-warmed without being promoted. TTL-only republishes do not
refresh, and personal-data PUT/DELETE responses remain independent
from background render or delivery outcomes.
Changed
- Smart sync is a top-level switch on the deck card, and the deck
editor's timing and conditions moved into a tuning card. Deck
conditions also surface on Lineups rows and in the setup wizard.
Docs
- Xteink X3 confirmed on real hardware (#187).
[0.260.0], 2026-08-04
Added
- Widgets can declare opt-in data-change updates (#185). A widget
manifest may expose strictupdates.on_changesource declarations,
optionally narrowed by a declaredselector_option. Grid cells and
Canvas widget elements persist an independentupdate_on_change
policy (off by default), and their editors surface the switch only
for capable widgets.