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
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:
@@ -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.
|
||||
|
||||
|
||||
@@ -5,14 +5,12 @@ full-bleed graph, so what you are looking at is always the flow.
|
||||
|
||||
## The canvas
|
||||
|
||||
Nodes are laid out for you — left to right on a desktop, top to bottom on a
|
||||
phone — and the arrangement is recomputed as the graph changes. There is
|
||||
Nodes are laid out for you, left to right on a desktop and top to bottom on a
|
||||
phone, and the arrangement is recomputed as the graph changes. There is
|
||||
nothing to drag and nothing to tidy.
|
||||
|
||||
That is on purpose. The edges come from message names, so a stored layout would
|
||||
be a second, weaker description of the same thing, and something to merge when
|
||||
two people edit the same flow. It also keeps flows honest: a graph nobody can
|
||||
hand-arrange is one worth splitting.
|
||||
The edges come from message names, so there is no layout to store or to merge
|
||||
when two people edit the same flow.
|
||||
|
||||
What is drawn:
|
||||
|
||||
@@ -38,7 +36,7 @@ the canvas with its panel open.
|
||||
Floats on the right; a full-screen sheet on a phone.
|
||||
|
||||
**Consumes** and **Provides** are the ports. Each row is a message name, a
|
||||
payload type, and — on an output — a *streaming* toggle for ports that publish
|
||||
payload type, and, on an output, a *streaming* toggle for ports that publish
|
||||
repeatedly during one execution. The name field suggests messages already in
|
||||
the flow, which is usually how you wire something: type the name that already
|
||||
exists.
|
||||
@@ -47,7 +45,7 @@ Renaming an output renames the message everywhere it is read. Inputs are not
|
||||
renamed, because an input is more often re-pointed at a different message than
|
||||
renamed.
|
||||
|
||||
**Settings** is generated from the node type's parameter schema — so an MQTT
|
||||
**Settings** is generated from the node type's parameter schema, so an MQTT
|
||||
node shows broker, topic and QoS, and an ntfy node shows server, topic and
|
||||
priority. A field marked as a credential renders as a picker over your stored
|
||||
[secrets](operations.md) instead of a text box.
|
||||
@@ -59,7 +57,7 @@ ports. See [Where a node's values come from](../concepts/values.md).
|
||||
**Timeout** (Function nodes) is how many seconds the code may be *silent*
|
||||
before it is stopped: it measures silence, not duration, so a node that yields
|
||||
every few seconds can run for hours under a timeout of 300. Left empty there is
|
||||
no limit, which is the default — set one where a node going quiet means it is
|
||||
no limit, which is the default. Set one where a node going quiet means it is
|
||||
stuck rather than working. A `0` says so explicitly.
|
||||
|
||||
**Code** is the editor. It saves as you type and applies on ⌘S; the node
|
||||
@@ -71,10 +69,10 @@ instead.
|
||||
Editing a shared node edits the copy every flow using it runs, which is the
|
||||
point and also the caution.
|
||||
|
||||
!!! note "Not in the panel (yet)"
|
||||
!!! note "Not in the panel yet"
|
||||
|
||||
`device` and `device_policy` — which machine a node's code runs on — are set
|
||||
through the API rather than the panel. See
|
||||
`device` and `device_policy`, which machine a node's code runs on, are set
|
||||
through the API rather than the panel, with `PUT /flows/{name}`. See
|
||||
[Remote workers](../code/workers.md).
|
||||
|
||||
## The flow panel
|
||||
@@ -95,8 +93,8 @@ Click the canvas title to open the flow's own settings.
|
||||
|
||||
Click a wire. You get the last payload that went down it, when, and a sparkline
|
||||
of its recent numeric history. You can also republish that value from the node
|
||||
that produced it, which re-runs everything downstream — the fastest way to test
|
||||
a change without waiting for the real sensor.
|
||||
that produced it, which re-runs everything downstream. It is the fastest way to
|
||||
test a change without waiting for the real sensor.
|
||||
|
||||
## The dock
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# The dashboard app
|
||||
|
||||
The web interface is a single-page app served at `app.${DOMAIN}` — or, for an
|
||||
The web interface is a single-page app served at `app.${DOMAIN}`, or, for an
|
||||
instance reached through a portal, at `${DOMAIN}/i/{instance-id}`.
|
||||
Either way it is the same application, and it is a client of the same REST API
|
||||
you can script against.
|
||||
@@ -54,7 +54,7 @@ Each flow also has a switch beside it in the list, which starts and stops it.
|
||||
|
||||
Two columns under the graph, exactly as tall as each other, most recently
|
||||
worked on first. The flows column is the list with the switches; the dashboards
|
||||
column is a mosaic, each tile a schematic of that dashboard's layout — blocks
|
||||
column is a mosaic, each tile a schematic of that dashboard's layout: blocks
|
||||
where its widgets sit, shaded by what kind of widget each one is. Neither
|
||||
column grows past about six rows: past that it scrolls in place rather than
|
||||
pushing the health block down the page.
|
||||
@@ -98,7 +98,7 @@ Opening one takes you to [the flow editor](flow-editor.md).
|
||||
|
||||
### Deleting several at once
|
||||
|
||||
Press and hold a card — or ctrl-click it — to pick it, then tap the rest. While
|
||||
Press and hold a card, or ctrl-click it, to pick it, then tap the rest. While
|
||||
anything is picked, **New flow** in the toolbar becomes a trash button, and it
|
||||
asks once before deleting the lot. Unpicking the last one puts the list back;
|
||||
so does Escape. The Dashboards screen works the same way.
|
||||
@@ -117,5 +117,5 @@ so does Escape. The Dashboards screen works the same way.
|
||||
## Appearance
|
||||
|
||||
Light and dark follow your system by default; **Settings → Appearance**
|
||||
overrides it. Both themes are first-class — the wall-panel view in particular is
|
||||
designed to be legible in dark from across a room.
|
||||
overrides it. Both themes are first-class, and the wall-panel view is designed
|
||||
to be legible in dark from across a room.
|
||||
|
||||
@@ -23,8 +23,8 @@ They are encrypted at rest with a key derived from the instance's
|
||||
flows are a git repository you may well push somewhere: what gets committed and
|
||||
shared never contains a password.
|
||||
|
||||
A name that no longer resolves is a clear error on the node that wanted it —
|
||||
"No secret named 'x' — add it under Secrets" — rather than a mysterious
|
||||
A name that no longer resolves is a clear error on the node that wanted it
|
||||
("No secret named 'x'; add it under Secrets") rather than a mysterious
|
||||
authentication failure.
|
||||
|
||||
!!! warning "Rotating `SECRET_KEY`"
|
||||
@@ -36,7 +36,7 @@ authentication failure.
|
||||
## Modules
|
||||
|
||||
Node code runs in a virtual environment of its own, on the instance's data
|
||||
volume — separate from the packages Fluksio itself runs on. A pin of yours can
|
||||
volume, separate from the packages Fluksio itself runs on. A pin of yours can
|
||||
never shadow one of ours, and vice versa.
|
||||
|
||||
**Modules** is a pip manifest and an **Apply** button. It is installed with
|
||||
@@ -49,7 +49,7 @@ and whether the manifest and the environment agree. A failed resolve comes back
|
||||
as `uv`'s own output, in the pane, because that is the only thing anyone can
|
||||
act on.
|
||||
|
||||
An install takes effect immediately — the worker processes pick up the new
|
||||
An install takes effect immediately: the worker processes pick up the new
|
||||
interpreter state without the engine restarting.
|
||||
|
||||
!!! note "Big scientific stacks"
|
||||
@@ -79,7 +79,7 @@ literal, so a webhook URL with a token in it does not sit in the config.
|
||||
### Rules
|
||||
|
||||
A rule is a set of events and the channels they go to. **A rule with no events
|
||||
ticked covers all of them**, including ones added in later versions — which is
|
||||
ticked covers all of them**, including ones added in later versions, which is
|
||||
usually what you want for the first rule you write.
|
||||
|
||||
| Event | Fires when |
|
||||
@@ -113,7 +113,7 @@ Superusers get an **Admin** screen: add, edit and remove accounts, and see who
|
||||
is a superuser.
|
||||
|
||||
Accounts that arrived through a portal are badged *Portal*. They have no
|
||||
password — they sign in at the portal, not here — and they are never
|
||||
password (they sign in at the portal, not here) and they are never
|
||||
superusers. Deleting one cuts their access immediately and independently of the
|
||||
portal, so it works even if the portal cannot be reached. See
|
||||
[Accounts and the portal](portal.md).
|
||||
|
||||
@@ -13,13 +13,13 @@ The **portal** is optional, and it exists to solve two specific problems:
|
||||
|
||||
An enrolled instance dials *out* to the portal and holds one websocket
|
||||
open. The portal serves the dashboard from its own side, and only the API calls
|
||||
travel down the tunnel — so the interface loads at portal speed and your
|
||||
travel down the tunnel, so the interface loads at portal speed and your
|
||||
machine stays unreachable from the internet.
|
||||
|
||||
## Enrolling
|
||||
|
||||
Two halves, deliberately: whoever performs the second step decides what the
|
||||
instance's owner gets.
|
||||
Two halves: whoever performs the second step decides what the instance's owner
|
||||
gets.
|
||||
|
||||
**On the portal** (`hub.${DOMAIN}`, or [fluksio.com](https://fluksio.com) for
|
||||
the hosted one): **Instances → Add instance**, give it a name, and copy
|
||||
@@ -39,10 +39,9 @@ fluksio enroll ABCD-1234 --portal https://hub.fluksio.com
|
||||
|
||||
The code is single-use and expires in fifteen minutes.
|
||||
|
||||
A portal session then arrives as *that local account* — the settings screen
|
||||
states this plainly, because it is the whole security model in one sentence.
|
||||
Use `--as someone@example.com` to enrol as a specific local account when the
|
||||
instance has several superusers.
|
||||
A portal session then arrives as *that local account*, which the settings screen
|
||||
states plainly. Use `--as someone@example.com` to enrol as a specific local
|
||||
account when the instance has several superusers.
|
||||
|
||||
Once enrolled, the instance appears under **Instances** with its
|
||||
status, when it was last seen and its version. **Open** takes you to its
|
||||
@@ -55,7 +54,7 @@ the tunnel. What those requests may do is decided **on the instance**, by
|
||||
the same checks a local session passes.
|
||||
|
||||
The trust anchor is a signing keypair on the portal. Every instance pins
|
||||
its public half at enrolment and rejects anything else — which is what stops a
|
||||
its public half at enrolment and rejects anything else, which is what stops a
|
||||
hijacked DNS entry or a mis-issued certificate from impersonating the portal.
|
||||
|
||||
## Letting someone else in
|
||||
@@ -72,7 +71,7 @@ admits them, and they arrive as a local user of their own rather than as you.
|
||||
you.
|
||||
|
||||
The instance redeems that code against the portal using its own credential.
|
||||
A portal session cannot do this — which is what stops somebody you let in from
|
||||
A portal session cannot do this, which is what stops somebody you let in from
|
||||
letting others in.
|
||||
|
||||
On the instance they appear under **Admin → Users**, badged *Portal*, never
|
||||
@@ -95,7 +94,7 @@ unsure: it does not need the portal's cooperation.
|
||||
|
||||
## Running your own portal
|
||||
|
||||
The portal is the `index` stack's `hub` service — accounts, the registry of
|
||||
The portal is the `index` stack's `hub` service: accounts, the registry of
|
||||
connected instances, and the websocket each one dials in on. Two things
|
||||
about it are load-bearing:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user