Skip to content

v0.296.0

Latest

Choose a tag to compare

@github-actions github-actions released this 13 Aug 08:57
· 1 commit to main since this release

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, so play, next and
    previous answered invalid_target for 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 as resolved_device_ids on the projection so a
    client targets and labels from the server's answer rather than deriving it.
    An explicit device_ids subset is bounded by the resolved set.
  • Adding an unknown dashboard to a Lineup by PATCH is 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/session returns 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 the session_read capability.

[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_change means 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't sent, and the Play button branched on sent alone. 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 sent and
    quiet recorded 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_change now 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 gtfs widget 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 the stop_id a cell
    needs and reports which routes call there and what each direction_id means
    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_from dropdowns 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_schedule so 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 numeric 300 choice) 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 one contract_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:write permission 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 the ETag from the copy it read: the web editor is
    very likely the other writer, and a stale write is refused with 412 rather
    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, not 401
    (#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 still 401.

[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, previous and
    play return kind: lineup_action rather than riding on dashboard_push, so
    a client's activity view can name explicit Lineup control instead of
    presenting it as an ordinary push. The History source stays companion.

[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 the TimeoutError: widget data fetch exceeded the page-render budget on 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_text and fetch_json had the same gap, so any widget on a
    CDN-fronted API could receive an undecodable body. The decoder is now one
    implementation in app.plugin_http, which calendar_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_minutes defaults 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 for POST /lineups to 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}/actions enables, 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 reports native_editable: false with 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:read and lineups: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: /register for an existing device id, and
    /discover matching 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 at rotation: 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's icons/ 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, then data/authored/, then data/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/discover and
    /api/v1/device/register. Sending it means panel_w / panel_h describe 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 as native_w / native_h in the
    /frame envelope. Omitting the field keeps the previous reading, where the
    reported dims are the canvas itself.
  • The CircuitPython renderers rotate onto a declared framebuffer.
    circuitpython_png and circuitpython_bmp emitted 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 .bin renderers 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/register or 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_4 was 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
    no touch-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 from base_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 the Referer header. 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
    ph token, so a custom property named --ph pulled 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 :root block, 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_report names the libraries a code element was given.
    injected_libs rides along without debug=1 and lists what each sandbox
    inlined, each entry carrying inferred and the matched token behind the
    choice, so a stylesheet nobody asked for is visible in the report.
  • autolibs: false on 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=1 diagnostics cover the composed stylesheet. Each code sandbox
    self-reports any rule missing from the sheet it actually parsed, tagged
    authored or library, 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_core 0.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_area routes, 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 (description on
    RSS, summary falling back to content on 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_legacy hardware 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_select on 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

  • /discover announces can suggest a display name (discussion #24).
    The Register card in Settings → Devices gains a Display name field
    prefilled from the announce's optional name; 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 /discover and /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×), a date_label_style
    choice with a full-word option, and a show_location toggle for
    the widgets that lacked one. calendar_day also gets configurable
    day_start_hour/day_end_hour and a show_density toggle;
    calendar_week gets a range_mode option 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_hour narrowed 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
    held with 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/new accept
    device_ids accordingly.
  • 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 from docs/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 strict updates.on_change source declarations,
    optionally narrowed by a declared selector_option. Grid cells and
    Canvas widget elements persist an independent update_on_change
    policy (off by default), and their editors surface the switch only
    for capable widgets.