Docs / docs (push) Successful in 30s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m7s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m54s
pre-commit / pre-commit (push) Failing after 4m24s
Test Backend / test-backend (push) Successful in 3m8s
Compose Smoke Test / test-compose (push) Successful in 40s
Playwright Tests / merge-reports (push) Successful in 1m33s
A port may now declare `image`, `audio` or `video`. Each is the artifact
reference the engine already had, narrowed by the `media_type` on it, so a
speech recogniser declares what it eats rather than taking any bytes at all and
finding out. Bytes still never travel as a message and nothing on the wire
stops being JSON: a camera publishes one reference per frame, a microphone one
per chunk, and a reference may carry a `meta` dict nothing here interprets.
Streaming media is therefore an ordinary streaming port — with one change to
what that means. An emission used to journal an item with no payload, so
downstream read whatever was current when the item was claimed; a consumer
slower than its producer saw only the newest chunk and the ones between were
lost. That is right for a training curve and wrong for a second of speech, so
an emission now journals a `kind="emission"` item carrying its values, and the
executor hands them to the nodes reading that message instead of writing them
to state again. The value in state stays the latest, which is what everything
else reads, and the wave is filtered by what actually changed rather than
walking everything reachable. No queue serialization change — the existing
`outputs` field carries it.
Continuous media makes the store's missing GC a real problem, so this closes
it: `sweep_artifacts` runs hourly, keeps every digest a `run_artifact` row
records or a live message holds, spares anything written in the last hour, and
stands aside entirely while a run is in flight, since a node may store a
checkpoint long before it returns the reference to it. That also collects the
orphans a deleted flow has always left behind. `ARTIFACT_GC_INTERVAL_S=0` turns
it off.
Around the edges: `GET /artifacts/{digest}` serves the media type the caller
passes and answers ranged requests, so a browser plays a clip rather than
downloading it; `PUT` spools to disk instead of holding the whole body in
memory, as does `save_artifact` given a path; a Media widget draws whatever its
message points at, and a wall panel may fetch the bytes its own tiles are
showing and nothing else; and a connector gets `save_artifact`, for a device
whose readings are bytes.
What this cannot do is live video: a frame every second or two is a glance, and
the honest answer above that is the camera's own stream, which the widget takes
as a URL and the browser plays from source.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
271 lines
12 KiB
Markdown
271 lines
12 KiB
Markdown
# 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 |
|
|
| **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 |
|
|
| **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.
|
|
|
|
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.
|
|
|
|
## 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
|
|
— so the tile fetches 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. A frame every second or two is a glance at a
|
|
door, and works; through the portal, make that every few seconds. Live video is
|
|
not something to push through the message plane at all — 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 may fetch the bytes
|
|
its own tiles are showing and nothing else.
|
|
|
|
## 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 |
|
|
| **Touch** | touch friendly on or off | a `bool` 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 — a Theme pointed at a
|
|
`float` is refused by the editor and by the server — and 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, on purpose. **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" —
|
|
and the same channel then serves anything else you want to drive, 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](https://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 — a failure must never
|
|
be 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.
|
|
|
|
### Touch
|
|
|
|
Bigger controls, and nothing that only happens on hover. A phone gets this
|
|
anyway, from its own width; a wall panel has no way to say so for itself.
|
|
|
|
### 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.
|
|
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.
|
|
|
|
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 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](portal.md) 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
|
|
|
|
- [Payload types](../reference/payload-types.md) — what a widget can bind to
|
|
- [Flows, nodes and messages](../concepts/flows.md) — where the names come from
|
|
- [Accounts and the portal](portal.md) — reaching all of this from outside
|