Make the docs state things rather than argue them
Docs / docs (push) Successful in 37s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m35s
Playwright Tests / test-playwright (2, 2) (push) Failing after 17s
pre-commit / pre-commit (push) Failing after 2m8s
Test Backend / test-backend (push) Failing after 2m48s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m25s

The site read as a design journal: rationale paragraphs, hedges
("deliberately", "on purpose", "genuinely"), meta-commentary about the docs
themselves, and one em-dash every ten lines carrying an aside.

Roughly twenty rationale blocks are gone or reduced to what a reader needs
in order to use the thing. Em-dashes go from 507 to 135, and what is left is
structural rather than prose: list and definition separators, table cells,
and four inside code blocks that quote what the CLI actually prints.

Also: api.example.com becomes api.fluksio.com (the emails stay, since
bootstrap.py really defaults to admin@example.com and RFC 2606 reserves it);
the mqtt table gains the two settings it had drifted behind on and inject's
wording matches the engine; llms.txt lists the two connector pages that were
in the nav but not in it; and the two device/device_policy notes now agree.

Builds clean under `zensical build --strict`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
This commit is contained in:
2026-08-31 10:49:58 +02:00
co-authored by Claude Opus 5
parent 2422a9b22b
commit bdad6d7fc2
25 changed files with 450 additions and 479 deletions
+33 -35
View File
@@ -1,6 +1,6 @@
# Dashboards and panels
A dashboard is a grid of widgets bound to message names the same names that
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.
@@ -39,7 +39,7 @@ and dragging is off. Picking a widget and editing its settings still works.
| **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
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.
@@ -54,8 +54,8 @@ screen reader calls its controls, and what a published value is labelled with.
| **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
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`.
@@ -82,21 +82,20 @@ 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
**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.
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
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
@@ -105,8 +104,8 @@ 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,
controls, a video. Media does not travel as a message; a reference to it does.
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**
@@ -115,7 +114,7 @@ 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
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.
@@ -136,7 +135,7 @@ carrying what to do about it:
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
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
@@ -147,7 +146,7 @@ reading that arrives is taken as the truth and the count restarts from it.
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
to any tile on it, and a screen bolted to a wall has nobody standing at it to
set them.
| Setting | Is | Driven by |
@@ -169,25 +168,24 @@ They all work the same way, and both halves are optional:
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
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, 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.
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
**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
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.
@@ -205,7 +203,7 @@ A palette is an ordered list of colours, and **position is the role**:
| 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
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
@@ -213,8 +211,8 @@ 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.
Fault and success keep their own colours in every palette, so a failure is never
paintable as a reading.
### Background
@@ -249,25 +247,25 @@ A wall tablet has no keyboard, so it pairs.
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,
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.
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
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
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"
@@ -290,7 +288,7 @@ 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
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.