Added
-
Mosaic looks for code elements. Mosaic
is an open design system for e-ink dashboards, now vendored and optional. A code element that
names adata-look, uses Mosaic's classes or callsMosaic.gets its core, that look, its
script, the look's fonts and its icons. Five looks:bauhaus,almanac,signal,pixeland
os7(classic Macintosh System 7). See docs/mosaic.md. -
Device log upload and failure reporting. Native firmware that
advertiseslogs: {schema: 1}on its status can now send its serial log
to the server. The device page gains a Logs section (only for such panels)
to collect logs on the next 1, 5 or 20 wakes, view an upload inline or
download it, and see the last detected failure. The status response asks
for an upload with a top-levellogs: {upload: true}, never inside
config. A status carrying a newdiagreport (failed paint, brownout,
panic or watchdog reset) writes an error row to Events, and by default
pulls the log up on that same wake; the "Collect a device's log after a
failed paint" switch in Settings › Server turns that off.POST /logwith
Content-Type: text/plainstores a batch (64 KB cap,413above) under
data/core/device_logs/<device_id>/, keeping the newest 20 uploads up to
1 MB per device; JSON log lines keep their single Events row. Deleting the
device deletes its logs. -
Touch off during quiet hours (#327). A new quiet-hours switch, off by
default, on the app-level window and on a device's override (touch panels
only). Inside the window, the REST status response reports
touch_enabled: falsefor that wake, so firmware v1.40.0 or later puts the
touch controller to sleep instead of keeping it awake for a tap. The saved
setting is untouched, so the first wake after the window turns touch back
on. Always-on panels and the relay config mailbox are unaffected. -
The relay can cap frames per panel per day. A relay configured with
FRAME_DAILY_LIMITanswers a frame upload past the cap with429 rate_limitedand aRetry-Afterof the next 00:00 UTC; the upload is not
stored and the panel keeps its last frame. The count rides the frame pointer
the Worker already reads and writes, so the cap adds no storage operations.
Unset means unlimited, so self-hosted relays are unaffected. Tesserae holds
uploads for a capped panel until the reset instead of retrying on every
render, and Settings → Cloud relay shows which panels are paused, when they
resume, a suggestion to refresh them less often, and a link to self-hosting
the relay. The contract gains the
rate_limitedcode. -
Xteink X4 Pro in 4-level grayscale (#335).
xteink_x4_pro_grayis the
server half of the CrossInkx4-probuild, which ships with grayscale on
and already announced that id: same panel block asxteink_x4_pro, with
the renderer overridden toesp32_gray2_bin(96000-byte 2-bpp frame).
Confirmed on real hardware by the contributor, which also confirms the
mono entry's panel geometry andportrait_flipped. -
ha_entitiescan hide the last-updated badge (#336). A new
"Show last updated" toggle (default on) drops the 'just now' / 'Nm ago'
badge, the changed-row wash and the changed count in the title row for
compact layouts.ha_entities0.8.0. -
list_widgetscan ask for less too (#257). Bridge 0.18.0 passesqand
fieldsthrough to the catalog route, and the tool text explains when to use
them.EXPECTED_VERSIONfollows, so Settings → System → MCP offers the
upgrade. -
/api/mcp/catalogcan be asked for less (#257).?q=keeps the
widgets whose key, name or full description contains every term,
case-insensitively and ignoring punctuation, and?fields=trims each entry
to the named fields (keyis always kept; an unknown or empty field list is
a 400 that lists the valid ones undervalid_fields).
A filtered response carries afilterblock with how many of the catalog's
widgets matched, so an empty list reads as "nothing matched". Without either
parameter the response is unchanged. -
KOReader e-readers as a device kind.
koreader_clientpairs a jailbroken
Kindle, a Kobo, or any other e-reader running KOReader with the new
Tesserae plugin over the REST
device protocol. The reader reports its own screen size at pairing and takes
16-level greyscale frames from theesp32_gray_binpacker, decoded on the
device, withIf-None-Matchso an unchanged dashboard costs no repaint.
Hardware entries for six Kindle and four Kobo models fix the default panel
and store listing; all are awaiting a confirmed test on real hardware. The
Kindle quickstart and the clients page now lead with this plugin; KOReader's
TRMNL plugin remains documented as the 1-bit alternative.
Fixed
-
Touch buttons on a rotated panel draw upright, and no button goes missing (#343).
On a panel whose firmware draws its own touch controls (protocol v2), the renderer
left every button, switch, slider and stepper blank for the device to fill in. Two
things went wrong. The firmware draws labels and icons in its own framebuffer
orientation, so on a panel set to 90° or 270° (a portrait-native reTerminal Sticky
used in landscape) or to a flipped mount they came out sideways or upside down and
spilled out of the button. Buttons on such a turned frame now stay out of the touch
spec and the server paints them; taps still reach them through their action, as on
any other panel. And a primitive the spec skips, such as a button with no action,
was blanked but never drawn, so it disappeared from the panel; the renderer now
blanks only what the spec carries. Switches, sliders and steppers on a turned frame
are still drawn by the device, because a server-painted one would not respond to a
tap; their labels still read sideways until the firmware learns about rotation. -
No "Switch to MQTT" on REST-only hardware (#341). The device card
offered the transport flip on every instance, but the flip only rewrites
the manifest; nothing is sent to the device. On a reTerminal Sticky, whose
firmware has spoken REST only since July 2026, taking it left the device
polling REST as before while the card badge read "HTTP" and every render
was published to the broker for nothing. The button is now offered only
where the client could follow (the generic ESP32 folder kinds and the Pi
and Pico clients); hardware SKUs on the native firmware, KOReader and
PicPak get no switch to MQTT, the route refuses a hand-crafted POST, and
an instance an older version let you switch is put back on REST at
startup. The confirm dialog and the REST transport page say so. -
ha_energy"Today vs yesterday" chart is on a clock axis (#339). The
sparkline binned Home Assistant history by sample count, and a solar
sensor that sits at 0 W all night has almost no night samples, so the
night was squeezed to nothing and yesterday's peak could land after
sunset. Both lines are now calendar days in the panel's timezone, 48
half-hour slots each, binned by timestamp with a state carried forward
until it changes. Today's line stops at now and the "now" pip rides its
last point. The two lines share an x-axis, so the sun peaks line up. -
Home Assistant history longer than a day comes back whole (#339).
ha_coreasked HA's history endpoint for a start time only, and HA ends
the period one day after the start when noend_timeis given. The
ha_energysparkline asks for 48 hours, so it only got the day before
yesterday into yesterday at the current time; yesterday's line went flat
from there and today's was a flat carry of that last value. History
requests now sendend_timeas now. The HA data service'shours
option (up to 720) was cut to its oldest 24 hours the same way. -
The Home Assistant Energy widget uses the configured timezone for its clock (#340).
Its displayed time and hour-based state now follow Tesserae's app timezone
instead of the server's local timezone. -
Typing a value into a slider's number box now counts as a change (#337).
The box next to every slider (pre-dither contrast, saturation, and the rest
of the device-page fields) copied its value onto the range input without
firing an event, and the box itself has no form association, so the save
bar never appeared and the value was never saved unless the thumb was
dragged. The range input now re-emitsinputandchangewhen the box
is edited. -
A whole-frame change no longer logs a warning (#337). A gallery photo
swap or a picture-quality edit legitimately repaints the whole panel, so
push not diverted to patchesforover_budgetand
render_settings_changednow logs at INFO; the other reasons stay at
WARNING. -
A TRMNL client's screen is now the buffer its frames are turned onto (#333).
Thetrmnl_png,trmnl_png_gray16, andtrmnl_png_colorrenderers rotate
the finished composition 90° onto the client's reported buffer when the two
disagree on aspect, so a landscape dashboard on a portrait Kindle running
KOReader is no longer served at the wrong aspect and squashed by the
client's scaler. The size a client reports on its first poll (png-width
/png-height, orWidth/Height) is recorded as the panel's native
buffer and the composition canvas is seeded to match the stored
orientation; later polls, missing headers, and zero or absurd values leave
the panel alone. Native dims guessed from the preset table never trigger
the turn. -
The first-run wizard no longer loops back to the opt-in step (#330).
The device step's Continue, Skip and Next buttons still pointed at the
online-features step from before it moved to second place, so skipping the
pairing code sent setup back through timezone and transport forever. They
now go on to the dashboard step, and Welcome's Get started now leads to the
opt-in step instead of passing over it. -
An LXC install from cloud-init can update itself again (#328). The
cloud-init cloned the newest release with--branch <tag> --depth 1, which
left a detached, shallow checkout that fetched only that one tag, so the
Edge update check failed with a bare git usage error. The cloud-init now
clonesmainin full and moves it back to the release tag. The updater also
repairs a checkout made the old way on its next check (fetching every branch,
deepening the history, and moving a detached checkout ontomainon an Edge
update), and the Stable channel now follows onlyvX.Y.Zrelease tags, not
the MCP bridge'smcp-v*tags. -
The MCP bridge stays responsive during a slow render. Bridge 0.17.1 runs
each tool on a worker thread instead of the server's one event loop, so a
render_previewthat takes several seconds no longer queues every other tool
call behind it. Tesserae now reports 0.17.1 as the current bridge. -
Canvas widgets load assets through Home Assistant Ingress. The Canvas editor
prefixed widget script imports but left root-relative stylesheet and image URLs
inside widget shadow roots pointed at the HA frontend. It now prefixes those
asset URLs after rendering, matching the Send preview. -
One hung render no longer stops every dashboard from rendering. The warm
browser renders every dashboard on a single worker. The font wait before the
screenshot had no time limit, so a font request that never answered could
hold that worker indefinitely, and every later dashboard push failed at the
105 s pool deadline with a bareTimeoutErroruntil the app restarted, while
calibration pages (which skip the browser) still went through. The font wait
is now capped at 5 s like the image wait. A render whose caller has given up
is dropped rather than run for nobody, and if the worker is still inside one
render past that render's deadline, the pool starts a fresh worker and
Chromium and moves the queued renders to it. The push history now says
"browser pool gave no result within 105s" instead of a bareTimeoutError. -
The LXC cloud-init installs the current release again.
scripts/cloud-init.yaml
cloned a tag written into the file, which was last edited at v0.71.2 and had
gone hundreds of releases stale, so anyone provisioning a container from it
got a Tesserae from months ago. It now resolves the newestv*tag from the
repo at boot and clones that, falling back tomainif the tag list can't be
read, so the file cannot go out of date again.
Added
-
The battery chart's data feed now carries millivolts as well as percent.
/devices/battery/<id>/series.jsonreported only the percentage, which is
a lookup against a discharge curve that barely moves through the middle of
a LiPo's range. Two panels shedding the same charge per day can therefore
report very different percent per day depending on where they sit on it,
which makes "percent per day" a poor way to compare one panel against
another or to chase a drain complaint. The raw reading was already being
stored on every heartbeat; it is now returned alongside, with the same
per-device calibration offset applied, and null for firmware that sends a
percentage only. -
A panel reports the colours it can dither, not only the inks it prints.
list_deviceshas always answered with the raw ink palette, and the
composing instructions have always said to design in full colour and not
flatten a layout to those inks. That does not work: the prose is an
abstraction and the palette is six named hexes, so an agent given both
reaches for the hexes and the dashboard comes out poster-flat. The mixes
are now named too, each one pair of inks at one ratio with its hex computed
from the panel's own palette, so a Spectra 6 panel answers with six inks
and eighteen blends and a BWRY panel with four and ten. Each carries a
safe_on, because a dithered colour holds across a card fill and speckles
on a hairline, which is the rule that decides where it can be used.
Greyscale and one-bit panels report none, their levels being the whole
vocabulary already.
Changed
-
A panel's
refresh_floor_sis now shown rather than ignored. 41
hardware profiles list a repaint floor for their glass, and the server read
the field nowhere, whileschema/hardware.schema.jsoncalled it a lower
bound on poll cadence "surfaced in the Settings UI" and
docs/dev/adding-hardware.mdsaid it was "enforced on the always-on path",
an enforcement removed in v0.332.0. The "?" help on the device card's
cadence fields now says what the profile lists, the Companion API reports
it aspanel.refresh_floor_s, and the schema and the hardware guide
describe the field as the profile's declaration. Nothing gates on it, and
nothing here promises a repaint rate: the declared values are authored by
protocol rather than measured, and the reTerminal Sticky lists 60 while its
own notes put a full paint at 1.2 s. -
The MCP instructions now cover craft, not just mechanics. An agent was
told how to place an element and nothing about making the result look
designed, so dashboards shipped on the renderer's fallback font, on
whatever icon weight came to hand, with sizes scattered through the markup
as raw px, and with a week of readings printed as today's number instead of
drawn. The instructions now name the bundled families by job, state that
duotone and fill icons survive the dither where the thin weights speckle,
ask for a type scale declared up front, map data to the chart form that
suits it, and give the Chart.js overrides that keep a chart legible on
e-ink. They also record what the code sandbox does not inherit: the page
theme, style and accent tokens stop at its edge, which was true and written
down nowhere. -
A new dashboard is no longer preceded by a read of the existing ones.
list_pagescarried no guidance on when it was for, so the gap was filled
with a reflexive survey of the operator's other work before any building
started. The instructions now say to go straight to the build, keeping the
cases where reading a page is right: a request that points at one, or
checking a name is free. -
A button press on a relayed panel now lands inside its awake window.
A press over the cloud relay is store-and-forward: home only learns of
it on its next relay poll, then has to render the next rotation step
and upload it before the panel stops polling and goes back to sleep.
A cold render of a widget-heavy dashboard was the part of that budget
that lost the race, and the frame then only showed up at the next timer
wake, which read as the press doing nothing. Home now keeps the steps
either side of a relay panel's current one composed while it sleeps,
refreshed every time a new frame reaches its mailbox, so the answer to
a press is a promote rather than a render. The warm compositions live
for the current step's dwell (one minute at least, thirty at most), so
the page a press brings up is never older than the frame it replaces. -
A dashboard can declare how often the panel showing it should wake.
Wake cadence has always been a device property, but the content is what
knows how often it changes: a once-daily agenda on a five-minute panel
woke roughly 288 times a day to collect 287 304s (discussion #24). A
dashboard now carries an optionalsleep_interval_s, and a sleeping
device showing it wakes on that instead of its own interval — longer
for a daily page, shorter for a transit board. Clamped to the device
kind's own min/max, because only the device knows what its firmware
accepts, and skipped entirely for an always-on panel, which is not on
the sleep grid at all. Set it in Dashboards → Wake, or over MCP with
PATCH /api/mcp/pages/<id>/canvas; leaving it empty hands the device
back its own interval, which is what every existing dashboard does.
Fixed
-
A device's firmware line no longer paints over Last seen. The
Devices table gave firmware a fixed, narrow column, and a client that
reports free text there rather than a bare version -- a CircuitPython
client reportingCP client: 0.1.0 / app API: 0.4.0-- ran straight
through it and over the column beside it (#325). Firmware now takes
the flexible column and clips with an ellipsis, with the full string
on hover; the stacked phone layout wraps it instead. A value that
isn't a bare version number also stops being given avprefix, so
that same client no longer reads asvCP client: .... -
The device page's section links work in Firefox. Clicking a link
in the vertical nav scrolled the page to the section and then also
scrolled the clicked link into view so the phone strip keeps it
centred. Two scroll requests against the same box is a browser
coin-flip: Chrome drops the second as a no-op, Firefox honours it and
cancels the first, so the click did nothing at all (#326). The strip
now scrolls its own scrollport, which desktop's sticky column never
needs. -
An anchored device section lands below the topbar, not under it.
The sub-nav's sticky offset and the scroll-margin a section comes to
rest on were both a fixed 16px, measured from the top of the viewport
rather than from the bottom of the sticky topbar, so clicking a
section put its heading behind the bar (#326). The bar's own height
moves: its nav wraps onto a second row between roughly 900px and
1100px wide, which is where this was reported. Its measured height is
now published as--t-topbar-hand both offsets derive from it. The
highlight follows: the scroll-spy probe used a quarter of the viewport
height, which on a short window sits above the line a clicked section
lands on, so the pill above the one you clicked lit up instead. -
Deck navigation now reaches panels paired through the cloud relay.
Stepping a deck served the new page instantly over REST but left a
relayed panel on the previous image, because a step that finds its
frame already warmed swaps it into the live slot without going through
a push, and uploading to a panel's relay mailbox hangs off the push.
The swap now tells the delivery listeners, so the selected frame is
sealed and uploaded like any other. This covers every cached step, not
just a button press: timed deck advances, the return to the home page,
touch navigation, the companion app and the Decks page controls were
all affected. Warming stays silent, and a step back to an identical
frame still uploads nothing. -
The NASA APOD widget no longer shows a half-loaded picture. It
asked for NASA's HD file, which is routinely several megabytes, and
the renderer only waits a few seconds for the network to settle before
it screenshots whatever has arrived. It now takes the standard
~1000px image, which is larger than any supported panel anyway, and
falls back to the HD file only when NASA offers nothing else.