Skip to content

v0.318.0

Latest

Choose a tag to compare

@github-actions github-actions released this 19 Aug 02:04
· 2 commits to main since this release

Added

  • A /discover claim says when the server kept a different device id. The MAC is the
    identity on that path, so announcing a new device_id for an already-registered MAC hands
    back the stored id and its token rather than renaming anything. That's what lets a re-flashed
    board re-acquire, but the response never said so, and a client that missed the echoed id kept
    talking about a device the server doesn't have. The claim response now carries
    device_id_changed (plus announced_device_id when it did), sends
    X-Tesserae-Device-Id / X-Tesserae-Device-Id-Changed headers so a client can branch without
    parsing JSON, and logs the mismatch at WARNING. Still a 200: the device is paired and its
    token works. Both headers are sent on every claim, so an absent header means an older server
    rather than "unchanged". (#239)

[0.317.0], 2026-08-18

Added

  • Companion can securely register a physically nearby display during
    Bluetooth setup.
    Authenticated app sessions with device_setup:write may
    mint a short-lived, single-use firmware registration code through
    POST /api/app/v1/device-pairings. Firmware still redeems that code through
    the existing device registration endpoint, so the app never receives a
    display token. Each Companion session keeps at most one unredeemed code;
    requesting another invalidates the previous one. Existing Companion sessions
    require an explicit operator grant; new pairings include the setup permission.

[0.316.0], 2026-08-18

Added

  • A dashboard's settings open in the composer, in the same slide-out the
    source config uses, so the values it asks for can be changed without leaving
    the canvas. The fields are the same ones the standalone settings page renders,
    and saving re-reads the dashboard from the server rather than trusting the
    editor's in-memory copy, so the rewritten element options show up immediately
    instead of arriving as an "changed externally" prompt.

Fixed

  • The Dashboards list stays aligned when a dashboard has settings. The row's
    actions are anchored to the right, so a button that only some rows carried
    shifted Edit and Push sideways on those rows. Every row now reserves the slot
    and only the rows with settings fill it.

[0.315.1], 2026-08-18

Fixed

  • Agents are told about a dashboard's settings surface. The MCP handshake
    instructions now cover declaring inputs and that patch_canvas accepts them
    on their own, so the one-call path for declaring settings after the elements
    are placed is discoverable. The bridge serves per-tool descriptions from its
    own package, so this had to reach agents through the server-fetched
    instructions rather than a tool description.

[0.315.0], 2026-08-18

Added

  • A freeform dashboard can carry its own settings page. A canvas declares
    the values it asks for (canvas.inputs), and /pages/canvas/c/<id>/configure
    renders one field per declared setting, pre-filled from what the dashboard is
    currently rendering. An agent building a dashboard over MCP declares these as
    it goes, so whoever inherits it changes the API endpoint or the postcode on a
    settings page instead of hunting through per-element option drawers. A
    dashboard that never declared any can derive them: Suggest settings runs the
    Share flow's own analysis, so whatever would be redacted and asked of an
    installer becomes a setting here. The declaration is the same shape the
    template format already used, so a dashboard configured locally asks an
    installer exactly what its author was asked.

Fixed

  • A sensitive option that has to stay readable no longer renders as a
    password box.
    The secret flag on a widget option was answering two
    questions at once: keep this out of the render context and off the
    marketplace, and hide it from the person editing it. Those diverge on a URL,
    which is install-specific and can carry a key in its query string yet cannot
    be maintained when masked, so masking moves to its own mask key that
    defaults to secret. The REST / JSON service's URL opts out and is editable
    again (#237).

[0.314.0], 2026-08-18

Added

  • Apple Health seven-day summaries can now be published through Companion.
    Servers advertise the opt-in health.summary source, strictly validate its
    bounded Activity, Sleep, and Workout sections, retain only the latest
    publisher-isolated snapshot until expiry, and emit source-wide semantic
    refresh events without putting raw Health values in logs, API errors, or
    backups.

Fixed

  • Prioritised multi-select lists can actually be reordered. Checked rows
    now expose a drag handle that works with a mouse, touch, or pen, plus arrow,
    Home, and End keys for precise accessible movement. The new DOM order is
    submitted immediately, so priority-based widgets such as Album Art evaluate
    Home Assistant entities in the order shown by the editor. Reordering keeps
    the handle focused across the move, so arrow keys step an entry more than
    once and ticking a row doesn't drop keyboard focus to the page.

[0.313.0], 2026-08-17

Added

  • Custom request headers on webpage renders, so a dashboard behind a bearer
    token or an API key can be put on a panel without making it public (#234).
    Available on the Webpage widget, on Send → Webpage, and on
    POST /api/app/v1/webpages. The shape is a JSON object, matching the
    headers option rest_service already uses rather than inventing a second
    convention:

    {"Authorization": "Bearer …", "X-API-Key": ""}

    User-Agent is accepted and becomes the browser identity for that capture
    rather than staying a header, so a site that checks navigator.userAgent
    client-side sees the same value that went on the wire. That covers services
    which reject unknown clients, though as noted in #178 it will not get past
    protections that also inspect TLS fingerprints.

    Headers are scoped to the target URL's own origin. They are attached by
    Chromium's request interceptor, not set on the browser context, because a
    context header goes to every origin the page touches: on a dashboard render
    that is every other widget's upstream. Origin scoping means a credential
    cannot reach a third-party subresource, another cell's service, or wherever a
    redirect points next, and it makes that a property of the mechanism rather
    than a rule to remember. The SSRF block decision still runs first, so a host
    the guard refuses is never handed headers on the way out.

    Values are never logged, never written to History, and never included in
    error messages; the log line records how many headers and which names. On the
    Send page the field is not remembered between pushes. Transport and
    browser-managed headers (Host, Content-Length, Connection,
    Transfer-Encoding, Cookie, Proxy-*, Sec-*, and others) are refused,
    as is any value containing a control character, since a newline would be
    header injection.

    One limitation worth knowing: headers cannot reach the live preview in your
    browser, only the server-side render. A webpage cell may show a 401 in the
    editor and render correctly on the panel.

Fixed

  • Secret cell options are no longer written into the composed page.
    compose.html serialises every cell's options into a data-options
    attribute for client-side widgets to read, without regard for the manifest's
    secret flag, so a credential stored on a cell was readable in the composed
    DOM. Reachable only over loopback, and template export already redacted these
    before sharing, so nothing could leave an install; but it put a token
    somewhere it had no reason to be, and anything reading page source (render
    reports, diagnostics) would capture it. Secret-flagged options are now
    dropped before the payload reaches the browser. The server-side data fetch
    runs first and still sees the full option set, so rest_service is
    unaffected.

[0.312.1], 2026-08-17

Changed

  • App-only Docker upgrades download much smaller changed layers. System
    packages and Python runtime dependencies are installed before the frequently
    changing Tesserae source, and tagged builds can fall back to the main build
    cache, so unchanged large layers can be reused when the cache is available.

[0.312.0], 2026-08-17

Added

  • Edge handling now reaches CircuitPython clients. The
    circuitpython_png and circuitpython_bmp renderers quantise on the
    server but read only fit, contrast and dither mode, so a palette
    profile's "Protect the panel's own colours", "Preserve line-art edges"
    and "Smoothing radius" were resolved, handed over, and dropped. All
    three apply now, using the same helpers and the same meaning as the
    .bin packers. Each is a no-op at its default, so a display that has
    never opened that block gets a byte-identical file.

    Tolerance behaves differently here and the difference is worth knowing.
    This path quantises against the gamut's nominal palette, so panel
    black is #000000 rather than a measured #1F2226, and a photographic
    near-black sky sits further from it than it would after the calibrated
    pre-pass. Where the .bin path wants single digits on photo content,
    this one wants 32 to 48 to catch the same pixels.

    The calibrated palette itself is still not applied on this path: the
    wire palette is a per-gamut contract with the client, and changing it
    would repaint every existing CircuitPython panel. That stays a
    follow-up, and wants to be opt-in per display when it lands.

Fixed

  • Edge-handling settings were discarded on every edit after the first.
    The tone editor forks a bundled preset on first tweak and edits the fork
    in place afterwards. The fork path wrote the submitted edge settings; the
    edit-in-place path wrote the base profile's, so "Protect the panel's own
    colours", "Preserve line-art edges" and "Smoothing radius" took whatever
    value they were forked with and then silently ignored every later change.
    The only way to move one was to reset the display back to the bundled
    preset and fork again. Tone and dither were unaffected, which is why the
    editor looked like it was saving.

  • The tone editor showed two identical Save buttons. The inline button
    and the sticky save bar are meant to be alternative affordances, but the
    bar is sticky rather than fixed, so its natural position is a few pixels
    under the inline button and both were on screen together the moment a
    slider moved. The inline button now hands over to the bar while the form
    is dirty, so exactly one Save is visible at a time.

[0.311.0], 2026-08-17

Added

  • A per-display timeline of the next visible updates, at
    GET /api/app/v1/devices/{device_id}/upcoming behind the
    device_timeline capability. It answers what is expected to change or
    repaint one panel next, when, and why: Lineup advances, Keep Fresh
    re-renders, and Home Return, each with the dashboard it lands on and
    whether the timing is scheduled, conditional, or an estimate.

    The projection replays the running engine's own gates rather than
    walking the stored records, because everything that decides whether an
    update actually reaches the glass lives in the engine: the dwell grid
    and its manual overrides, minimum hold measured window to window,
    per-record cooldowns, daily backfill suppression, Smart Sync, quiet
    hours, and which panel an unbound record resolves to. Anything landing
    inside a display's quiet hours is left out, since it repaints nothing,
    and records due in the same tick are coalesced to the one the panel is
    left showing.

    Dashboard and widget refresh timings are deliberately not included.
    Refresh is becoming event-driven, so covering only its timed half would
    make the timeline quietly incomplete; the cause values are reserved so
    adding them later is additive. Manual sends, webhooks, and Home
    Assistant events are not predicted at all.

  • current_frame_at on the same response, so a countdown has a start
    as well as an end. For a REST display it is when the server handed over
    the frame the panel now holds, taken from the delivery-side snapshot
    already persisted next to the served digest, so it survives a restart;
    for other transports it is when the current frame was published. It is
    never a claim that the panel finished repainting, and is null when no
    baseline can be established rather than guessed at.

[0.309.1], 2026-08-16

Fixed

  • Portrait pictures arrived sideways on mono and greyscale ESP32
    panels.
    esp32_bw_bin, esp32_gray_bin and esp32_gray2_bin decided
    the quarter turn by comparing the source image's aspect with the
    firmware's row stride, so any picture taller than it is wide was turned
    90° on a landscape panel, whether or not the panel wanted a turn. A turn
    belongs to the panel: the upload is now fitted to the display's
    composition first, and rotated only when the composition and the
    hardware stride actually disagree, which is what esp32_bin and the
    CircuitPython path already did. Dashboards were unaffected, they arrive
    composition-sized.

[0.309.0], 2026-08-16

Fixed

  • Phone photos landed on their side on Pi panels. A camera stores a
    landscape pixel buffer plus an EXIF orientation tag rather than rotating
    the pixels. The .bin renderers apply that tag inside their own fit,
    but pi_png fits client-side and never called it, so the tag was
    dropped on re-encode and the panel painted the photo turned a quarter.
    Orientation is now settled once when the image is taken in, so every
    renderer sees upright pixels and the History thumbnail matches what the
    panel paints.

Added

  • A rotation control for pushed images, on the Send page and as
    rotate on POST /api/v1/push/image. auto turns the image a quarter
    when its aspect is the opposite of the panel's, so a portrait photo
    fills a landscape panel instead of arriving as a letterboxed strip;
    90 / 180 / 270 turn it clockwise regardless. Unset keeps the
    orientation as shot, since an image carrying text is worse rotated than
    letterboxed and nothing here can tell the two apart. A panel that is
    mounted the other way around still wants the display's own Rotation
    setting, which dashboards follow too.

[0.308.1], 2026-08-16

Fixed

  • Uneven gaps between the cards on the Stats page. Three different
    things were spacing the page: the tiles and sponsor card carried their
    own bottom margins, adjacent section cards get 16px from a global rule,
    and that rule stops applying the moment a wrapper (the two-column grid)
    sits between two cards, which left some seams at zero. The page now
    stacks with a single 16px flex gap and resets the parts that were
    contributing their own, including the adjacent-card rule that outranks a
    plain reset.

[0.308.0], 2026-08-16

Added

  • One sponsorship ask, on the Stats page, after a milestone. A card
    appears once an install has been running a year or has painted 10,000
    frames, on the page someone opened to look at what Tesserae has done for
    them. Fleet size is deliberately not a trigger: panel count can't tell a
    hobbyist with secondhand hardware from a business. Dismissal is
    permanent and silent, with no "remind me later", and the flag lives in
    settings rather than the stats store so deleting the counters can't
    bring the card back. Nothing is gated, delayed, or degraded either way,
    the footer sponsor link is unchanged, and none of this is ever rendered
    to a panel.

[0.307.0], 2026-08-16

Added

  • A Stats page, counted and kept on your own server. The event log is
    capped and rolls over within days, so until now nothing could answer how
    much work an install actually does. /stats now shows pushes per day
    grouped by what asked for them, frames painted per display, check-ins,
    render times, outcomes, and a snapshot of what the install is made of.
    Counters are aggregated as events happen into data/core/stats.db: one
    row per date, metric, and dimension, with no URLs, dashboard titles,
    push contents, or clock times finer than the day. Nothing reads that
    file except the page and the export button, and the page carries the
    controls to prove it: export the whole store as JSON, pause collecting,
    or delete every counter. Documented under Privacy.

[0.306.0], 2026-08-16

Added

  • Flat backgrounds can keep the panel's own colours instead of
    dithering (#227).
    A pixel that already sits on one of the panel's
    colours generates no dither error of its own, but it still receives its
    neighbours', so a background beside saturated content gets pushed off
    its colour and speckles. The palette profile's edge section gains
    "Protect the panel's own colours", a tolerance in sRGB distance: above
    0, those pixels keep their colour, and on the error-diffusion dithers
    they stop passing error onward so a protected region can't collect
    error on one side and dump it out the other. The mask is built on the
    tone-mapped source, since exposure, the S-curve and the calibrated
    pre-pass all move the pixels it means to protect. Wired for the colour
    packers and for black-and-white panels, left out of the greyscale ramp
    where nearly every pixel is close to some level and the same guard
    would flatten photographs. Off by default, and off renders exactly as
    before.

[0.305.0], 2026-08-16

Fixed

  • Saving an album from the Gallery kept only what that form can edit. The
    album form builds a whole record from its own fields, so an explicit frame
    order, a disabled album, or an interval that isn't a round number of minutes
    was dropped the next time someone renamed the album or changed its displays.
    All three come from surfaces the form knows nothing about (the MCP tools
    today, the Companion API next), and none of them are the form's to discard.
    A save now carries them across, leaves the interval alone unless the minutes
    field actually changed, and says an album is disabled instead of reporting a
    success about displays it isn't playing on.

[0.304.0], 2026-08-16

Added

  • Push a single image from a script (#231). POST /api/v1/push/image
    sends one image straight to named displays, using the same webhook token
    as POST /api/v1/push. Until now the only scriptable image push was the
    Companion API, which is a paired-client contract and isn't in the public
    OpenAPI spec, so the obvious place to look didn't have it. Multipart
    upload, required device_ids, optional fit and History label,
    per-device quiet hours honoured (with an override_quiet_hours opt-out for
    images that are worth waking a panel for), and a response that names each
    display under sent, quiet or failed rather than leaving a partial
    result to be inferred from a count. Documented in the spec and in the
    OpenAPI guide with a curl recipe.

Fixed

  • The webhook token's location in the docs was wrong. The API spec and the
    OpenAPI guide sent people to Settings → Server → App; the token has always
    lived under Settings → System → Webhook.

[0.303.0], 2026-08-15

Changed

  • A display plays one offline album at a time, and says so. Binding a
    display that already plays another album used to save cleanly and then
    resolve to whichever album was found first, so the other silently never
    synced. The album form now refuses the save, names the album holding each
    display, and offers a "take over" tick to move them deliberately. A
    displaced album keeps its remaining displays rather than being deleted.
  • A display that reports a frame cache says how much it holds. Its
    advertised capacity and frame cap now ride along with the capability state
    in the Companion API, so a client can tell whether a folder fits without
    guessing from the model name. Absent when the display didn't report them.

[0.302.2], 2026-08-15

Fixed

  • Deleting a device takes its own History, not the dashboard's (#229). A
    push records the dashboard it came from and the displays it reached; the
    wipe only looked at the dashboard. Deleting one display off a shared
    dashboard left its pushes behind, and deleting the last display then removed
    every row for that dashboard, including the first display's. Rows delivered
    only to the deleted display are now removed with it, rows shared with
    another display survive with the dead one dropped from their delivery chips,
    and the delete dialog counts the two separately.
  • A dashboard bound to the same display twice is listed once (#229). A
    repeated id in a dashboard's binding listed it twice under that display on
    the Dashboards page, read as "Push to 2 linked displays", and pushed the
    same frame to the same panel twice. Bindings now collapse repeats when a
    dashboard is loaded or saved, so existing dashboards heal on their own.

[0.302.1], 2026-08-15

Fixed

  • Community templates, install and uninstall work again under the Home
    Assistant App.
    The merged Browse catalog (0.297.0) prefixed its request
    URLs with the ingress path a second time, because the ones it reads from the
    page already carry it. Home Assistant answered the doubled path with a 404,
    which the page reported as "Template catalog unreachable right now"; install
    and uninstall failed the same way. Requests now go through one idempotent
    prefix helper, and a test pins both halves of the contract.

[0.302.0], 2026-08-15

Changed

  • Browse catalog rows lead with the screenshot. Rows are roomier by
    default: a 300×180 preview instead of 152×95, a larger title, and three lines
    of description rather than two. A Cozy / Compact control in the toolbar
    switches back to a scanning density, and the choice is remembered per browser.
  • A row's screenshot enlarges in place. Clicking a row thumbnail opens the
    full-size preview directly, the same lightbox the detail sheet uses, instead
    of only being reachable by opening the sheet first.

[0.301.1], 2026-08-15

Fixed

  • Deleting a device no longer strands the dashboards it shared (#229).
    A deleted device's id stayed on every dashboard it was bound to alongside
    another device, and the delete flow decided what a device owned by counting
    ids rather than live devices. A dashboard bound to two panels therefore kept
    a dead id after the first was deleted, which made it look permanently shared:
    deleting the second panel never offered to remove it, and no later delete
    could either. Ownership is now judged on devices that still exist, so the last
    real device to go takes the dashboard with it and dashboards already carrying
    a dead id heal on the next delete. Wiping a device now also drops it from
    dashboards it shared, and the Dashboards list counts live bindings, so a row
    can no longer read "unbound" and "Push to 2" at once. Panel view resolves its
    gamut from the first live binding for the same reason.

[0.301.0], 2026-08-14

Fixed

  • Touch controls are drawn in the render unless the panel draws them
    itself
    (#228). The button, switch, slider and stepper elements reserved a
    blank rect in every composition, because a touch panel's firmware draws the
    control into that rect itself. On every other target, a display-only panel, a
    preview, a dashboard with no device bound, nothing ever filled it, so a
    control placed in the editor rendered as an invisible hole. The reserve is
    now kept only for panels advertising protocol v2; everywhere else the server
    paints the control. The editor and the renderer share one painter, so the
    preview and the composition can't drift, and render_report gained
    touch_primitives, naming the bound devices that draw each control
    on-device.

[0.300.4], 2026-08-14

Changed

  • Companion responses are validated against contract 0.12. The vendored
    copy of the app contract was several versions behind, so Lineups, the
    session endpoint and the whole Gallery surface were checked against local
    transcriptions of shapes rather than the published ones. The refreshed copy
    carries every value we had been serving ahead of it, so the local errata
    list is now empty, and the Gallery, Lineup, session and capability-support
    fixtures are exercised directly.

[0.300.3], 2026-08-14

Fixed

  • BMP and GIF images are visible over the Companion API (#117). The
    Gallery accepts both and the Web UI serves them unchanged, but the client
    contract's media types cover neither, so a folder of pre-dithered BMPs read
    as empty from the phone. Those images are now served as a full-size PNG
    rendition, generated on first request and cached beside the thumbnails. The
    stored file is never rewritten, and the reported filename, size and cache
    validator describe what is actually served.

[0.300.2], 2026-08-14

Fixed

  • A body that isn't JSON says so. /api/v1/device/discover and
    /register fell back to an empty object when the body failed to parse, so
    the rejection blamed the first field they couldn't find (device_id is required) rather than the parse. A client that formats a Python repr
    (single quotes, bare None) instead of serialising JSON now gets body is not valid JSON. An empty body still names the missing field.

[0.300.1], 2026-08-14

Fixed

  • A placeholder MAC can no longer claim another device's token (#226).
    /discover matches the announced MAC against registered instances, and it
    took any non-empty string at face value. A client that formatted its own
    null into the body sent "None", so two such clients resolved to one
    instance and the second was handed the first one's access token; an
    all-zero or broadcast MAC from a radio that wasn't up yet did the same.
    null, "None", 00:00:00:00:00:00, ff:ff:ff:ff:ff:ff and similar
    stand-ins now read as no MAC at all: /discover rejects them with 400,
    /register pairs but stores no MAC, and an instance that already carries
    one from an earlier install stops being claimable. No migration; the
    stored value stays where it is and simply stops matching.

[0.300.0], 2026-08-14

Added

  • Companion API can browse and add to the photo Gallery (#225). Six
    routes under /api/app/v1/gallery/ list folders, create an internal
    folder, read a folder's images, upload one image, and serve a thumbnail or
    the stored content. Folders and images are addressed by opaque server-minted
    ids rather than filenames or host paths, external folders are read-only, and
    gallery:read + gallery:write ride along with the pairing role.
    Uploads are normalised on the way in: all EXIF is dropped (location
    included), source orientation is baked into the pixels, and the ICC profile
    is preserved. Sending a Gallery photo still goes through the existing
    POST /images flow, so there is one send path rather than two.
  • Displays report which protocol capabilities they support. Each device in
    the Companion API's list carries a capability_support map with a
    supported / unsupported / unknown state per capability, computed from
    the display's own heartbeats. A display that has not checked in recently
    enough to tell reads unknown rather than being reported as incapable.

Fixed

  • The offline photo album form no longer offers displays that can't play
    one.
    It listed every registered display whose kind resolved, so an album
    could be bound to a panel with no frame cache, save cleanly, and then never
    receive anything. Displays that reported no frame cache are now disabled
    with the reason shown, ones that have not checked in yet are marked as
    unconfirmed but still bindable, and a submitted target that reported no
    frame cache is dropped rather than saved.

[0.299.0], 2026-08-14

Fixed

  • /api/v1/device/discover rejects an announce with no MAC (#226). The
    claim path matches the firmware's MAC against a registered instance, so a
    MAC-less announce landed in the Discovered strip, registered cleanly, and
    then polled forever on registered: false with nothing in the response
    saying why. It now returns 400 naming the missing field and pointing at
    the pairing-code path for clients that have no MAC to report.

Changed

  • Device cards show the device id and the MAC. Connection details carry
    both, and a REST device with no stored MAC says so rather than omitting the
    row, so a stuck pairing or a pair of clients announcing the same MAC is
    visible from the card.

[0.298.0], 2026-08-14

Changed

  • Browse catalog rows carry a larger preview. The row thumbnail goes from
    104x65 to 152x95 and the description runs to two clamped lines, so a
    screenshot is legible in the list rather than only in the sheet. The sheet's
    own preview grows to 264px and now opens in a lightbox on click, with the
    thumbnail strip choosing which shot opens, arrow keys stepping through an
    entry's screenshots, and Escape closing the lightbox before the sheet.

[0.297.0], 2026-08-14

Changed

  • Browse community widgets and Community templates are one Browse catalog
    page.
    Widgets, themes and templates now share a single list of dense rows
    with a persistent filter rail (type with live counts, status, category,
    panel fit), a ranked search, a sort menu, and a detail sheet holding the
    long description, the plugin folders and the install detail that used to
    make every card tall. Selecting Templates switches the list into the
    resolution-grouped sections the standalone templates page was built around;
    /plugins/templates/ redirects into it. Install and uninstall post in
    place, so the list keeps its filters, sort position and scroll, and the
    topbar "Restart required" button lights up as soon as the first change
    lands.

[0.296.1], 2026-08-13

Fixed

  • Release notes cover the whole span a release ships. Every version bump
    writes its own CHANGELOG section, and the release workflow extracted only
    the section matching the tag, stopping at the next ## [. A release cut
    after fifty bumps therefore published the last bump's entry and dropped
    everything else, which is what v0.296.0 did on first publish. The workflow
    now reads down to the previous release tag's section, keeping the
    intermediate version headers, and falls back to the single-section
    behaviour when there's no earlier tag to stop at.