Files
app/docs/interface/dashboards.md
T
stroblmeandClaude Opus 5 d471614e6a Push a frame instead of storing and fetching it
The rate the media dtypes could carry was one frame every second or two: each
was a file on the data volume, an event on the socket, and a request back for
the bytes. This closes both halves of that, and they are one feature.

`save_artifact(..., volatile=True)` writes to a `VolatileStore` — the same
content-addressed store, in `/dev/shm`, bounded by size with the oldest falling
out (`ARTIFACT_VOLATILE_BYTES`, 48 MB under the container's raised `shm_size`).
Nothing sweeps it: a frame nobody kept is not worth walking the store to find.
`ArtifactStore.path` falls through to it, which is what lets a volatile frame be
an ordinary reference everywhere else — the dtype check, a panel's digest scope,
`load_artifact` in a node, and the widget's own fetch all work on one unchanged.
`adopt` copies one into the store when a run records it, so "returned media is
kept, emitted media is not" stays true.

The bytes then go down the flows websocket as a length-prefixed binary frame,
sent just ahead of the `message_value` naming them, so a tile has the frame when
it hears the value moved. Nothing is pushed unasked: a client names the messages
it is drawing (`{"type":"media","names":[…]}`), a panel's list is intersected
with the scope it already had, and only the newest frame per name in a batch is
sent — a client that fell behind is not handed frames it would draw over. The
tunnel relays text only, so a screen reached through a portal falls back to
fetching, which is why the rate table now has two rows.

Around the edges: the remote worker's fetch cache is bounded at last
(`FLUKSIO_ARTIFACT_CACHE_BYTES`), since content addressing means nothing in it
ever expires and a media stream fills it with chunks nothing asks for twice; a
port carrying an image draws the frame in the node panel rather than only
saying `image/png · frame.png · 1.79kB`; and an edge chip says that much instead
of a line of hash. The media screenshot stops waiting for `networkidle` — a
camera is a socket that never goes quiet, which is the point of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YC4u66vjzW54fnHu5Juhh9
2026-09-02 10:15:14 +02:00

14 KiB

Dashboards and panels

A dashboard is a grid of widgets bound to message names, the same names that wire the graph. It is its own document rather than a set of nodes, so it reads across flows without belonging to any of them, and a flow stays the logic it was.

There is no second tool and no separate deployment. You build the panel next to the thing feeding it.

Building one

Dashboards → New dashboard, then drag widgets onto the grid from the widget picker and bind each to a message.

Like flows, dashboards are drafts until you publish. A wall panel reads only the published version, so a half-arranged page never reaches the wall.

On a phone the dashboard is read, not arranged: the grid stacks to one column and dragging is off. Picking a widget and editing its settings still works.

The widgets

Display

Widget Binds to Notes
Value anything a formatted reading with a unit and a precision
Gauge float, int min, max, unit
Chart float, int up to five series; see Two kinds of chart below
Bar float, int up to eight readings, a row each, with a scale per row
Icon numbers, booleans, weather strings maps a value onto a glyph
Text markdown you write; a label, a note, an instruction
Agenda list upcoming items, e.g. from a calendar connector
Forecast list a short outlook strip
Notification record title, body and severity — what an alert channel writes
Media image, audio, video a camera frame, a clip; see Media tiles below
Player record what a streamer is playing, with its transport; see Player tiles below
Clock the time, in a size a wall can read

Every widget carries a title, and Show title decides whether the panel draws it. Turned off, the tile is just the reading, which is what a row of gauges under one heading wants. The title is still the widget's name: what a screen reader calls its controls, and what a published value is labelled with.

Input

Widget Publishes Notes
Button a fixed value one-shot: run it, open it, reset it
Switch bool on/off
Slider float, int min, max, step; Drawn as makes it a vertical fader
Input text or a number free entry
Selector one of a list a mode, a scene, a preset — as a menu, or as a row of choices with the active one held
Colour [h, s, v], [r, g, b] or "#rrggbb" a hue wheel with saturation and brightness, for an RGB fixture

The colour wheel sends [h, s, v] by default (hue 0-360 degrees, saturation and value 0-100 percent, which is what a DMX encoder expects) and its Sends setting switches that to [r, g, b] (0-255 each) or to a "#rrggbb" string, because fixtures differ. The first two bind a list message, the third a str.

A control publishes the message it is bound to, exactly as a node would. On the flow canvas it is drawn as a labelled endpoint feeding the nodes that read it, so nobody has to guess where the value came from.

Typed bindings

Widgets are type-checked against the message the same way ports are: a switch takes a bool, a gauge takes a number, an agenda takes a list, a notification takes a record. Bind one wrong and the editor says so rather than drawing nothing. The same table is enforced on the server.

Charts are the exception worth knowing: because identity across five series is carried by lightness alone, a chart with more than one series always draws a legend.

Two kinds of chart

Live. Bind up to five messages and the chart draws the engine's own ring buffer of recent values, extended over the socket as new ones land. Nothing else is involved. This is what you want for "the last few hours of the living room".

Querying. The chart publishes a request (the window and resolution it wants) exactly as a slider publishes a value, and draws the series some flow answers with. What serves that request is the flow's business: typically a small Python node that builds a Flux query, an InfluxDB node that runs it, and another Python node that shapes the rows.

The widget never learns which database answered it, so swapping the store changes one flow and nothing else. An answer states what it was computed for, so an answer to a different question is ignored.

!!! note "Drop the bucket that is still filling"

The request carries a window and an interval, and the binning is the flow's
own work, so the newest bucket only ever holds the part of an interval
that has elapsed. Drawn, it reads as a fall that never happened. The
engine's own [Activity charts](index.md#activity) end on the last closed
bin for that reason; a flow answering a chart has to drop or hold back its
newest bucket the same way.

Media tiles

A media widget draws what its message points at: a picture, a clip with controls, a video. Media does not travel as a message; a reference to it does. The tile takes the bytes behind whichever reference the message holds, and redraws when a new one arrives.

Crop or fit decides how a picture fills the tile. Play as it arrives starts a clip by itself, though a browser only plays sound once somebody has touched the page, so a screen nobody has tapped stays silent.

Rate is the thing to get right, and it is decided by the node publishing rather than by the tile. A frame the engine is holding in memory is pushed down the same websocket that carries the value, so ten a second is a real view on the local network; a stored one is fetched, a round trip each, which suits a glance at a door every second or two. Through the portal everything is fetched, so make it every few seconds there. Above that, put the camera's own address in Live stream and the browser plays it from source, leaving the messages to carry the occasional still that a flow can actually react to.

Panels see media the same way, and only their own: a screen is sent, and may fetch, the bytes its own tiles are showing and nothing else.

Player tiles

A player is the one tile that both reads and publishes, so it has two bindings. It shows a record describing what is playing and publishes to a str carrying what to do about it:

Field of the record Means
title, artist, album what is playing
status play, pause, stop, load, or off for a streamer with no power
position, duration seconds, both

The buttons and the bar publish words: toggle, next, prev and seek:<seconds>. Those are a streamer's own vocabulary rather than this app's, which is what lets one tile drive whatever is on the other end: the node that receives them decides what they mean for its device.

The position counts forward in the browser between readings, so the bar moves at one second while the device is polled at whatever rate suits it. Every reading that arrives is taken as the truth and the count restarts from it.

Dashboard settings

Most of what a dashboard carries is a widget: a tile bound to a message. A handful of things are not, because they belong to the whole surface rather than to any tile on it, and a screen bolted to a wall has nobody standing at it to set them.

Setting Is Driven by
Look Fluksio, Material or Glass a str message
Theme System, Light or Dark a str message
Palette the dashboard's colours, in order a list message
Background the URL of an image a str message
Lock read-only on or off a bool message

They all work the same way, and both halves are optional:

  • Just a value. Set Theme to Dark and that dashboard is dark wherever it is shown, whatever the device or the browser prefers. This costs no flow at all, and it is what most wall panels want.
  • Driven by a message. Pick one in Driven by and a flow takes the setting over, exactly as it drives a tile. The value you set stays the fallback: what the dashboard uses before the first message arrives, and whenever the message is silent.

A bound setting is type-checked like a widget binding, so a Theme pointed at a float is refused by the editor and by the server. The panel's credential is extended to it, so a paired screen may read its own theme message and nothing further.

There is no schedule field. A schedule is a node publishing to the bound message: an inject with a cron expression feeding a change node that maps the hour onto a palette is the whole of "warmer after sunset". The same channel drives anything else, including locking a panel down remotely.

Look

Fluksio is the app's own design (--card surfaces separated by a hairline and a low shadow, pill controls, one slate-blue accent) and is what a dashboard wears until you choose otherwise. Material lays flat, tonal cards on a plain ground. Glass floats translucent tiles over a soft, slowly moving one.

They are three complete sets of components, not three stylesheets, but they are the same dashboard: every widget, every control and every keystroke behaves identically, so switching look never changes what a panel can do.

Palette

A palette is an ordered list of colours, and position is the role:

# Role
1 the ground the dashboard sits on
2 the surface a widget is
3 the primary — fills, active controls, the first chart line
4 the accent — the second chart line
5 text
6+ further chart colours

Paste a coolors.co link (or a list of hex colours) and the whole dashboard is recoloured: every widget, the rail and the charts. Leave the later roles off and they are worked out from the ones you gave, so three colours are a whole dashboard. Rotate turns the list when the roles landed in the wrong order. Text that could not be read on a surface is replaced with black or white there, so no palette can produce a line nobody can see.

While a palette is set, Theme is idle: the first colour is the ground, so whether the panel reads light or dark is already decided by the palette itself. Fault and success keep their own colours in every palette, so a failure is never paintable as a reading.

Background

An image drawn under the widgets, covering the canvas. It replaces the ground the Glass look brings with it. Bound to a message, a flow decides the picture.

Lock

Lock is a read-only surface, not a permission. The controls stay visible, stop publishing and read as disabled, and the panel says Read-only in the corner. What a paired screen is allowed to reach is still decided by its own credential, below.

Showing one

  • /view/{name} — a browser tab pointed at one dashboard. Needs an ordinary session.
  • Panels — a named device that pairs instead of logging in. Below.

Panels: hanging a screen on a wall

A wall tablet has no keyboard, so it pairs.

  1. Dashboards → Panels, add a panel named after where it hangs, and tick the dashboards it shows. More than one and the screen draws a rail to switch between them, with each dashboard's own icon on it.
  2. Tick Touch friendly if the screen is touched rather than pointed at. Controls and the rail grow to a finger's size — the rail gets taller without taking a wider column, so the arrangement does not move. It sits here rather than on a dashboard because it describes the screen: the same dashboard may also be open in a browser with a mouse. A phone gets it anyway, from its own width.
  3. Point the device's browser at the link the dialog shows. The device then displays a six-character code.
  4. Type that code into the same panel's Pair device field. The line under it names what is holding the code. Check it is the screen you just hung, because approving adopts whatever answered. The screen picks the credential up within a few seconds and never asks again.

What the screen holds is not a login. It reaches that panel's published dashboards and the messages its own widgets read or publish, and nothing else. A message no tile on it draws is refused in both directions.

The credential cannot be made strictly read-only, and that is honest rather than an oversight: a querying chart publishes its request, and a control on a panel is the reason you put one there. The dashboard's own Lock setting above stops its controls publishing and can be driven by a flow, which is how you quieten a screen remotely, but that is the surface behaving, not the credential being narrowed.

Deleting the panel revokes the credential and the assignment together, which is how you retire a device and what it showed. Unpairing revokes only the credential: the panel, its dashboards and their arrangement stay exactly where they are, and the screen falls back to asking for a new code, which is how you swap the device out without rebuilding what hangs there.

!!! note "If the link is wrong"

The pairing link is built from the instance's `FRONTEND_HOST`. If that
is not the address devices on your network actually reach, fix the setting
rather than the link: it is the same one password-reset mails and the OAuth
metadata are built from.

A screen somewhere you cannot reach

Another building, someone else's network, no route in. An instance enrolled with a portal shows a second link, https://hub.${DOMAIN}/i/{instance-id}/panel, and the same three steps work through it: the portal serves that one page without a session, forwards the pairing calls down the tunnel, and mints the credential when you approve the code. The pairing line then reads via portal.

The portal names the panel and nothing else. What the panel may read is decided on the instance, on every call, by the same check a locally paired screen passes. Two differences: it acts as the account the instance was enrolled with rather than as whoever approved it, and deleting the panel stops it here immediately while the portal's copy of the token expires on its own, which is also why unpairing, which works on a credential this instance signed, does not reach a remote screen. Revoke that one at the hub.

See also