This commit is contained in:
2026-10-13 10:02:20 +02:00
parent a93f865e91
commit 80e595c02e
24 changed files with 2210 additions and 4310 deletions
+137 -244
View File
@@ -1,324 +1,217 @@
# 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.
A **dashboard** is a grid of widgets bound to message names. It can show values
and send controls across several flows. A **panel** assigns published dashboards
to a paired screen, such as a wall tablet.
## Building one
**Dashboards → New dashboard**, then drag widgets onto the grid from the widget
picker and bind each to a message.
1. Open **Dashboards → New dashboard**.
2. Enter edit mode, choose **Add widget** and select a type.
3. Bind the widget to a message in its settings. On desktop, drag and resize
tiles on the grid.
4. **Publish** when the dashboard is ready.
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.
Edits stay in a draft; paired screens show the published version. On phones,
tiles stack in one column and dragging is disabled, but settings remain editable.
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.
Controls can publish real values while you edit, so choose their targets with
care. Your new Switch may already be flipping something!
## The widgets
### Display
| Widget | Binds to | Notes |
| Widget | Binds to | Shows |
|---|---|---|
| **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 |
| **Website** | — | another site's page in the tile; see *Website tiles* below |
| **Value** | anything | formatted reading, unit and precision |
| **Gauge** | `float`, `int` | reading within a min/max range |
| **Chart** | numeric messages, a query response or run metrics | up to five lines |
| **Bar** | `float`, `int` | up to eight readings with separate scales |
| **Icon** | numbers, booleans or strings | a glyph selected from the value |
| **Text** | — | Markdown |
| **Agenda** | `list` | upcoming items |
| **Forecast** | `list` | an outlook strip |
| **Notification** | `record` | title, body and severity |
| **Media** | `image`, `audio`, `video`, `artifact` | image, audio or video |
| **Player** | `record` | playback status and transport controls |
| **Clock** | — | current time |
| **Website** | — | embedded page or image |
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.
Every widget has a title. **Show title** controls its visibility; the title
still names the widget for accessibility and published values.
### Input
| Widget | Publishes | Notes |
| Widget | Publishes | Settings |
|---|---|---|
| **Button** | a fixed value | one-shot: run it, open it, reset it |
| **Button** | fixed value | one-shot action |
| **Buttons** | a value per button | grid of actions |
| **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 |
| **Slider** | `float`, `int` | min, max, step; horizontal or vertical |
| **Input** | text or number | free entry |
| **Selector** | selected value | menu or row of choices |
| **Colour** | list or string | HSV, RGB or hex |
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`.
Colour defaults to `[h, s, v]`: hue 0–360°, saturation and brightness 0–100%.
**Sends** can select RGB (`[r, g, b]`, 0–255) or hex (`"#rrggbb"`).
HSV and RGB bind to `list`; hex binds to `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.
Controls publish to their bound messages. The flow canvas shows them as
endpoints feeding the nodes that consume those messages.
### Typed bindings
Bindings are checked in the editor and on the server. See
[Payload types](../reference/payload-types.md).
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
Charts draw their series in one fixed order of colours: the primary first, then
hues picked to stay apart, colour blindness included. A chart with more than
one series still always draws a legend, so no line is told by colour alone.
Choose the chart's data source:
## Two kinds of chart
- **Live:** recent numeric message history, updated as values arrive.
- **Query:** publishes a `record` request containing `range_s` and
`interval_s`, then draws a flow's `series` response. Include those fields in
the answer so the chart can reject responses for a different window.
- **Runs:** compares a numeric metric from named runs, a sweep or the latest
runs of a flow. See [Runs](../concepts/runs.md).
**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".
Multi-line charts show a legend as well as different colours.
**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.
!!! tip "Skip the unfinished bucket"
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.
If your query aggregates into time buckets, leave out the bucket still
filling. Otherwise an incomplete total can look like a sudden drop.
## 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.
Media messages contain artifact references; the widget loads the bytes they
name. **Crop or fit** controls image sizing. **Play as it arrives** enables
autoplay, subject to the browser's rules for sound.
**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.
For local streams, `volatile=True` artifacts can be pushed over the websocket
with their message. Stored artifacts are fetched individually, as are frames
viewed through a portal. Use occasional snapshots for remote screens.
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.
For continuous video, set **Live stream** to a browser-compatible camera URL.
The browser must be able to reach that URL; enrollment does not expose the
camera itself. See [Media payloads](../reference/payload-types.md#image-audio-video).
## 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:
A Player reads a status `record` and publishes commands to a separate `str`
message.
| Field of the record | Means |
| Status field | Meaning |
|---|---|
| `title`, `artist`, `album` | what is playing |
| `status` | `play`, `pause`, `stop`, `load`, or `off` for a streamer with no power |
| `position`, `duration` | seconds, both |
| `title`, `artist`, `album` | track details |
| `status` | `play`, `pause`, `stop`, `load` or `off` |
| `position`, `duration` | seconds |
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.
Controls send `toggle`, `next`, `prev` and `seek:<seconds>`. Your receiving
node translates those into device actions. The position advances in the
browser between readings and resets to each fresh reading.
## Website tiles
A website tile frames whatever page its **Address** points at: a weather map, a
camera's own web interface, a timetable. A site that refuses to be framed shows
its own refusal in the tile.
Set **Address** to a page to embed, or choose **Image** for a snapshot or MJPEG
URL. A website may refuse embedding.
The page runs sandboxed. By default it gets an origin of its own that matches
no site, so it can run scripts but has no cookies or storage, and many
interactive pages will not load like that. **Trusted site** lets it keep its
real origin, with its cookies and storage, which is what visiting the site
would give it. Turn it on only for a site you trust. A trusted tile pointing at
this app's own address, or the hub's when the app is reached through one, shows
a notice instead: on that origin the page could reach into the app and read
your session.
**Image** draws the address as a picture scaled to fit the tile, rather than
as a page. Use it for a camera's MJPEG stream (`http://<camera>/stream`) or a
snapshot URL, which a page frame would show at the camera's native size.
Embedded pages run in a sandbox without cookies or storage by default.
**Trusted site** permits the site's own origin, cookies and storage.
Use it only for a trusted destination. The app blocks trusted embeds of its
own origin, including the portal origin, to protect your session.
## 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.
Open **Dashboard settings** from the dock. Each setting can hold a fixed value
or follow a message chosen in **Driven by**. The fixed value is the fallback
when no usable message value is available.
| Setting | Is | Driven by |
| Setting | Choices | Message type |
|---|---|---|
| **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 |
| **Look** | `fluksio`, `material`, `glass` | `str` |
| **Theme** | `system`, `light`, `dark` | `str` |
| **Palette** | ordered hex colours | `list` |
| **Background** | image URL | `str` |
| **Lock** | read-only on/off | `bool` |
They all work the same way, and both halves are optional:
To change one on a schedule, have a scheduled node publish its bound message.
- **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.
### Look and theme
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.
**Fluksio** is the default look. **Material** uses flat tonal cards; **Glass**
uses translucent tiles. Widgets work the same in all three.
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.
Theme follows the chosen preference unless a palette sets the background
colour.
### Palette
A palette is an ordered list of colours, and **position is the role**:
Paste a colour list or a [Coolors](https://coolors.co) palette link. Colours
have roles by position:
| # | Role |
| Position | 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 |
| 1 | background |
| 2 | widget surface |
| 3 | primary controls and first chart line |
| 4 | accent |
| 5 | text |
| 6+ | the next chart lines, before the app's own chart colours |
| 6+ | additional chart lines |
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.
Missing roles are derived, so three colours are enough to get started.
**Rotate** shifts their order. Text contrast is corrected automatically;
failure and success indicators keep their status colours.
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 and lock
### 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.
Background draws an image beneath the widgets. Lock disables publishing from
controls and shows **Read-only** when viewing the dashboard. It does not
restrict API permissions and does not disable controls in edit mode.
## 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.
Open `/view/{name}` for a dashboard-only view with your normal session.
For a dedicated screen, pair a panel.
## Panels: hanging a screen on a wall
A wall tablet has no keyboard, so it pairs.
As a superuser:
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. On a phone the rail is a
row floating along the bottom; the app shows it there too, above its own
buttons, for a dashboard that hangs on such a panel.
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.
1. Open **Dashboards → Panels**, add a panel and select its dashboards.
2. Enable **Touch friendly** for larger controls.
3. Open the displayed pairing link on the device. It shows a six-character code.
4. Enter that code for the panel, check the device description, then press
**Pair device**.
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 screen collects its credential and shows the assigned published dashboards.
Several dashboards get a navigation rail.
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.
A panel credential permits its assigned dashboards, their bound messages and
the artifacts they show. It also permits publishing from its controls and query
charts. **Lock** changes the viewing surface; it does not narrow the credential.
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.
**Unpair screen** revokes a locally paired credential and keeps the panel's
dashboard assignments. Deleting the panel removes the assignment and its
access.
!!! note "If the link is wrong"
!!! note "If the local 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.
Pairing links use `FRONTEND_HOST`. Set it to the app address reachable
from the screen's network.
### A screen somewhere you cannot reach
Another building, someone else's network, no route in. An instance
[enrolled with a portal](portal.md) 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*.
An [enrolled instance](portal.md) also shows a portal pairing link. Use that
link on the remote screen, then approve its code in the same dialog. The
device description says *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.
The instance enforces the same dashboard and message limits. Remote
credentials are issued by the portal, so **Unpair screen** does not revoke
them; revoke them at the portal. Deleting the panel removes access on the
instance.
## 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
- [Payload types](../reference/payload-types.md)
- [Flows, nodes and messages](../concepts/flows.md)
- [Accounts and the portal](portal.md)
+99 -127
View File
@@ -1,177 +1,149 @@
# The flow editor
The canvas is where most of the work happens. It floats its chrome over a
full-bleed graph, so what you are looking at is always the flow.
Build a flow by naming its messages and configuring its nodes. Fluksio draws
the connections and arranges the canvas for you.
## The canvas
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.
The graph runs left to right on desktop and top to bottom on phones.
The edges come from message names, so there is no layout to store or to merge
when two people edit the same flow.
- **Nodes** show their execution status.
- **Edges** pulse when values pass through. Click one to inspect its payload.
- **Endpoints** show flow inputs, dashboard widgets and nodes in other flows
that exchange messages with this flow.
What is drawn:
- **Your nodes**, with a status dot — running, succeeded, failed, or nothing at
all when idle. Status is always named in words too, never colour alone.
- **Edges**, which pulse when a value goes down them. Click one to inspect it.
- **Endpoints** — anything wired into this flow that is not a node in it: a
dashboard control writing a message, a tile reading one, a node in another
flow on the far side of a dotted name. Drawn as labelled boxes at the
boundary, so a value never appears to come from nowhere.
Values are live. The editor holds a websocket to the engine, and the connection
banner tells you when it is not.
Values update over a websocket. A connection banner tells you when the editor
cannot reach the engine.
## Adding a node
**Add node** on the dock, or ⌘P / Ctrl-P for the command palette, which also
jumps between flows and offers your shared nodes. Pick a type and it appears on
the canvas with its panel open.
Choose **Add node** in the bottom dock, or use ⌘P / Ctrl-P. The command palette
also jumps between flows and offers reusable nodes. Select a type to add it
and open its settings.
## The node panel
Floats on the right; a full-screen sheet on a phone.
**Consumes** and **Provides** declare the node's ports. Each has a port name,
payload type, bound message and delivery interval. Outputs also have a
streaming toggle.
**Consumes** and **Provides** are the ports. Each port has two lines: its name
and payload type (a `list` also says what its items are), then the message it
is bound to, how often at most it is delivered, and, on an output, a
*streaming* toggle for ports that publish repeatedly during one execution. The
message field suggests messages already in the flow, which is usually how you
wire something: type the name that already exists.
Message fields suggest existing names. Matching an output's message is how
you connect an input.
A port is named after its message until you name it yourself. Its name field
then stays empty and shows the name it follows. A name typed there stays when
the message changes, which is how a port is bound to a message called
something else, as a [`wren`](../reference/node-types.md#wren) target is;
clear it to follow the message again. The name is what the node's code sees,
so it is letters, digits and `_`, not starting with a digit, and no two inputs
or two outputs of a node share one. The field keeps the name it had otherwise.
A blank port-name field follows the message's last segment. Set a port name
to keep the argument name fixed while changing its message binding. Names use
letters, digits and `_`, cannot start with a digit, and must be unique among
the node's inputs or outputs.
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.
Renaming an output's message updates its consumers in the flow. Changing an
input binding points that input at a different message.
**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. A setting holding JSON, such as
a Switch node's rules, shows one line of it; click it to edit the whole value
in a larger editor, which saves only once the text parses. Escape or Cancel
leaves the setting as it was. An inject's payload and a flow input's starting
value of type `json`, `record` or `list` open the same editor, which also holds
the shape (a `record` is an object, a `list` an array); emptying it leaves no
value.
### Settings
A Function node gets a second settings section where you add your own: a
setting is a constant of this node's code and arrives as an argument beside its
Built-in nodes show forms for their configuration. Credential fields offer
stored [secrets](operations.md#secrets). JSON settings open a larger editor
and save only when the value parses.
Function nodes also let you add settings passed as arguments alongside the
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
stuck rather than working. A `0` says so explicitly.
**Timeout** on a Function node limits silence, not total duration. A node
yielding regularly can run longer than its timeout. Empty or `0` means no
limit, the default.
**Code** is the editor. It saves as you type and applies on ⌘S; the node
reloads without the flow stopping. A node file defines `process(...)`; if it
defines exactly one public function under another name, that one is used
instead.
**Resources** selects a shared worker pool, a named flavor or custom CPU,
memory and GPU requirements. Placement by `device` and `device_policy` is
configured through the API or flow JSON; see [Remote workers](../code/workers.md).
Code runs as the engine, so changing it is a superuser's. Anyone else sees the
editor read-only, a Command node's settings disabled, and no Function, Command
or reusable nodes to add.
### Code and reuse
**Shared** turns a node's code into a library entry other flows can reuse.
Editing a shared node edits the copy every flow using it runs, which is the
point and also the caution.
Define `process(...)` in **Code**. If the file instead contains exactly one
public function, Fluksio uses that function.
!!! note "Not in the panel yet"
Code saves as you type. ⌘S / Ctrl-S in the code editor saves immediately;
**Publish** applies ordinary node code to the running flow. Generated code
from `fluksio sync` is read-only: edit its source repository and sync again.
`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).
Only superusers can add or edit code-running nodes. Other users can read code
and configure permitted built-in nodes.
**Reuse** shares node code as a library entry. Saving shared code takes effect
immediately in every flow using it, so check its users before changing it.
## The flow panel
Click the canvas title to open the flow's own settings.
Open **Flow settings** from the pencil button in the dock.
- **Running** — the switch that starts and stops the flow. Stopped means none
of its subscriptions, schedules or webhooks exist.
- **Mode** — `live` or `batch`. Live reacts to what arrives; batch only runs
when a run asks it to. See [Runs](../concepts/runs.md).
- **Inputs** — messages the flow takes from outside, with the value each starts
from. For a batch flow these are also the run's parameters.
- **Outputs** (batch only) — which messages a run reports as its result.
- **Contents** — a reminder that this flow's name namespaces every message
inside it.
| Setting | Purpose |
|---|---|
| **Running** | starts or stops the published live flow's subscriptions, schedules and webhooks |
| **Mode** | chooses continuous `live` operation or requested `batch` runs |
| **Inputs** | declares external messages and starting values; batch inputs become run parameters |
| **Result** | selects batch output messages |
| **Contents** | shows the node count and message namespace |
For live inputs, **Publish this value now** sends the starting value immediately.
Changing a stored starting value alone does not replace an existing message.
## Inspecting an edge
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. It is the fastest way to
test a change without waiting for the real sensor.
Click an edge to see its latest payload, timestamp and recent numeric history.
Republish its value from the producer to run downstream nodes without waiting
for the next sensor reading.
## The dock
Along the bottom, floating over the canvas.
| Control | What it does |
| Control | Action |
|---|---|
| **Add node** | opens the palette |
| **Fit** | returns to the view the flow opened with |
| **Run** | injects a value by hand; on a batch flow, opens the parameters dialog |
| **Pause / Resume** | holds messages instead of running them |
| **Step** | releases exactly one held message |
| **Publish** | puts the draft live |
| **Discard** | throws the draft away |
| **Issues** | what is stopping this flow from running, per node |
| **Logs** | what the nodes printed, filterable |
| **Add node** | opens the command palette |
| **Fit to screen** | fits the graph; desktop only |
| **Run** | executes the current live flow once; for batch flows, asks for run parameters |
| **Pause / Resume** | holds or releases pending work while values still arrive |
| **Step** | releases one held item while paused |
| **Flow settings** | opens the flow panel |
| **Publish** | puts the saved draft live |
| **Discard** | removes unpublished changes |
| **Issues** | lists problems by node |
| **Logs** | shows filterable node output |
Pause and step together are how you test a flow before it moves anything
physical: pause it, inject a value, step it through, watch the values appear on
the edges, then publish.
**Run** uses the current draft when one exists without publishing it. It
executes real node code, including device and network actions. To try a single
value in a live flow, publish it from a flow input or edge inspector.
Pause and step are useful for inspecting held work. They do not make a draft
run a simulation.
## Shortcuts
| Chord | Action |
Use ⌘ on macOS and Ctrl elsewhere.
| Shortcut | Action |
|---|---|
| ⌘P / Ctrl-P | command palette |
| ⌘S / Ctrl-S | publish the flow — or, with focus in the code editor, apply the code |
| ⌘Z / ⌘⇧Z | undo / redo (the flow; the code editor has its own) |
| ⌘C / ⌘V | copy and paste nodes, including between flows |
| ⌘S / Ctrl-S | publish the flow; inside the code editor, save code |
| ⌘Z / Ctrl-Z | undo a graph edit |
| ⌘⇧Z / Ctrl-Shift-Z | redo a graph edit |
| ⌘C / Ctrl-C | copy selected nodes |
| ⌘V / Ctrl-V | paste nodes, including into another flow |
## Drafts and publishing
Text fields and the code editor keep their own undo and clipboard behavior.
Saving writes a draft; the engine keeps running what was published. Publishing
promotes it and reloads.
## Drafts, conflicts and issues
If someone else saved the same flow while you were working, publishing answers
with a conflict rather than overwriting them. The Flows list offers **Publish
all changes** when several flows are sitting on drafts.
Edits are saved as drafts. The engine keeps running the published version until
you publish; shared code follows the exception above. **Discard** returns to
the published version. Delete a never-published flow to remove it.
## Issues
If another editor saved the same flow, a stale save or publish returns a
conflict. Reload and review their changes before retrying.
The canvas validates as you edit and marks the node each issue belongs to:
- a port that needs a message nothing in reach provides
- a message that exists but has never held a value
- a dependency cycle
- a node reading a message it also writes, with nothing to start it from
- code that did not load
- a webhook with no shared secret (advisory — it does not stop the flow)
- a node that loaded but reports itself down, such as a connector that cannot
reach its device
A flow with any of these except the last two does not run, and the health
summary on Home counts it. The last one clears on its own once the node reports
itself well again.
The canvas reports unconnected inputs, missing starting values, cycles and
code-load errors. These prevent the affected flow from running. Unauthenticated
webhooks are advisory; runtime node-health problems clear when the node
recovers. See [Flow issues](../concepts/flows.md#when-a-flow-needs-attention).
## See also
- [Flows, nodes and messages](../concepts/flows.md) — the model behind the canvas
- [Node types](../reference/node-types.md) — everything the palette offers
- [Writing node code](../code/nodes.md) — what goes in the editor
- [Flows, nodes and messages](../concepts/flows.md)
- [Node types](../reference/node-types.md)
- [Writing node code](../code/nodes.md)
+56 -97
View File
@@ -1,128 +1,87 @@
# The dashboard app
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.
Open your instance's app address, or select it at [fluksio.com](https://fluksio.com)
after enrollment. For the local Docker setup, use
[app.localhost](http://app.localhost) and the admin login printed by
`make init`. See [Getting started](../getting-started/index.md) for setup.
Sign in with the account the instance was created with. On a fresh
instance that account was printed once, on the first start.
## Finding your way around
## The shell
The sidebar opens as a sheet on phones.
A floating sidebar on the left, the selected screen filling the rest. On a
phone the sidebar collapses to a sheet.
| Entry | What lives there |
| Entry | Use it to |
|---|---|
| **Home** | the brain graph, health, and everything that recently happened |
| **Flows** | the list of flows, and the canvas for each |
| **Dashboards** | the widget canvases, and the panels that display them |
| **Runs** | starting a run with values you choose, and the log of what ran |
| **Workers** | the machines that nodes can be placed on |
| **Secrets** | credentials your nodes reference without holding |
| **Modules** | the Python packages your node code may import |
| **Alerts** | where failures get sent |
| **Admin** | users (superusers only) |
| **Search** | anything in this instance, by name |
| **Settings** | your account, appearance, remote access, and the assistant |
| **Home** | see the brain graph, health and recent activity |
| **Flows** | create flows and open their editors |
| **Dashboards** | build widget dashboards and pair wall panels |
| **Runs** | start batch runs and review their results |
| **Workers** | manage machines that execute nodes |
| **Secrets** | store credentials; superusers only |
| **Modules** | see or install packages used by node code |
| **Alerts** | choose where failures are reported |
| **Admin** | manage users and OAuth clients; superusers only |
| **Search** | find items by name |
| **Settings** | change your account, appearance and remote access |
**Assistant** appears when a provider is configured. **Documentation** opens
the docs in another tab.
### Search
**Search** at the foot of the sidebar, or ⌘K / Ctrl-K from anywhere, opens a
panel that finds things by name as you type: flows with the nodes inside them
and the inputs and outputs they declare, dashboards and the widgets on them,
panels, secrets, modules, workers and alert channels. Picking a node opens its
flow with that node in focus, an input or output opens its flow's settings,
where it is declared, and a widget opens its dashboard.
Use **Search**, or ⌘K / Ctrl-K, to find flows, nodes, flow inputs and outputs,
dashboards, widgets, panels, secrets, modules, workers and alert channels.
Selecting a result opens its editor or settings.
It searches this instance. Reached through a portal, other instances
are behind **All instances** at the top of the sidebar.
Search covers the current instance. When connected through a portal,
**All instances** returns to the instance list.
## Home
The one screen you leave open. Four things share it.
### The brain graph
Every flow drawn as a neuron, wired to the flows it exchanges messages with.
This is the brand mark made live, and it is also the fastest read on the
instance: a neuron pulses when its flow is running work, and its ring turns
terracotta when the flow cannot run as written. A neuron with a problem keeps
its label showing so you can see which one it is without hovering.
The graph combines nodes from published flows. Nodes talking to the same
broker topic, URL or other external target share a neuron; computation nodes
appear individually. Messages connect them, and activity makes them pulse.
Each flow also has a switch beside it in the list, which starts and stops it.
A [`wren` node](../reference/node-types.md#wren) anywhere adds a chat button
at the graph's top right, in the header on a phone, which opens the chat on
its [Control tab](../code/agents.md#control).
A problem keeps the affected neuron's label visible. A
[`wren` node](../reference/node-types.md#wren) also adds a chat button for its
[Control tab](../code/agents.md#control).
### Flows and dashboards
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
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.
The tiles are a footprint, not a live view. They show you which dashboard is
which at a glance; the readings are on the dashboard itself.
The flow list includes switches to start and stop flows. Dashboard tiles show
a small sketch of each layout; open one for its live readings.
### Health
Always answers, degraded or not. The tiles cover:
- **Flows** — total, running, paused, quarantined, and how many cannot run
because their graph does not validate
- **Nodes** — how many failed to load
- **Runs running** — batch runs in flight right now, and how many are
waiting. Only on an instance that has run something
- **Queue** — depth, and how old the oldest pending item is
- **Loop lag** — whether the engine's event loop is keeping up
`status: degraded` comes with a list of named problems, in words. "3 flow(s)
cannot run: house, pv, hallway" is more useful than a red dot, so that is what
it says.
Health shows flow status, node load errors, queue depth and event-loop lag.
After you have used batch runs, it also shows running and queued runs.
A degraded instance lists the problems that need attention.
### Activity
Charts of executions and failures over the selected range (1h / 6h / 24h / 7d),
with the recent runs, recent failures, dead-lettered work and the audit trail
underneath.
Choose 1h, 6h, 24h or 7d to review executions and failures, recent runs,
dead-lettered work and the audit trail. Hover a chart to filter the lists to
that moment; click to hold the selection while you read.
The charts are scrubbable: hover a moment and the lists below filter to it,
click to hold it while you read. That turns "something went wrong around two
o'clock" into the actual rows.
## Managing flows and dashboards
## Flows
The lists show titles, unpublished changes and status. Use **New flow** or
**New dashboard** to create one. The Flows list offers **Publish all changes**
when drafts are waiting.
The list shows each flow's title, node count, whether it has unpublished
changes, and whether it is enabled, paused or quarantined. The toolbar searches,
creates, and offers **Publish all changes** when several flows have drafts.
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
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.
## Everything else
- [The flow editor](flow-editor.md) — the canvas, the code editor, running and
testing
- [Dashboards and panels](dashboards.md) — widgets, bindings, and hanging a
screen on a wall
- [Secrets, modules and alerts](operations.md) — the three screens that keep an
instance running
- [Accounts and the portal](portal.md) — reaching an instance from outside
its network
To delete several items, hold a card or Ctrl-click it, then select the rest.
The create button becomes a trash button. Deletion asks for confirmation;
Escape clears the selection.
## Appearance
Light and dark follow your system by default; **Settings → Appearance**
overrides it. Both themes are first-class, and the wall-panel view is designed
to be legible in dark from across a room.
Light and dark follow your system by default. Change this under
**Settings → Appearance**. Dashboards also have their own theme settings.
## Next stops
- [The flow editor](flow-editor.md)
- [Dashboards and panels](dashboards.md)
- [Secrets, modules and alerts](operations.md)
- [Accounts and the portal](portal.md)
+65 -98
View File
@@ -1,139 +1,106 @@
# Secrets, modules and alerts
Three screens that have nothing to do with each other except that an
instance you actually depend on needs all of them.
Store credentials, manage node dependencies and choose where failures go.
These screens help keep an instance humming.
## Secrets
A node never holds a credential. A parameter written as
Open **Secrets** as a superuser, then add a name and value. The list shows
names only; saved values are not displayed again.
Reference a secret from a node setting:
```json
{"$secret": "influx-token"}
```
is replaced with the stored value when the node is built, and the editor
renders any field a node type marks as a credential as a picker over what you
have stored.
Fluksio resolves it when building the node. Credential fields in the editor
offer a secret picker. Missing names produce an error on the affected node.
**Secrets** is where the values live. Add a name and a value; the value is
never shown again, and the list only ever shows names.
Secrets are encrypted with a key derived from `SECRET_KEY` and stored outside
the flow repository. Using references keeps credentials out of saved flows.
They are encrypted at rest with a key derived from the instance's
`SECRET_KEY`, and kept **outside** the flow repository. That matters because
flows are a git repository you may well push somewhere: what gets committed and
shared never contains a password.
!!! warning "Keep your encryption key"
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`"
The secrets store is encrypted with a key derived from it. Change
`SECRET_KEY` and the store no longer decrypts. Re-enter the secrets, or
plan the rotation properly.
Changing `SECRET_KEY` makes existing secrets unreadable. Keep the old key
for re-encryption, or re-enter the secrets. Source installations include
`backend/scripts/rotate_secret_key.py` for rotating the secrets store.
## 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
never shadow one of ours, and vice versa.
**Modules** shows the Python environment used by node code, its installed
packages and Python version. There are two setups:
**Modules** is a pip manifest and an **Apply** button, which only a superuser
sees: a package runs its own code on install. It is installed with
`uv pip sync`, so a line you delete is uninstalled rather than left behind.
The manifest is versioned alongside your flows, so what a deployment installed
is recorded with what uses it.
- **Managed environment:** Docker uses a separate node environment. A
superuser edits **Requirements** and presses **Apply**. Packages are synced
with `uv pip sync`; deleting a requirement removes it. The manifest is
versioned with the flows, and successful installs refresh the workers.
- **Adopted environment:** a fresh pip installation inside your own virtual
environment can use that environment for nodes too. Modules is read-only.
Install dependencies there with pip or uv, then sync or restart Fluksio.
The page shows what is currently installed, the interpreter's Python version,
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.
Set `NODE_VENV=managed` to request a separate environment. Installation
failures show the package manager's output on the page.
An install takes effect immediately: the worker processes pick up the new
interpreter state without the engine restarting.
!!! note "Big scientific stacks"
Installing torch into this venv works and is often the simplest thing. The
alternative, when you already have an environment you would rather not
duplicate, is to attach it as a worker: `fluksio-worker --python
/path/to/venv/bin/python`. See [Remote workers](../code/workers.md).
For an existing scientific environment on another machine, attach a worker
with `fluksio-worker --python /path/to/venv/bin/python`. See
[Remote workers](../code/workers.md).
## Alerts
Everything that goes wrong already travels the engine's event bus. Alerts is
where you say who hears about it.
Create a channel, choose events in a rule, and use **Test** to check delivery.
Enable alerts for the rules to send.
### Channels
| Kind | Settings |
| Kind | Destination |
|---|---|
| **ntfy** | server, topic, and a token for a protected topic |
| **SMTP** | an address to send to (the instance's mail settings do the rest) |
| **Webhook** | a URL to POST to |
| **Dashboard** | a message name a notification widget reads |
| **ntfy** | server and topic; optional token |
| **SMTP** | recipient address, using the instance's mail settings |
| **Webhook** | URL that receives a POST |
| **Dashboard** | declared message read by a Notification widget |
| **webpush** | browsers subscribed from the Alerts page |
Any channel setting may hold a `{"$secret": "name"}` reference instead of a
literal, so a webhook URL with a token in it does not sit in the config.
Channel settings may use `{"$secret": "name"}` references. Browser push also
needs notification permission and a secure browser context.
### 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
usually what you want for the first rule you write.
A rule selects events and destination channels. Leaving all events unchecked
matches every supported alert event, including future additions.
| Event | Fires when |
| Event | Trigger |
|---|---|
| `node_error` | a node failed |
| `node_health` | a connection dropped |
| `flow_quarantined` | a flow crashed often enough that the engine stopped restarting it |
| `task_crashed` | a background task crashed |
| `engine_degraded` | the engine is struggling |
| `cascade_dropped` | work was given up on |
| `queue_unavailable` | the work queue is unreachable |
| `node_error` | a node fails |
| `node_health` | a connection goes down |
| `flow_quarantined` | repeated crashes stop a flow's background work |
| `task_crashed` | a background task crashes |
| `engine_degraded` | engine health degrades |
| `cascade_dropped` | the engine drops work |
| `queue_unavailable` | the work queue becomes unreachable |
**Test** sends one message through a channel so you find out it is misconfigured
now rather than at three in the morning.
Repeated faults are deduplicated with a 15-minute default cooldown.
Flapping connections are suppressed, and the engine caps alert dispatches at
ten per hour. A quiet channel may be cooling down; **Test** helps distinguish
that from a broken configuration.
### What it mostly does is not send
## Admin: users and clients
This is the part worth knowing before you decide it is broken:
Superusers use **Admin** to add, edit and remove accounts, and manage OAuth
clients for [agents](../code/agents.md).
- the same fault repeating is **one** alert, with a cooldown (15 minutes by
default per rule)
- a connection flapping is one story, not one alert per transition
- however bad it gets, **ten alerts an hour** is the ceiling
An engine that is genuinely on fire produces a handful of messages, not
thirty-six thousand.
## Admin: users
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
superusers, so they read node code but cannot change it. 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).
The same screen manages OAuth clients, which is how [agents](../code/agents.md)
are registered.
Accounts admitted from a portal are marked *Portal*. They sign in through the
portal, have no local password and are not superusers. To block local access,
edit the account and clear **Is active?**. See [Accounts and the portal](portal.md).
## Settings
Your own account: name, email, password, and appearance (light, dark, or
follow the system).
Change your name, email, password and appearance under **Settings**.
**Remote access** is where an instance is paired with a portal, remote
users are admitted, and the link is cut again. That has [its own
page](portal.md).
**Remote access** manages enrollment and admitted users; see
[Accounts and the portal](portal.md).
**Assistant** turns on the chat that builds and edits flows: pick a provider,
paste a key, fetch the model list and choose one. The key is stored encrypted
on the instance and never shown again. One provider serves everyone who signs
in here, and until one is set nothing in the interface mentions the feature.
How the chat works, and what it may do, is on the [agents
page](../code/agents.md#the-assistant).
A superuser can configure **Assistant**: choose a provider, store its key,
fetch models and select one. The encrypted key is shared for assistant access
on this instance. The chat appears after configuration; see
[The assistant](../code/agents.md#the-assistant).
+59 -115
View File
@@ -1,142 +1,86 @@
# Accounts and the portal
Fluksio is self-hosted by default. An instance runs offline, keeps its data
on its own disk, and never contacts anything unless you tell it to.
The optional portal lets you open an instance from [fluksio.com](https://fluksio.com),
including a pip installation with no local dashboard. The instance connects
outward over a WebSocket, so you do not need to open an inbound port.
The **portal** is optional, and it exists to solve two specific problems:
Flows still run on your instance. The portal serves the interface and carries
API requests, responses and live updates through the connection.
1. **Your machine has no inbound route.** A homelab behind CGNAT, a cluster
node with no open ports, a laptop. Opening one is work, and often not
allowed.
2. **You want a browser on it anyway.** A `pip install fluksio` instance
has no web server for the dashboard at all.
## Connect an instance
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
machine stays unreachable from the internet.
1. Sign up or sign in at **fluksio.com**.
2. Open **Instances → Add instance** and copy the code.
3. On the machine running Fluksio, open another terminal in the same directory
as `fluksio serve` and run:
## Enrolling
```sh
fluksio enroll <code>
```
Two halves: whoever performs the second step decides what the instance's owner
gets.
The CLI uses `https://hub.fluksio.com` by default. If you started the engine
with `--data-dir` or `--global`, use the same option when enrolling. A running
engine picks up the connection automatically; otherwise start it with
`fluksio serve`.
**On the portal** (`hub.${DOMAIN}`, or [fluksio.com](https://fluksio.com) for
the hosted one): **Instances → Add instance**, give it a name, and copy
the code.
From a local dashboard, you can instead use **Settings → Remote access**:
leave the default portal URL, paste the code, and press **Connect**.
Enrollment requires a superuser account.
**On the instance**, either from the dashboard:
> **Settings → Remote access**, enter the portal URL and the code, press
> **Connect**.
or from the command line, which is the path for an instance with no web
interface of its own:
The portal owner's sessions use the local account that enrolled the instance.
Check **Acting as** under Remote access. With several local superusers, the
CLI can choose one explicitly:
```sh
fluksio enroll ABCD-1234 --portal https://hub.fluksio.com
fluksio enroll <code> --as someone@example.com
```
The code is single-use and expires in fifteen minutes. A fresh code on an
instance that is already paired replaces the pairing, so moving an instance to
another portal is one command rather than a disconnect and a reconnect.
A new valid code replaces an existing connection. An expired or invalid code
leaves the current connection intact. Once connected, open the instance from
the portal's **Instances** list.
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.
## Let someone else in
Once enrolled, the instance appears under **Instances** with its
status, when it was last seen and its version. **Open** takes you to its
dashboard at `${DOMAIN}/i/{instance-id}`.
Each person gets an ordinary local account, with no superuser rights. You can
admit them in either of these ways:
**Upgrade a self-hosted portal before its instances.** An instance dials the
tunnel with its credential in an `Authorization` header, where it stays out of
access logs, and a portal from before that reads it only from the URL. The
portal still takes the old `?token=` for one release, so older instances keep
attaching meanwhile.
- Share an invitation link from the portal's **Instances** list.
- Ask them for a code from **Instances → Join an instance**. Enter it on your
local instance under **Settings → Remote access → Add remote user**.
## What the portal can and cannot do
Their account appears under **Admin → Users** with a *Portal* badge. They
sign in through the portal rather than with a local password. Invitations are
time-limited; generate a fresh one if it expires.
The portal holds one credential for your instance and proxies requests down
the tunnel. What those requests may do is decided **on the instance**, by
the same checks a local session passes.
## Manage or end remote access
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
hijacked DNS entry or a mis-issued certificate from impersonating the portal.
**Settings → Remote access** shows the connection status and the latest
connection error. The instance needs outbound access to the portal, and any
proxy on that route must support WebSockets.
## Letting someone else in
- To remove a person's access, remove them from the portal's sharing dialog
or edit their account under **Admin → Users** and clear **Is active?**.
- To end all portal access from the instance, press **Disconnect**. This stops
the connection and invalidates portal credentials locally, without requiring
the portal to be online.
- To replace the pairing, use **Connect with a new code**. Remote users and
panels may need to pair again.
Anyone else on the portal reaches your instance only if you let them, and they
arrive as a local user of their own rather than as you. There are two ways,
and the difference is who starts.
Local sign-in and flows continue after disconnecting. Portal requests follow
the instance's normal account permissions, and the instance verifies portal
tokens against the public signing key pinned during enrollment.
### Share a link
## Custom portals
On the portal, the share icon on your instance's row gives a QR code and a URL.
Whoever opens it, signed in to Fluksio on that device, joins. It works once and
expires in fifteen minutes; **New link** replaces it, which is also how a link
you would rather not have sent is cancelled.
The CLI accepts another compatible portal with `--portal https://<portal-host>`;
the dashboard has the same URL field. HTTPS is required except for a portal on
loopback.
Nothing has to be entered on the instance. It learns of the new member when
they first arrive and asks the portal who they are, then gives them their local
account.
### Take a code
For a machine whose owner would rather admit people at the machine itself.
1. **They**: **Instances → Join an instance**, and copy the code. It is
bound to their portal account and expires in fifteen minutes.
2. **You**, on the instance: **Settings → Remote access → Add remote
user**, and enter the code.
Either way they now see the instance under **Instances**, marked *Shared by*,
with **Open** and nothing else. Renaming, re-keying and removing stay with you.
The instance redeems the code against the portal using its own credential.
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
a superuser and with no password.
## Cutting it off
| From | Action | Effect |
|---|---|---|
| The portal | share dialog → **Remove** | that member stops reaching it from the portal; their user on the machine stays until it is deleted there |
| The portal | **Leave**, on a row shared with you | gives up your own access to somebody else's instance |
| The portal | **New code** | rotates the credential and drops the current link |
| The portal | **Remove** | deletes the registration and cuts the connection |
| The instance | **Disconnect** | unilateral and immediate — the portal's tokens stop verifying here whatever the portal still has on file |
| The instance | delete a user under **Admin → Users** | that one person, immediately, independent of the portal |
**New code** also cuts every credential the portal minted for this instance,
including [wall panels paired through it](dashboards.md#a-screen-somewhere-you-cannot-reach).
The instance's own **Disconnect** is the one to reach for if you are ever
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
connected instances, and the websocket each one dials in on. Two things
about it are load-bearing:
- **It runs a single process.** It keeps its attached instances in the
memory of the process holding their sockets, so a second worker would answer
for links it does not hold. Scaling out needs a routing layer first.
- **Back up the signing keypair with the database.** Replacing it forces every
instance to be enrolled again.
Websocket upgrades must be enabled on the `hub` hostname in whatever proxy
fronts it. Without them every instance sits in a reconnect loop and the
portal shows them all offline.
This public repository contains the instance and its connector. The portal
service is not included, so the Docker stack does not start one.
## See also
- [The `fluksio` command](../code/cli.md) — `fluksio enroll`, and what it writes
- [Dashboards and panels](dashboards.md) — pairing a wall screen through the portal
- [Getting started: data science](../getting-started/data-science.md) — the
shortest path from a pip install to a browser
- [The fluksio command](../code/cli.md): data directories and enrollment options.
- [Dashboards and panels](dashboards.md): pair a wall screen through the portal.
- [Getting started](../getting-started/index.md): install and open your first instance.