Files
app/docs/interface/dashboards.md
T
stroblmeandClaude Opus 5 6d84316ce5 Bound a panel credential to its own widgets, and let one screen be re-paired
Three things a paired wall panel needed.

The scope check now walks the panel's widgets instead of allowing the
`/messages/` prefix wholesale: a screen may publish what its own controls and
querying charts point at, read the history of what its tiles draw, and nothing
else — the catalogue of every message in the installation included. The same
walk that already bounds its socket, so both surfaces agree.

Pending pairing codes moved out of the per-process dictionary into Redis, keyed
per code with the code's own TTL and indexed in a zset so the fifty-code cap
means the same thing to every worker. Without a Redis there is one process by
definition, and the dictionary stays.

And a per-panel nonce in the token, bumped by `POST /panels/{id}/unpair`: that
refuses the screen hanging there without touching the panel, its dashboards or
their arrangement. A save cannot write the nonce back, so a stale client cannot
undo a revocation. Only for a credential this installation signed — one the
portal minted carries no nonce and is revoked at the hub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tULRZJUkZsw7rMJ3h4xvu
2026-08-22 11:57:37 +02:00

6.5 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 a reading, optionally nesting up to three inside it
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
Clock the time, in a size a wall can read

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
Input text or a number free entry
Dropdown one of a list a mode, a scene, a preset

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.

That indirection is the point. The widget never learns which database answered it, so swapping the store is a change to one flow and nothing else. The answer also states what it was computed for, so an answer to a different question is ignored rather than two charts overwriting each other's picture.

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.
  2. Point the device's browser at the link the dialog shows. The device then displays a six-character code.
  3. 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.

It 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.

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 installation'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 installation enrolled with a portal shows a second link, https://hub.${DOMAIN}/i/{installation-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 installation, on every call, by the same check a locally paired screen passes. Two differences: it acts as the account the installation 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 installation signed, does not reach a remote screen. Revoke that one at the hub.

See also