Added
-
A widget can say the least Tesserae it needs.
tesserae_minin a
plugin's manifest and on its catalog entry ("0.463.0"). A host at or
above this version refuses to install or update past it, says so on the
Browse card and in the plugin list, and leaves the installed version in
place. Older hosts ignore the field, so a widget that uses it still has
to load there and say what it is missing. -
Artwork: pictures a widget makes and keeps, once for every widget.
app.artwork.Collectiontakes the function that makes one picture and
does the rest: the store with earlier versions and panel-converted copies,
a budget in US dollars at the models' listed prices, the queue and its
workers (resumed after a restart), the hold while a dashboard is being
edited, what each dashboard asked for, the job tray cards, and an admin
page at/plugins/<id>/artwork/<name>/with Render now, Paint the held,
Purge, a live ticker and per-picture Repaint, Delete and versions.
app.falis the one fal.ai client behind it. Widgets get
ctx["cell_id"]andctx["settled"]so the hold no longer guesses from
a timer. See "Pictures a widget makes and keeps" in the widget docs. -
History folds follow-ups under the send they finish. A page sent
again once its paintings landed now sits under the original push as
small dots on the rail, with its outcome and timing, and one line says
when the finished page reached the panel. Deleting the send deletes
the chain; the follow-up chip still filters to them on their own. -
Widgets can be run ahead of a scheduled send. A widget whose work
takes longer than a fetch may (an AI painting) declares
render.warm_ahead_sin its manifest, and the scheduler runs its page
that long before each projected send (schedules, rotation steps, the
page's own refresh cadence), polling until the page stops asking for a
refresh, so the panel paints the finished page once. The lead learns:
a widget says how many pieces of work are pending (live_pending), the
server works out seconds per item from how long the page took to settle,
keeps a typical count per send, and sizes the next lead from the two,
30 s to 15 min, asking for more after a warm that ran out of time.
Nothing reaches a panel from a warm. -
A page still painting is sent again when it is done. A widget that is
waiting on something (an AI painting in flight, say) already tells the
editors to fetch it again in a few seconds throughlive_refresh_s. A push
now honours the same hint: the composed page carries it as a<meta>, the
capture reads it back, and the push pipeline sends the page to the same
devices again after the wait, up to eight follow-ups in a row, one pending
follow-up per page. A scheduled send of an Overhead or Specimen Sheet page
no longer leaves the panel showing the page as it was caught mid-work.
Fixed
-
A big picture no longer reaches the panel as an empty box. The
renderer waited for every image to load, not to decode, and Chromium
decodes a large one (a widget's own data-URL JPEG, a camera frame) after
the load event; a frame captured in between had the picture's box empty.
The image wait now decodes each loaded picture and lets a frame through
before the screenshot, and says in the log when a picture would not
decode or the wait gave up. -
Dashboard previews no longer show oversized fonts and icons on large
panels. Previews were laid out at an 800px-capped size, so a 1200x1600
panel was laid out at 600x800 and its widget text and icons looked twice
too big once shown at panel size. Previews now lay out at the panel's real
size, exactly as a push does, and the dashboards-list thumbnail is shrunk
afterwards. The editor's Panel view also renders each bound panel at its
own size instead of the largest panel's. Covers the dashboards list, the
decks lineup and the companion app's/dashboards/<id>/preview. (#364)
Added
- Custom CSS on a dashboard (#366). The dashboard editor's Appearance
section has a Custom CSS box. Its rules apply to the page and, since each
widget renders inside its own shadow root, are copied inside every widget
on the dashboard as well, so[data-widget="weather_now"] .w-title { display: none; }hides that widget's title row. The live preview picks
the box up like any other field, and a closing style tag in the text is
neutralised on save. Documented under Dashboards in the docs. - A button of a job's own in the job tray.
jobs.reporttakes
action=(label, callable): the card shows a button with that label and
pressing it runs the callable throughPOST /jobs/act. The Specimen Sheet
uses it for "Paint now" on the paintings its editor previews hold back. - Rolling weeks and wrapped titles on the month widget.
calendar_month
has a new Rolling weeks option: the calendar month as before (the
default), or 1 to 6 weeks starting at the week that holds today and
rolling forward week by week, so late in the month the grid shows what is
coming instead of the past. The title follows today's month, so the days
of the next month fade as they do in the month view, and the 1st of a
month shows its month name (1 Nov). A new Event title lines option
(1 to 4, shown with the text event style) wraps event titles onto that
many lines instead of cutting them at one; the+Nchip stays visible
however tall the titles get. - Examples on a textarea option. A widget's
textareacell option can
carrypresets([{label, value}]): the editor shows an Examples
dropdown above the box, and picking one fills the box with that text to
be edited from there. The Specimen Sheet's scene uses it.
Changed
- Paper is the default design. New installs, and installs that never
touched the design switch, now get Paper; the switch in the header (and
the phone menu) goes back to the classic design in one click, and an
install that already chose classic stays on it. Opt-in since 0.437,
Paper was chosen by about half of the installs that could see it and
nobody who tried it went back. The heartbeat'sui_chosenflag tells a
switch back from an install that never saw the other design.
Added
-
Live previews while a widget has background work. A widget whose data
carrieslive_refresh_s(2 to 120 seconds) is fetched again by the page
editor's preview and the Panels editor after that long, and the cell
repaints when the data changed, so pictures and cut-outs appear as they
finish instead of on the next edit. Polling stops when the field goes, the
cell leaves the page, or the tab is hidden (documented in the widget guide). -
Background job tray. Anything working in the background now shows a
progress card at the bottom right of every admin page: a title, a detail
line, an animated bar (a sweeping block when the fraction is unknown), a
Stop button for jobs that can be stopped, and a check, warning or stop mark
when it ends. Jobs live in the server, so the tray carries across page
navigation without replaying its entrance; it folds to a "2 jobs running"
pill and stacks above the agent toast. A widget'sserver.pyreports with
from app import jobsandjobs.report(source, key, title, progress=..., detail=..., stop=...), thenjobs.finish,jobs.failorjobs.cancel
(documented under "Background jobs" in the widget guide). Polled at
/jobs/jobs.json, quick while a job runs and paused in a hidden tab. -
Bloomin8 Canvas support (#344). A new device kind speaks the Canvas's
own Schedule Pull API: point its upstream URL at<tesserae>/api/bloomin8
with the token from the device card and it pulls the latest frame as a
full-colour JPEG on each wake, sleeping until the time Tesserae gives back.
Wakes follow the page's schedule within the configured interval, a frame
the Canvas already painted isn't sent again, and the battery level it
reports shows on the device card and in Home Assistant. Built against
Bloomin8's spec and not yet tested on a Canvas; see the new quickstart. -
Widget options that only show when they apply. A widget can mark an
option withshow_if(for example, "only for the cluster layout") and the
grid editor and the canvas Configure drawer hide it until it applies,
updating as you change the other options. Hidden options keep their value.
See "Fields that only apply sometimes" in the widget docs. -
Upload a widget from the Widgets page. "Upload widget" takes a
.zipor
.tar.gzof a widget folder and installs it into the data folder beside
Tesserae Studio pushes, so a widget written for one install needs neither
the catalog nor file access (the Home Assistant App keeps its data where
Samba and SSH can't reach it). It is usable straight away; one with an admin
page asks for a restart. A zip made with Finder's Compress works as is.
Uploaded and pushed widgets get a Remove button on the same page. -
Yesterday's solar total on the HA energy widget (#362). A new switch,
"Show yesterday's solar production", adds a chip with yesterday's total next
to today's, read from the history of the daily solar-energy entity. Off by
default, so existing widgets look the same until it is turned on. -
Heartbeat: was the design chosen? The daily heartbeat's
uifield
(classic or Paper) gainsui_chosen, true when somebody set the design by
hand rather than leaving the default. It lets a later change of default be
measured by people switching back, instead of by who happens to be on
which design. Privacy doc updated. -
What panel is this for? Making a dashboard on an install with no panel
registered now asks for the size to design at: the common panel shapes
(7.5" landscape and portrait, 4.2", Inky Impression 4", M5Paper, 6"
e-reader, 10.3", 13.3" both ways). A grid dashboard keeps the chosen size
as its own panel and a canvas dashboard gets it as its artboard, instead of
the server's default panel or 600×400. Binding a device later switches it
to the device's size. -
Server name and colour (#350), for anyone running more than one
server. Settings → Server → This server takes a short name and a colour:
one of five presets (the Spectra 6 inks toned to match the admin's red,
each with a lighter step for dark mode) or a custom colour. The colour
paints a 4px stripe across the top of every admin page and the canvas
editor, tints the tab icon and the phone's toolbar, and becomes the
accent, in Paper and the classic design alike. The name shows as a chip
under the wordmark and leads the tab title (dev · Dashboards), and the
companion API reports it as the instance name. Nothing changes until one
is set, and panel renders never show either. -
Touch wake for the reTerminal E1003 and reTerminal Sticky (#327): a
third touch setting next to Touch input and Touch linger. Any tap keeps
the touch controller scanning through deep sleep as before; Double tap or
swipe parks it in its gesture mode, about 1 mA instead of several, where a
single first tap does nothing, a double tap or a swipe wakes the panel, and
taps then work as usual while it is awake. Sent to the device as
touch_wake(taporgesture) in the status config block; needs device
firmware 1.44.0 or later, older firmware ignores it. -
Colour e-readers (E Ink Kaleido 3). A KOReader reader that pairs with
gamut: "kaleido3"(the plugin announces it for a Kobo Libra Colour or
Clara Colour with colour rendering on) is served a 24-bit RGB PNG with
every channel dithered to 16 levels, 4096 colours, through the new
kaleido_pngrenderer;/framesaysformat: "png"and the plugin
blits it in colour. The gamut is accepted at register and discover, the
editor's Panel view previews it, a reader that first paired as greyscale
moves over when its plugin re-pairs with the new gamut (and back when
colour rendering is turned off), and hardware entries for the Kobo Libra
Colour and Clara Colour list the renderer first. Not yet tested on a
colour reader. -
Credits name esptool-js, which Tesserae Cloud's setup page uses to flash
firmware over USB; docs/credits.md and the cloud's /credits are built from
the same list. -
Search, filters and sorting on every list. One shared toolbar (a
search box, one or two filters, a count) now heads Settings › Devices,
Dashboards, History, Events, Lineups, Widgets › Installed, the Themes
strip, Rooms, Firmware, Cloud relay remote panels, Companion app paired
apps, Device batteries, the Stats display list and System backups. Table
columns sort from their heads (ascending, descending, then back to the
list's own order, with a caret on the sorted column); lists without
column heads get a Sort menu. Numbers, sizes and times sort by value, and
the last sort is remembered per list in the browser. History and Events
filter the rows the page loaded and say so; their server-side filters are
unchanged. Live rows (Events, the Lineups refresh) join the current
search and sort as they arrive. -
Paper's sidebar folds to an icon rail. A Collapse button at the foot
of the desktop sidebar narrows it to 64px: the logo and one icon per
item, each named by a tooltip, with Widgets opening as a flyout to the
right and the theme and design switches as icons. The choice is
remembered in the browser and applied before the page paints; the page,
the Settings column and the batteries pill follow the new width. The
phone drawer and the classic design are unchanged. -
Paper, an opt-in redesign of the web UI. A switch in the header
("Try the new design"; in the phone menu as "New design") turns it on for
the install, and the same switch turns it off again. Paper moves the
navigation into a left sidebar on desktop (a left drawer on phones),
swaps the teal for warm paper, ink and a red accent, uses Archivo and
Martian Mono (already bundled), flattens shadows and gradients to 1px
hairlines, recolours the Tesserae mark, and adds a light page-enter
animation that switches off under reduced motion. Dark mode follows. The
classic design is unchanged and stays the default. With online features
on, the daily heartbeat now says which of the two designs the install
runs (ui), so adoption can be counted; see docs/privacy.md.
Changed
-
Dashboards are grouped by display again, inside the table. Each
display gets a full-width group row with its status dot, name, size,
what it is showing, a count and a link to its device page, with "Not on
a panel" last. A dashboard on several displays is listed under each, and
bulk select and the count take it once. Groups fold one by one
(remembered) or all at once from the toolbar, and search, the Panel
filter and Active / Archived work across them. The Panel column is gone
since the group names the display; a dashboard also on other displays
shows "+N panels". -
The canvas editor is laid out again, and works on a phone. It now
follows the app: the Paper or classic design, and the app's light, dark or
system theme (its own dark-mode button is gone). In Paper the app's icon
rail sits on the left. The left column is one sheet with Layers, Add and
Widgets tabs; the old Appearance card becomes the inspector's Page view,
shown while nothing is selected. The inspector folds an element's fields
into Content, Position and size, Style, Data and Touch sections that
remember whether they are open, and moves Front, Back, Duplicate and Delete
into its "…" menu. The agent becomes a one-line strip above the inspector
(what it is doing, steps, how long it has been quiet) that opens onto the
steps, the nudge box and Follow; the toolbar pill reads "Agent working".
The selected element shows its name and size above it, the zoom control
gains a Fit button, a tap on an Add tile drops it in the middle of the
canvas, and the less-used toolbar buttons fold behind More on narrower
screens. On a phone the "Best on a bigger screen" notice is gone: the
canvas fits the width, a finger selects and drags elements, pans, and
pinches to zoom, and a bottom sheet with Add, Layers, Element and Agent
tabs opens half way or folds down to its tabs, with the agent floating
under the canvas while it works. The code editor opens full screen there. -
The canvas editor, the MCP API and the template marketplace are now
regular features, on by default. Settings › System › Experiments becomes
a Features card with the same three switches, so any of them can still be
turned off, and a feature someone already switched off stays off.
TESSERAE_EXPERIMENT_<NAME>=0still turns one off for a whole deployment.
The canvas editor drops its "experimental" label and first-open notice; it
is reached from Dashboards by creating a Freeform canvas dashboard or
opening one. The template marketplace still needs Online features. The MCP
API still needs its token from any other machine, and a request relayed
through a reverse proxy (or carrying a forwarding header) now needs the
token too, since a forwarded127.0.0.1could otherwise pass for a local
caller. The admin pages only watch for agent activity once an agent has
connected, and the heartbeat'smcpflag now means an agent has connected
rather than that the API is enabled. The calibration page's "Experimental:
edge handling" section is now just "Edge handling". -
The Dashboards list is a table. It works like Settings › Devices: a
toolbar with search (name, panel or id), a Panel filter (each display, or
"Not on a panel"), the Active / Archived switch and a count; one row per
dashboard with a status dot (solid while a panel shows it, hollow while a
lineup holds it), a thumbnail, its panel, size, update and wake cadence,
last push and state. A row opens in place onto a larger preview, four
figures (on the panel, last pushed, updates, lineups) and its controls:
Updates, Wake, Push, settings, Duplicate, Archive or Restore, Delete and
Open in editor. Bulk select gains a select-all box for the rows shown. The
per-display groups give way to the Panel column and filter, and the create
form now opens from New dashboard beside Send now in the page head. Columns
drop as the table narrows (Wake, then Updates, Size and Last pushed), rows
become two lines on phones, and nothing scrolls sideways, in both designs. -
Paper follows the approved mockups more closely. Sections, tables,
stats and empty states sit on raised sheets (a lighter surface with a 1px
edge and 2px corners) on a slightly darker page; Settings gets a second
sidebar for its sections on wide screens, with labels on the left and
controls on the right; sidebar items are square and the selected item is
a deeper shade with no accent bar; a small red square before each section
title is the one splash of colour; and every form control is one height
(40px, 44px on phones, 32px inside list rows).
Fixed
-
A page's Auto-update lands on the clock. A page refreshing every N
minutes now renders once per N-minute slot of the wall clock, just after it
starts, and the scheduler checks on the clock rather than 30 s after its
last check finished. Before, each refresh came N minutes after the previous
render ended, so a 1-minute clock page crept a few seconds every minute and
skipped a minute every few. -
Touch-v3 labels draw as text, not as boxes (#360). The server packed
touch atlases the overlay's way (0 = black) while touch-v3 reads 0 as paper,
so every button label showed as white letters in a black box. Touch atlases
are now packed ink over paper as the atlas schema says, and a server that
had the old ones cached rebuilds them once. -
Glyph atlases are built ahead of time and kept (#359). The render prune
deleted them every 50 renders, so the next device poll went out without
label text until a rebuild. They are now spared, keyed on an atlas format
number and the Tesserae version, built at startup (touch-v3) and right after
a render (overlay sizes), and a device request never builds one inline. -
The Docker image and Home Assistant App start on older x86-64 CPUs
(#361). numpy 2.4 needs an x86-64-v2 processor, so a pre-2009 CPU, or a VM
given a basic CPU model such as Proxmox'skvm64, stopped at "numpy failed
to import". The image pinsnumpy<2.4, and the startup check now names the
CPU cause or prints numpy's own error instead of "the image is broken". -
Picture, NASA APOD shows the real picture again. NASA moved APOD to
science.nasa.gov on 2026-09-29, and theapi.nasa.gov/planetary/apod
route the widget used now answers every date with the NASA logo. It now
readsscience.nasa.gov/wp-json/wp/v2/apod-basic, which needs no API key
and has no quota, so the NASA API key setting is gone (a stored key is
ignored). One request for the last 14 entries replaces the day-by-day walk
past videos, falling back to a video's still frame when no image is in
reach; the HTML credit is flattened to text and shown without a © mark,
since it mixes agency credits with personal copyrights; the image is
requested at most 1200px through the asset host's resizer; and the
entry's alt text is carried to the<img>. The NASA APOD credit now links
to science.nasa.gov/apod. -
History's Clear history control no longer asks "Delete the selected
history?" when it is about to delete everything older than the chosen
cutoff; the confirm now names the cutoff. With rows ticked, the same
button deletes the selection instead of reporting that it deleted 0
entries (#346). -
Settings › Rooms: the "widget isn't installed" notice drew the dark
theme's text on a fixed cream card; it now uses the warn tokens in both
themes. Its Install Room Status button goes to the catalog with the
search prefilled, where the widget can actually be installed, instead of
the installed-widgets settings; the calendar prompt does the same when
the calendar connector is missing (#346). -
Settings › Themes says that these are the palettes dashboards render in
on the panel, and that the admin UI's own light and dark mode is the
sidebar toggle, after a report that picking one was expected to switch
the UI (#346).
Changed (bridge)
- Settings › System › MCP now expects the
tesserae-mcpbridge 0.18.1, whose
messages no longer mention an "mcp experiment";pipx upgrade tesserae-mcp
picks it up.
Security
uv.lockresolves Pillow 12.3 (and pillow-heif 1.8), urllib3 2.8 and
Werkzeug 3.1.9, which carry the fixes for the advisoriespip-audit
reported against 11.3, 2.7 and 3.1.8. The version caps were already lifted;
the lock had kept the old versions. Every renderer and every dither mode on
every gamut renders pixel-identical output on Pillow 11.3 and 12.3. The
pip-auditstep in CI is now a hard gate (#142).- A client could set
X-Forwarded-Forto pass for this machine or for
the local network. From the network, claiming127.0.0.1opened the
pages kept for the built-in renderer (/compose/, the theme
stylesheets) without signing in; from the internet, claiming a LAN
address got past the network check on installs with the password
turned off. Only a reverse proxy on this machine or the local network
may now say who the real client is, and "this machine" means a direct
connection with no forwarding headers.