github dmellok/tesserae v0.505.0

45 minutes ago

Added

  • Buttons: a press can name its physical button (button_key) beside its action, and the device's button map binds that key first, so the four buttons on an Inky Impression can each show their own dashboard ({"a": "page:Calendar", …, "d": "page:Family"}) with tesserae-device-pi 0.9.3. Maps keyed by left / right / refresh work as before.

  • Raspberry Pi: one pi_client kind for the unified tesserae-device-pi client. A Spectra 6 Impression gets the packed .bin, every other Inky panel the PNG, from the panel the Pi reports when it pairs; it stays awake by default.

  • Device batteries: each chart has a 6h range too, and the second switch at the top of the page is gone.

  • Device batteries: a gear next to each frame's name opens its settings.

  • Lineups: a block that runs all day and carries on into tomorrow no longer counts down to an end; it says when quiet hours pause it.

  • Device page: no Next picture when the next step is the dashboard already showing, since its next picture isn't drawn yet.

  • Visionect frames: update firmware from the Firmware page, more frame settings and connection health on the device page, and a sleeping frame keeps its old picture with newer ones Queued.

  • Visionect firmware: the builds tested on a frame are checked against a known SHA-256 when downloaded or read from the cache, and a list entry whose checksum isn't plain hex is skipped.

  • Device page: summary tiles and the next picture in the left column, a Lineups link, accent group titles, Troubleshooting's buttons at its foot, and devices listed under Settings in the sidebar.

  • GPT Image from words alone (gpt_image_t2i) for a widget that puts a
    picture's whole layout in its prompt, such as a card with its own type,
    where a reference picture would pull the composition towards itself. It
    takes the same quality and size as the edit model, and goes straight to
    OpenAI when only an OpenAI key is set.

  • Device page: a summary rail beside Display, Schedule, Controls, Power, Troubleshooting and Remove sections, with compact field rows and Advanced disclosures.

  • Collapsible sections on the Server, System, About, Stats and other admin pages, remembered per page.

  • Sub-section titles are one heading style on every page, with a hairline above.

  • Paper's section title marker follows the theme's accent colour instead of a fixed red.

  • The Lineups page is centred like the other pages instead of sitting left.

  • Device batteries: the chart marks each wake-up with a dot and why it happened.

  • Device batteries: a Wakes / day tile, and time with no readings is shaded instead of drawn as a line.

  • Device batteries: each chart has its own 12h / 24h / 7d / 30d range and drag-to-zoom.

  • Device batteries: a readable dark tooltip, and Clear history is a corner link.

  • Device batteries: Visionect frames get drain and Reaches predictions too, and a short chart range predicts from at least a week of readings.

  • Device page: six collapsible sections instead of eleven, and the top tiles jump to their section.

  • Lineups: next update times are right for a display asleep in quiet hours, such as no more ~6:31 PM after a 9 AM wake.

  • Lineups: a display's top thumbnail always shows what is on the frame; what waits for it, from a lineup or a manual send, is a small tagged thumbnail on its corner.

  • Lineups: pushing a step from a lineup marks that lineup live, even one that does not run today; sending something by hand marks none.

  • Lineups: zoom a thumbnail into a larger view with Push now, and use previous and next arrows on a step to pick a photo or draw another picture before pushing it.

  • Quiet hours: a lineup step or interval skipped overnight now lands as quiet hours end, and a sleeping display wakes for it.

  • A widget that paints with GPT Image can ask for the quality (low, medium
    or high) and the exact size of the picture, so a poster can be painted
    sharp at its panel's own resolution (1200x1600 for a 13.3" or OpenPaper L
    panel) instead of 768x1024 at medium. fal.ai takes the size as asked,
    rounded to multiples of 16; a picture sent straight to OpenAI gets the
    quality at OpenAI's nearest size. The daily painting budget is charged the
    high-quality price, about 20 US cents, when high is asked for. Widgets that
    ask for neither are unchanged.

  • Paper's phone shell. The four places you go most (Send, Dashboards,
    Lineups, History) sit in a tab bar along the bottom and More lifts the
    rest up as a sheet, in place of the hamburger drawer. Once a page's title
    scrolls under the top bar, the bar turns into the page bar: the title
    where the brand name was, and the notices that scrolled away (restart
    required, an update, new devices, a low battery) as chips. Those notices
    sit in a band at the top of the page rather than the drawer. The battery
    pill leaves the top bar; the charge list lives in the sheet. Desktop and
    the classic design are unchanged.

  • A select cell option's choice can list aliases, the values an earlier
    manifest used for it. A cell that still holds one shows the choice it
    became rather than "Unavailable: ", show_if reads it the same way,
    and the next save writes the current value, so a widget can rename a value
    without stranding every cell set up before the rename.

  • Lineups redesign, part 1 of 3: a view model for the Lineups page and the
    endpoints the new page edits through. GET /decks/view.json describes every
    display (what is on screen, what is queued and when a sleeping frame picks it
    up, the real wake time, quiet hours, a day bar per lineup, each step with a
    small thumbnail). POST /decks/<id>/window, /days and /edits change a
    lineup's time window and weekdays in one locked write, refuse a change that
    makes two lineups on one display overlap (409) and warn when a run falls in
    quiet hours. POST /decks/<id>/push-step pushes one step now; with the
    step's composition_digest it resends the saved picture the tile shows
    instead of rendering again. Step thumbnails are small immutable WebP
    snapshots keyed by the pushed composition, looked up in one query per page;
    a dashboard never pushed is rendered once in the background. Lineup steps
    whose dashboard was deleted are flagged (missing) and the lineup editor
    lists them so they can be removed. Additive: the current Lineups page is
    unchanged apart from using the same small thumbnails.

  • Lineups redesign, part 3 of 3: a push to a sleeping frame says so. Every
    push surface (Send, Push now on the Dashboards list and device page, the
    canvas editor's Send, lineup steps and Push, History resend, calibration
    and test patterns, the setup wizard) adds a notice per waiting display,
    such as "Hallway Frame is asleep: it will pick this up at its next check-in
    (~3:48 PM)", or names quiet hours, an overdue check-in or a lost
    connection. The webhook, REST, canvas and MCP push responses gain an
    additive delivery list with the same words and expected_at, and a
    Lineups step reads "Queued · picks up ~3:48 PM". Pickup times use one
    formatter everywhere.

  • A device with its own menu can list the dashboards bound to it and show
    any of them. frames.json now lists, after "Current dashboard", one item per
    dashboard bound to the device that is not archived, with an ASCII title.
    Picking one, or adding ?page=<id> to /frame or /frame.bmp, shows that
    dashboard on the device without moving its rotation or lineup position, the
    same as a button mapped to page:<id>. A dashboard not bound to the device is
    refused.

  • The Dashboards list has a By display | All switch in its toolbar. All shows
    every dashboard once, A to Z, with a chip for each display it is on (or
    Unbound), instead of repeating it under each display; search, the Active and
    Archived tabs and bulk actions work as before. By display stays the default.
    ?view=all links straight to it, and the choice is remembered.

  • Widgets and data plugins, bundled ones included, can be disabled from the
    Widgets page, and enabled again, without a restart. A disabled plugin leaves
    the Admin pages menu and the add-a-widget pickers, its admin page and routes
    answer 404 with a friendly page, and it moves to a Disabled section with an
    Enable button. Dashboards that use it keep their settings and show a
    "Widget X is disabled" placeholder. Disabling something in use asks first and
    names the dashboards. Fonts, themes, renderers and devices cannot be
    disabled, nor can Home Assistant, Weather and Calendar, which the app itself
    relies on. Bundled plugins now carry a Bundled tag, since they can be
    disabled but not removed.

  • Every clock time in the app follows the language and region setting
    (Settings › Server › App › Locale, the host's when unset) through one
    formatter: 3:48 PM in a 12-hour locale, 15:48 in a 24-hour one. That
    covers the Settings › Devices "Next update" tile and detail, the device page
    and smart-sync line, the Lineups page (next advance, next push, fires, now),
    History and Events timestamps, rooms "busy until", schedule times, the
    battery chart axis and tooltip, the timed-lineup wizard, Visionect and relay
    notes, the generative-AI reset time and the Bluetooth/Wi-Fi reading stamps.
    Home Assistant sensors, the REST/companion API, JSON fields, ISO stamps,
    logs, saved settings and time inputs keep the 24-hour form. With no locale
    set and none from the host (the usual container), times stay 24-hour.

  • Visionect setup has an app-wide Server port setting (1024–65535), with
    environment and optional Home Assistant App option overrides, immediate listener
    rebinding, and instructions that show the configured port. Default: 11113.
    A port below 1024 or an unreadable TESSERAE_VISIONECT_PORT now turns the
    listener off with an error on the setup card, where it used to fall back to
    11113 silently. The Home Assistant App does not publish the port yet.

Changed

  • Raspberry Pi: the docs, README and issue templates point to the unified tesserae-device-pi client; the pi-png and pi-bin repos are archived. The Pi cloud-init (scripts/pi-client-cloud-init.yaml) installs it from PyPI with its update rollback guard, needs only server_url in meta-data (device_id optional, no panel model), and reboots itself once so SPI and I²C take effect.
  • Lineups redesign, part 2 of 3: the Lineups page is rebuilt on the view model
    from part 1. Each display gets a header that says what is on screen (the
    picture itself), what is queued and when it lands, and when the frame next
    wakes. Lineups show as Cards (a strip of each lineup's dashboards with Push
    now on every step) or as a Timeline (one track per display across the day,
    with repeated updates of one dashboard collapsed into one thumbnail and a
    "x N updates" badge). Each lineup has a day bar with ticks for every
    advance, instant tooltips, an "Update in N min" flag on the next one, start
    labels under busy bars and a hatched strip where quiet hours cut into an
    all-day block. Today | All switches between today's rows and every row; Fold
    all, a settings gear by each display name, a span icon for the Full cycle
    chip, drag and keyboard editing of windows and days, and a deleted
    dashboard shown as a flagged step with Remove (also in the lineup editor).
    Times follow the app locale's 12/24-hour style. Settings > Devices shows one
    "Now" line per display (the live lineup and until when, or the next one)
    instead of every lineup name. Each row's menu
    holds Edit, Step back (by-hand lineups), Push now, Enable or Disable and
    Delete; rows follow the order they next play in, and the Display order and
    Z to A sorts are gone.
  • The device page's Lineups section lists the display's lineups (when, which
    days, how many dashboards, Live or when it plays next), each linking to it
    on the Lineups page, above any schedules. It used to list schedules only,
    so a display run by lineups read "Nothing scheduled yet".
  • A lineup, schedule or rotation that names a dashboard bound only to another
    display is now refused unless you agree to bind it. Before, the save went
    through and every run failed with "dashboard targets none of the requested
    device(s)". The lineup editor, setup wizard and schedule form show a Bind to
    this display prompt (move it, or keep its other display too), and the save
    binds in the same request, before the record is written. Without consent
    every write path refuses: forms flash why, JSON and MCP answer 409
    pages_not_bound with the dashboards and a bind_pages fix, and the
    companion API returns 400 (it has no consent field yet). Lineups already in
    that state show a flagged "Not on this display" step with a one-click Bind
    button.
  • Display-accurate previews: thumbnails show what the display can actually
    show. A black-and-white screen previews in black and white instead of full
    color, a grey screen in greyscale, and a color screen is unchanged. A
    black-and-white display with a dithering mode previews as smooth grey, since
    dithering averages out at thumbnail size; only dither none uses the hard
    black and white threshold. Covers the Devices list and device page,
    Dashboards list, History, Events, Send, the dashboard editor preview and the
    lineup editor. Dashboards with no display, or shared across displays that
    differ, stay in color. Previews only; what is sent to the panel is
    unchanged. See docs/thumbnail-color.md.

Fixed

  • Home Assistant, Entities: a door, window or motion sensor reads Closed / Open / Clear the way Home Assistant words it, instead of Off / On.

  • Visionect frames: a saved Wi-Fi password now reaches the frame, is masked on the device page instead of sent back to the browser, and is hidden in the frame log if the frame echoes it.

  • History: 12-hour times no longer overlap the thumbnail.

  • A link now always sits left of a button, and the primary action is rightmost,
    across the admin pages, dialogs and banners.

  • Device batteries: a boot's reset reason no longer marks later status
    changes as restarts.

  • Lineups and Home Assistant's next update follow a wake interval the page
    on the display declares.

  • Device page: saving after an edit in quiet hours, the button map or battery
    offsets lands back on that group; underscan keeps its old maximum.

  • Widgets: Loader errors can't be collapsed out of sight.

  • A lineup step that lands a pre-rendered frame now shows up everywhere the
    newest push is read. Home Assistant's Next and Previous, a physical button,
    a tap, the companion API, the Lineups page's step and a home return could
    swap the cached frame onto the display without writing a History row, so
    the Lineups thumbnails, History and "from Home Assistant" labels kept the
    frame from before. The promote now writes that row itself (with its
    source), the timer advance no longer writes a second one, and a button
    press's row links to it.

  • Settings > Devices and the device page no longer show a stale or mislabelled
    preview for a pull (REST) frame after a push. The page used to keep the old
    thumbnail until reloaded, and after a reload showed the newest image under
    "Showing" with the old image's fetch time. The thumbnail is now what the
    frame last fetched, the image waiting for its next check-in sits under it
    tagged "Queued", and a line says when it was queued and when the frame picks
    it up ("Delivering now" for a connected frame). Thumbnail URLs carry the
    render digest, and the open page follows pushes and fetches on its own.
    Frames that fetch through another route (CrossPoint readers on
    /frame.bmp, deck and album clients) show the latest render as before.

  • The Mirror page (/mirror/<device>) loads its image behind a URL prefix
    such as Home Assistant's sidebar panel. The image URL was root-absolute, so
    under ingress it never loaded and the page stayed black; it now carries the
    prefix. The device name on the page is HTML-escaped too.

  • Picture Gallery: replacing an image with a new file of the same name now
    shows the new thumbnail. Thumbnail URLs carry a ?v=<mtime>-<size> version;
    a link without it is revalidated rather than cached for a year.

  • Deleting a dashboard that a lineup uses now asks first, naming the lineups
    ("X is in the lineups A, B. Delete it and remove it from them?"), and once
    approved it also takes the dashboard out of them: its step, any link to it,
    the home, entry and fallback pointers, and a display's saved position on it.
    A lineup left with no other dashboard is deleted, and the confirm names it
    beforehand.
    It applies to Delete and bulk delete on the Dashboards list (one confirm for
    the whole selection; without JavaScript a confirm page does the same). The
    canvas editor's delete route and the MCP DELETE /pages/<id> answer 409
    page_in_lineups with the lineup names (and would_delete_lineups) until repeated with
    remove_from_lineups=true; the canvas editor's own Delete asks the same
    question. MCP bridge 0.19.0 adds remove_from_lineups to
    delete_canvas_page. Before, the lineup kept a step pointing at a dashboard
    that no longer existed.

  • The image lightbox (gallery, history, events, artwork) no longer scrolls the
    page behind it: page scroll is locked while it is open (scrollbar width
    compensated, no layout shift, position kept on close) and released on every
    close path. Images now open fit to the screen; click, tap or Enter/Space
    toggles a full-size view that pans inside the overlay, and a second click
    returns to fit. Backdrop click closes in the fit view only; Escape always
    closes.

Security

  • certifi 2026.10.23 in the lockfile, for PYSEC-2026-4320 in 2026.7.22.

[0.496.1], 2026-10-31

Fixed

  • Dashboards: New dashboard no longer hides the create form when it is
    already open (an empty list, or ?create=1); it opens the form and
    focuses the name. The Grid / Freeform picker now says what each kind is.
  • A device id typed with spaces ("Kitchen Panel") becomes kitchen_panel
    instead of being rejected, in the setup wizard and Settings › Devices.
  • Setup wizard: the device step links to the web flasher, the MQTT choice
    notes that battery ESP32 boards should use REST, and "Add a device by
    hand" starts collapsed with a note that a flashed panel registers itself.
    The empty Devices list links to the web flasher too.

[0.496.0], 2026-10-30

Added

  • Gemini as a third provider for AI pictures. Settings › Generative AI takes
    a Gemini API key from Google AI Studio, with a Check that lists the
    models the key can use without painting anything. With it, Nano Banana
    and Nano Banana Pro are painted straight by Google instead of being
    relayed by fal.ai; an install with only a Gemini key can now paint, every
    picture going to Nano Banana. Charges and the ledger show them as "Nano
    Banana (Google)" and "Nano Banana Pro (Google)" at Google's price, inside
    the same daily and monthly ceilings. Widgets need no change.

[0.495.0], 2026-10-30

Added

  • A button or touch event with button_event_id=0 resets the device's
    stored event id: it is never dropped as a repeat of the last id, so a
    client that counts from 0 on every boot (the CircuitPython client) doesn't
    lose the first press after a restart. A quick resend of 0 is still caught
    by the time-window debounce. ESP32 firmware starts its ids at a random
    point and is unaffected.

[0.494.1], 2026-10-30

Fixed

  • Home Assistant notifications draw umlauts, accented letters, the euro sign
    and Chinese or Japanese text instead of empty boxes. The notice card used
    Pillow's built-in font, which only has ASCII; it now uses the bundled Inter,
    falling back to a system font that has every character of the message.
    Per #420.

[0.494.0], 2026-10-30

Added

  • Home Assistant gets a Next update sensor per display, the same time the
    Devices and Lineups pages show for when the next change reaches the frame
    (push time weighed against a live connection or the frame's next check-in,
    held by quiet hours or a pause). Its attributes carry the reason
    (connected, check-in, held by quiet hours, ...) and the next check-in.

Changed

  • Home Assistant's Next wake sensor no longer shows the raw telemetry
    prediction, which read 24 minutes for a Visionect sign that was connected and
    took pushes within seconds. It now means the next expected check-in of a
    sleeping frame (from the cycle it has actually been seen to keep), and reads
    unknown while the frame is connected or stays awake. A stale time left
    retained from an earlier session is cleared.

Fixed

  • The Devices page, the device page and the Lineups page now agree on when a
    frame actually gets its next update. They used to show two numbers that
    contradicted each other ("Next wake in 24 min" on one, "next advance in 1
    min" on the other, when the push was in fact taken within seconds). The
    Next update is the server's next push (lineup step, schedule, timer
    advance, page refresh) weighed against when the frame can take it: a frame
    holding a live connection or staying awake gets it right away, a sleeping
    frame gets it at its first check-in at or after the push. Quiet hours and
    pauses show as "held until 07:00". The Devices tile and Smart sync card read
    e.g. "Next update ~18:24 (frame sleeps; next check-in ~18:24)", and every
    lineup card with a next push says the same under "next advance".
  • Visionect signs: the check-in estimate follows the gap actually observed
    between the sign's statuses instead of the configured interval (a 32" set to
    30 min reports about every 15), so the prediction earns trust. A sign with a
    live connection reads "connected" instead of counting down a wake.
  • The status dot on the Devices list, the device page header, the Dashboards
    list's per-display dot and the dashboards' device chips follows a sleeping
    device's own cycle. It went red 5 minutes after a heartbeat, so a battery
    e-paper frame that wakes every 30 minutes read as stale for most of each
    cycle. When the device's sleep cycle is known the dot stays green until a
    little past one cycle, amber for a couple more, and red after that (and the
    "online", "quiet" and "stale" wording beside it agrees). The Companion API's
    freshness flag and Home Assistant's online state use the same observed
    cycle instead of only the configured interval. Devices with no known cycle,
    and always-on ones, keep the old bands.
  • Thumbnails keep the screen's shape instead of cropping a portrait picture to
    its middle. The latest-render thumbnail on the Devices list and the device
    page was a fixed 4:3 box; it now follows the panel's aspect ratio (taller for
    portrait, wider for landscape) and the whole picture fits.
  • The same goes for the Dashboards list (the small thumbnail on each row and
    the hover preview, as well as the opened row's preview) and the Lineups page
    (the dashboard cards, including "on the panel", and the page cards in the
    lineup editor): a portrait dashboard is a tall card, a landscape one stays
    wide, in the classic and Paper designs and at phone widths. The History
    thumbnails fit rather than crop too.
  • A build that keeps the same version number (a branch, or a fork's add-on) no
    longer serves stale stylesheets and scripts. The static URLs carried only
    the package version and were cached for a year, so a changed CSS file never
    reached a browser that had loaded the old one. The cache-busting version now
    also carries a short digest of the static files, which changes whenever any
    of them does.
  • The Timing block on the device page reads more clearly: Start and End sit
    under the days the window applies on, the switches explain themselves, and
    the override toggle says what On and Off mean.

[0.493.1], 2026-10-29

Fixed

  • The widget-update check no longer lets a refresh started before a reset
    mark the catalog as freshly fetched afterwards. Only the test suite reset
    it, where the coverage job could see the next check skip its fetch.

[0.493.0], 2026-10-29

Added

  • OpenAI as a second provider for AI pictures. Settings › Generative AI takes
    an OpenAI key next to the fal.ai one, with a Check that lists the account's
    models without painting anything. With it, GPT Image is painted straight
    by OpenAI instead of being relayed by fal.ai; an install with only an
    OpenAI key can now paint, every picture going to GPT Image. Charges and
    the ledger show it as "GPT Image 2 (OpenAI)" at OpenAI's price, inside the
    same daily and monthly ceilings. Widgets need no change.

[0.490.0], 2026-10-29

Added

  • Update all on Browse catalog: one click updates every installed catalog
    widget that the index lists a newer release for, skipping any whose new
    release needs a newer Tesserae. One failure doesn't stop the rest; the
    outcome names what was updated and what wasn't. Restart stays a separate
    click, as for a single install.
  • The sidebar's Widgets entry carries a count of installed catalog widgets
    with an update waiting, the way a phone's store icon does. It answers from
    the catalog index the server already caches and refreshes that index in the
    background at most once an hour, never on the request. Needs Online
    features on. Settings → Widgets → Catalog updates switches the count off;
    Browse catalog still marks each widget.

[0.489.0], 2026-10-29

Added

  • Each display gets a Pause number in Home Assistant (minutes). While it
    runs, nothing automated reaches that display: lineup steps, daily and
    interval lineups, rotations, schedules, page refreshes and returns to a
    lineup's home page. Manual sends and physical buttons still go through.
    When the pause ends, or is set to 0, the display gets what it missed
    (waiting for quiet hours to end if need be), and is left alone if
    nothing was missed. The other displays aren't repainted. Hold keeps its
    existing meaning.

  • Hardware kind lilygo_t5s3_pro for the LILYGO T5 E-Paper S3 Pro and Pro Lite
    (H752-01), a 4.7" 960x540 16-level greyscale panel on an ESP32-S3 with a
    TPS65185 power path. The same glass and 259200-byte esp32_gray_bin frame as
    the PaperS3, used landscape; firmware is the lilygo-t5s3-pro build in
    tesserae-device-firmware, confirmed on real hardware by its contributor on a
    Pro Lite. Not auto-selected from a relay self-report, since on the wire it
    matches the PaperS3.

  • Visionect e-paper signs, driven directly with no Visionect Software Suite.
    Tesserae listens on TCP 11113, where the sign dials in, and pushes each
    frame as 16 greys over the sign's own protocol through the pyvisionect
    library.

    • Adding a sign: Add device has a new Visionect tab. Leave the UUID blank
      and the first unknown sign that connects within the hour is bound. Any
      other sign shows up in the discovered devices banner, ready to register.
    • Models: the catalog has the Place & Play 32" and 13", Joan 6 and 13,
      and the 9.7" boards. Each sign reports its own screen size on connect, and
      the device adopts it. The Place & Play 32" declares "buttons": false, so
      its device page has no Controls section and Home Assistant gets no Input
      entity for it.
    • Delivery: one frame is in flight at a time and is tracked by the sign's
      ACK. Refusals are retried. Quiet hours with sleep-through send the sign to
      deep sleep only after the latest frame has landed.
    • Status: the device page shows whether Tesserae is listening and what
      the sign is doing, with Report in, Full refresh and Reboot buttons. Battery,
      signal, temperature and firmware reach the device card and Home Assistant.
      The sign's check-in interval and sleep mode are shown but not changed.
    • Next wake: the device page estimates when the sign will check in next:
      its last check-in plus its own check-in interval, or the end of a
      quiet-hours sleep Tesserae just ordered. Sleep cycle, last offset and the
      smart sync confidence follow from real check-ins. A sign set to stay awake
      has no estimate.
    • Port and address: the listener runs while a Visionect device exists. It
      binds the web server's address (TESSERAE_BIND_HOST), and
      TESSERAE_VISIONECT_PORT moves it off 11113. The protocol has no
      authentication, so keep the port on your LAN.

    See the Visionect quickstart.

  • Device kinds can register hooks in app.device_service (a check before a
    device's config is stored, a callback after it is stored, when a device is
    added from a discovery or removed), so the settings routes and the Home
    Assistant config setter apply a kind's own rules without naming the kind.

  • Adding a device now checks the config values the form supplies before the
    device is created, instead of dropping invalid ones afterwards.

Fixed

  • The Paper design's Settings dropdown marks the page you are on again when
    Tesserae runs behind a URL prefix, such as Home Assistant's sidebar panel or
    a reverse proxy's mount point. The links carried the prefix and the current
    path didn't, so nothing matched and no item was highlighted.

[0.488.1], 2026-10-28

Added

  • Companion supports conditional Dashboard fields, sliders with exact numeric
    input, place search and coordinate entry, and native color editing. The
    optional native-controls header preserves older clients' read-only behavior;
    hidden saved values, opaque field scopes and revision checks remain intact.

[0.488.0], 2026-10-28

Added

  • Each display gets Next and Previous buttons in Home Assistant. They
    step whatever that display is showing right now, on that display only,
    the way its own left and right buttons do. On a display with several
    lineups bound (one per time slot, say), they step the lineup whose page
    is on the glass. A one-page lineup, or a display that follows nothing,
    is left alone. They work while automation is paused and during quiet
    hours, like other manual sends.

Changed

  • Home Assistant commands that render run one at a time on a background
    worker, in the order they arrived, instead of on the MQTT connection's
    own thread, so a slow render no longer stalls other commands and
    heartbeats. Each waits at most a minute for the renderer and reports a
    busy renderer, a failed push or a full queue on Last error. Saving
    Settings no longer waits for a command in progress.
  • A newer push for a display only cancels an older one still waiting
    while the newer one is itself still waiting or running; one that gave
    up on its wait no longer cancels anything.
  • Button presses, Home Assistant's Next / Previous, lineup timer steps,
    home returns and daily or interval lineup fires take turns per display,
    so two of them can't leave the recorded position naming a page other
    than the one on the glass.

Fixed

  • A lineup's position is only recorded once its page has reached the
    display. A button press, a timer step or a return to the home page that
    failed, or that a newer send replaced, no longer leaves the position
    naming a page the display never showed. Daily and interval lineup fires
    now record their position too, so Next and Previous continue from the
    page they put up.

[0.487.1], 2026-10-28

Fixed

  • Interface text no longer changes typeface and apparent size after a page
    appears. Common UI fonts are preloaded, bundled fonts stay cached between
    pages, and a late font keeps the initial fallback for that page view.
    UI font aliases keep this policy independent of dashboard content fonts.
  • Paper pages start at their final position instead of sliding all text
    through fractional pixels on each navigation.
  • Icons reserve their final width while their font loads, keeping nearby
    navigation, button and display labels in place.

[0.487.0], 2026-10-28

Added

  • Two switches next to the Home Assistant discovery toggle, both on by
    default: Publish lineup controls to Home Assistant (each lineup's
    switch, Push now, Next and Previous, and the displays' Lineup picker)
    and Publish a button per dashboard to Home Assistant. Turning one
    off removes those entities from HA when you save; the Active dashboard
    and per-display Dashboard pickers stay.

Fixed

  • Removing a Home Assistant display or lineup no longer removes the
    entities of another whose id starts the same way (kitchen and
    kitchen_2). Each entity is torn down by the display or lineup that
    published it.
  • Home Assistant discovery that starts at boot now has the physical-button
    service, so each display's Input event entity appears straight away
    instead of after the next settings save.

Fixed

  • Home Assistant's per-display In quiet hours sensor follows the server's
    time zone. It read the time zone before it was set at startup, so on the
    Docker image and the HA add-on (both UTC) it switched on and off by the
    UTC clock, hours away from the configured window. Quiet hours themselves
    were not affected.

Changed

  • Form posts that arrive through a reverse proxy are now refused unless the
    proxy forwards the scheme and host (X-Forwarded-Proto and
    X-Forwarded-Host) or Settings → Server → Public URL is set to the address
    you use. If you reach Tesserae through a proxy that doesn't forward them,
    saving a form will fail with a 403 until you set a Public URL or forward the
    headers. The 403 says so.

Fixed

  • Browser requests that change something are refused when they come from
    another site (CSRF), whether or not they carry the session cookie. That
    covers admin, setup, login and token-free loopback MCP calls. Devices,
    webhooks and MCP clients with a valid token are unaffected, and a
    configured Public URL keeps direct LAN form posts working.
  • Forwarded origins, prefixes and Home Assistant ingress headers are only
    trusted from trusted proxies. TESSERAE_TRUSTED_PROXIES adds proxy CIDRs
    outside the private defaults (public IPv6, CGNAT, VPN).
  • Creating a dashboard or canvas requires POST. The old GET URLs open the
    form instead of creating one.
  • Malformed or non-ASCII MCP credentials are refused instead of raising a
    server error.

Added

  • Companion can report phone-confirmed Bluetooth mode, battery percentage and
    firmware version. Settings shows the saved observations with their collection
    times; newer Wi-Fi heartbeat readings take precedence. Reports persist across
    restarts without changing device reachability or delivery rules.

  • Widgets are told which placement they are rendering for: ctx.placement_id
    names the page, the cell and the device. A widget that declares
    render.per_placement in its manifest also gets its own fetch for each
    placement instead of one shared by identical cells, so a rotation keeps a
    separate turn on a big panel and a small one. Other widgets are unchanged.

  • The push pipeline can now be asked whether a render is under way for a
    device (PushManager.render_in_progress). It is true from the moment a
    push for the device starts, through the settle wait, the queue for the
    render lock, rendering and publishing, and a repaint after a renderer
    change, and false once the new render is the device's latest. Code that
    must not act on the old picture while a new one is on its way, such as
    putting a device to sleep for quiet hours, can wait on it instead of
    guessing.

  • A device kind or hardware entry can say "buttons": false when it has no
    physical buttons. Its device page then has no button map (and no Controls
    section at all if it has no touch panel either), and Home Assistant gets no
    Input event entity for it, instead of an entity that can never fire. Kinds
    that don't say anything are treated as before.

Fixed

  • Two things saving the same device at once can no longer lose a change. A
    device's file is read, patched and written back whole by many paths (a
    rename, a transport switch, a token regeneration, quiet hours, a panel size
    the device reports, a firmware-driven renderer switch). They now take turns,
    and a save that read a device's file just before the device was removed can
    no longer write it back.

  • Saving a device's button map, update delivery or synchronized wake, turning
    automatic firmware updates on or off, and removing a device now change only
    that one setting. Each used to copy the device's whole settings entry and
    write it back, which could undo something saved on the same device a moment
    earlier by another request.

[0.486.1], 2026-10-28

Fixed

  • Changing a dashboard's content-update cadence or device wake-up interval
    from the Dashboards list stays on the list. The save used to redirect into
    that dashboard's editor. (reported on tesserae-widget-underway#2)

[0.486.0], 2026-10-28

Added

  • Hardware kind m5stack_paperdiy for the M5Stack PaperDIY (Complete Kit and
    Assembly Kit), a 4.7" 960x540 16-level greyscale panel on an ESP32-S3 with
    the M5PM1 PMIC and a 140 mAh cell. The same glass and 259200-byte
    esp32_gray_bin frame as the PaperS3, used landscape; firmware is the
    m5stack-paperdiy build in tesserae-device-firmware, confirmed on real
    hardware by its contributor including both keys. Not auto-selected from a
    relay self-report, since on the wire it matches the PaperS3.

[0.485.0], 2026-10-28

Changed

  • Paper design: Settings is a dropdown in the main sidebar, like Widgets,
    instead of a second sidebar beside it. The dropdown holding the current
    page opens with that page highlighted. Phones keep the tab strip, and
    the classic design is unchanged.

[0.484.2], 2026-10-28

Fixed

  • Quiet hours end at the right time across daylight-saving changes. A
    window ending inside the skipped hour, or in the hour that repeats, used
    to wake the device early or late. Devices also stay quiet through the
    whole configured end minute, so device sleep and push gating agree.

[0.484.1], 2026-10-28

Fixed

  • Outbound fetches that refuse private addresses (picture imports, page
    assets, fonts, REST widgets, the companion and relay checks) check the
    host the socket will actually reach. Encoded hosts like 127%2e0%2e0%2e1,
    numeric spellings like 2130706433, IPv4 inside IPv6 and a DNS answer
    that changes between the check and the connection no longer get through.

[0.484.0], 2026-10-28

Added

  • Hardware kind huessen_epf1301 for the Huessen EPF1301, a 13.3" Spectra 6
    picture frame (EL133UF1, 1200x1600) on an ESP32-S3. It takes the same
    960000-byte esp32_bin frame as the reTerminal E1004; firmware is the
    huessen-epf1301 build in tesserae-device-firmware, confirmed on real
    hardware by its contributor. Not auto-selected from a relay self-report,
    since it is indistinguishable on the wire from the other 13.3" Spectra 6
    kinds.

Fixed

  • Smart sync predicts a silent device's next wake from the next_poll_s the
    /status response handed out, not from the configured sleep interval. A
    device that published neither sleep_until nor next_sleep_s and slept
    for a lineup-driven 5 minute next_poll_s under a 30 minute interval
    arrived "25 minutes early" every wake and never earned trust. (#387)
  • The calibration preview matches a palette profile to the panel's colour
    slots by colour, as the renderer has since #298. On inky_7colour panels
    (Inkplate 6COLOR, Inky Impression) the Palette Swatches pattern and the
    live palette editor preview painted the calibrated yellow in the green
    slot and the blue in the red one. (#386)

[0.483.2], 2026-10-27

Changed

  • The paperlesspaper OpenPaper L is confirmed on real hardware: catalog notes,
    tested data and the compatibility page say so.

[0.483.0], 2026-10-27

Added

  • Hardware kind waveshare_rlcd_42 for Waveshare's ESP32-S3-RLCD-4.2, a 4.2"
    400x300 reflective LCD (Sitronix ST7305). Not e-paper: it repaints instantly
    and holds its image only while powered. Inherits esp32_bw_client and the
    15000-byte mono frame the Waveshare 4.2" e-paper module takes; firmware is
    the waveshare-rlcd-42 build in tesserae-device-firmware, confirmed on a
    bench unit. Not auto-selected from a relay self-report because it shares
    the mono 400x300 report with the e-paper module.

[0.482.0], 2026-10-27

Changed

  • Settings builds only the cards the open tab shows. Every tab used to
    assemble the full device view-model (status, timetable, palette and
    firmware lookups for each display) and then discard it, which is what
    made the Server tab slow to open on a Docker install with a few
    devices.
  • The System tab's GitHub release check on Docker and other non-git
    installs runs in the background. The page renders the cached result,
    or a "checking" line that fills in once the check finishes, instead
    of waiting on GitHub before it can load.
  • The per-device firmware "update available" lookup no longer waits on
    api.tesserae.ink during a Settings render. A cold or expired cache
    starts one background fetch per device kind, and the next render
    shows the answer.

[0.481.2], 2026-10-27

Fixed

  • The Proxmox VE helper script asks for the release channel on the host,
    before the container dialog. Asking inside the container never showed
    the question, because the engine treats install-time prompts as
    unattended, so every install silently took stable.

[0.481.1], 2026-10-27

Changed

  • The Proxmox VE helper script defaults to 4 CPUs, 3 GB RAM and a 16 GB
    disk. The metadata record matches.

[0.481.0], 2026-10-27

Added

  • A Proxmox VE helper script in the community-scripts format, under
    scripts/proxmox/. It creates an unprivileged Debian 13 container,
    installs Tesserae on a stable (tagged release) or edge (main branch)
    channel with Chromium and a systemd service, and lands on the setup
    wizard. update in the container shell moves it forward on the chosen
    channel. The LXC install page documents how to run it.
  • Installers that own the upgrade path can set TESSERAE_INSTALL_METHOD;
    Settings → System then points at their update command instead of
    suggesting a re-run of install.sh. The helper script sets it.

[0.480.1], 2026-10-26

Fixed

  • The battery route tests run against the onboarding check added in 0.479.1,
    so CI passes again. No change to the app itself.

Don't miss a new tesserae release

NewReleases is sending notifications on new releases.