Publish the documentation site: docs.fluksio.com
A zensical site under docs/, served by a new `docs` compose service behind Traefik, built with --strict in CI. Same pattern the sibling n3xd workspace uses. Getting started splits the way the landing page does — one path is `pip install fluksio` and a training script, the other is a Docker stack and an afternoon in the browser — because the two audiences will not spend the same amount of time. Everything after that is shared: the concepts, the web interface (app and portal), the CLI and the API, and a reference for node types, payload types and configuration. The three flow guides move here from the docs submodule rather than being copied, so there is one version of them. Styling mirrors DESIGN-GUIDELINES.md: the app's token palette remapped onto Material's variables in both schemes, Inter, the 16px panel radius, and the one terracotta accent spent on the facility lane of the audience split. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M7Xv3cJEW5c8AXxn2hoojV
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
# Dashboards and panels
|
||||
|
||||
A dashboard is a grid of widgets bound to message names — the same names that
|
||||
wire the graph. It is its own document rather than a set of nodes, so it reads
|
||||
across flows without belonging to any of them, and a flow stays the logic it
|
||||
was.
|
||||
|
||||
There is no second tool and no separate deployment. You build the panel next to
|
||||
the thing feeding it.
|
||||
|
||||
## Building one
|
||||
|
||||
**Dashboards → New dashboard**, then drag widgets onto the grid from the widget
|
||||
picker and bind each to a message.
|
||||
|
||||
Like flows, dashboards are drafts until you publish. A wall panel reads only
|
||||
the published version, so a half-arranged page never reaches the wall.
|
||||
|
||||
On a phone the dashboard is read, not arranged: the grid stacks to one column
|
||||
and dragging is off. Picking a widget and editing its settings still works.
|
||||
|
||||
## The widgets
|
||||
|
||||
### Display
|
||||
|
||||
| Widget | Binds to | Notes |
|
||||
|---|---|---|
|
||||
| **Value** | anything | a formatted reading with a unit and a precision |
|
||||
| **Gauge** | `float`, `int` | min, max, unit |
|
||||
| **Chart** | `float`, `int` | up to five series; see *Two kinds of chart* below |
|
||||
| **Bar** | `float`, `int` | a reading, optionally nesting up to three inside it |
|
||||
| **Icon** | numbers, booleans, weather strings | maps a value onto a glyph |
|
||||
| **Text** | — | markdown you write; a label, a note, an instruction |
|
||||
| **Agenda** | `list` | upcoming items, e.g. from a calendar connector |
|
||||
| **Forecast** | `list` | a short outlook strip |
|
||||
| **Notification** | `record` | title, body and severity — what an alert channel writes |
|
||||
| **Clock** | — | the time, in a size a wall can read |
|
||||
|
||||
### Input
|
||||
|
||||
| Widget | Publishes | Notes |
|
||||
|---|---|---|
|
||||
| **Button** | a fixed value | one-shot: run it, open it, reset it |
|
||||
| **Switch** | `bool` | on/off |
|
||||
| **Slider** | `float`, `int` | min, max, step |
|
||||
| **Input** | text or a number | free entry |
|
||||
| **Dropdown** | one of a list | a mode, a scene, a preset |
|
||||
|
||||
A control publishes the message it is bound to, exactly as a node would. On the
|
||||
flow canvas it is drawn as a labelled endpoint feeding the nodes that read it,
|
||||
so nobody has to guess where the value came from.
|
||||
|
||||
### Typed bindings
|
||||
|
||||
Widgets are type-checked against the message the same way ports are: a switch
|
||||
takes a `bool`, a gauge takes a number, an agenda takes a `list`, a
|
||||
notification takes a `record`. Bind one wrong and the editor says so rather
|
||||
than drawing nothing. The same table is enforced on the server.
|
||||
|
||||
Charts are the exception worth knowing: because identity across five series is
|
||||
carried by lightness alone, a chart with more than one series always draws a
|
||||
legend.
|
||||
|
||||
## Two kinds of chart
|
||||
|
||||
**Live.** Bind up to five messages and the chart draws the engine's own ring
|
||||
buffer of recent values, extended over the socket as new ones land. Nothing
|
||||
else is involved. This is what you want for "the last few hours of the living
|
||||
room".
|
||||
|
||||
**Querying.** The chart publishes a *request* — the window and resolution it
|
||||
wants — exactly as a slider publishes a value, and draws the `series` some flow
|
||||
answers with. What serves that request is the flow's business: typically a
|
||||
small Python node that builds a Flux query, an InfluxDB node that runs it, and
|
||||
another Python node that shapes the rows.
|
||||
|
||||
That indirection is the point. The widget never learns which database answered
|
||||
it, so swapping the store is a change to one flow and nothing else. The answer
|
||||
also states what it was computed for, so an answer to a different question is
|
||||
ignored rather than two charts overwriting each other's picture.
|
||||
|
||||
## Showing one
|
||||
|
||||
- `/view/{name}` — a browser tab pointed at one dashboard. Needs an ordinary
|
||||
session.
|
||||
- **Panels** — a named device that pairs instead of logging in. Below.
|
||||
|
||||
## Panels: hanging a screen on a wall
|
||||
|
||||
A wall tablet has no keyboard, so it pairs.
|
||||
|
||||
1. **Dashboards → Panels**, add a panel named after where it hangs, and tick
|
||||
the dashboards it shows. More than one and the screen draws a rail to switch
|
||||
between them.
|
||||
2. Point the device's browser at the link the dialog shows. The device then
|
||||
displays a six-character code.
|
||||
3. Type that code into the same panel's **Pair device** field. The line under
|
||||
it names what is holding the code — check it is the screen you just hung,
|
||||
because approving adopts whatever answered. The screen picks the credential
|
||||
up within a few seconds and never asks again.
|
||||
|
||||
What the screen holds is not a login. It reaches that panel's published
|
||||
dashboards and the message endpoints its widgets speak, and nothing else.
|
||||
|
||||
It cannot be made strictly read-only, and that is honest rather than an
|
||||
oversight: a querying chart publishes its request, and a control on a panel is
|
||||
the reason you put one there. Deleting the panel revokes the credential, which
|
||||
is also how you retire a device — the screen falls back to asking for a new
|
||||
code.
|
||||
|
||||
!!! note "If the link is wrong"
|
||||
|
||||
The pairing link is built from the installation's `FRONTEND_HOST`. If that
|
||||
is not the address devices on your network actually reach, fix the setting
|
||||
rather than the link: it is the same one password-reset mails and the OAuth
|
||||
metadata are built from.
|
||||
|
||||
### A screen somewhere you cannot reach
|
||||
|
||||
Another building, someone else's network, no route in. An installation
|
||||
[enrolled with a portal](portal.md) shows a second link,
|
||||
`https://hub.${DOMAIN}/i/{installation-id}/panel`, and the same three steps
|
||||
work through it: the portal serves that one page without a session, forwards
|
||||
the pairing calls down the tunnel, and mints the credential when you approve
|
||||
the code. The pairing line then reads *via portal*.
|
||||
|
||||
The portal names the panel and nothing else. What the panel may read is decided
|
||||
on the installation, on every call, by the same check a locally paired screen
|
||||
passes. Two differences: it acts as the account the installation was enrolled
|
||||
with rather than as whoever approved it, and deleting the panel stops it here
|
||||
immediately while the portal's copy of the token expires on its own.
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,156 @@
|
||||
# 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.
|
||||
|
||||
## 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
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Adding a node
|
||||
|
||||
**Add node** on the dock, or ⌘K / Ctrl-K 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.
|
||||
|
||||
## The node panel
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
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 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
|
||||
ports. See [Where a node's values come from](../concepts/values.md).
|
||||
|
||||
**Timeout** (Function nodes) is how many seconds the code may run before it is
|
||||
stopped. Once a node streams, this becomes an *idle* timeout: it measures
|
||||
silence, not duration, so a node that yields every few seconds can run for
|
||||
hours under a timeout of 300.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
!!! 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
|
||||
[Remote workers](../code/workers.md).
|
||||
|
||||
## The flow panel
|
||||
|
||||
Click the canvas title to open the flow's own settings.
|
||||
|
||||
- **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.
|
||||
|
||||
## 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 — the fastest way to test
|
||||
a change without waiting for the real sensor.
|
||||
|
||||
## The dock
|
||||
|
||||
Along the bottom, floating over the canvas.
|
||||
|
||||
| Control | What it does |
|
||||
|---|---|
|
||||
| **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 |
|
||||
|
||||
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.
|
||||
|
||||
## Shortcuts
|
||||
|
||||
| Chord | Action |
|
||||
|---|---|
|
||||
| ⌘K / Ctrl-K | 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 |
|
||||
|
||||
## Drafts and publishing
|
||||
|
||||
Saving writes a draft; the engine keeps running what was published. Publishing
|
||||
promotes it and reloads.
|
||||
|
||||
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.
|
||||
|
||||
## Issues
|
||||
|
||||
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 flow with any of these except the last does not run, and the health summary
|
||||
on Home counts it.
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,88 @@
|
||||
# The dashboard app
|
||||
|
||||
The web interface is a single-page app served at `app.${DOMAIN}` — or, for an
|
||||
installation reached through a portal, at `${DOMAIN}/i/{installation-id}`.
|
||||
Either way it is the same application, and it is a client of the same REST API
|
||||
you can script against.
|
||||
|
||||
Sign in with the account the installation was created with. On a fresh
|
||||
installation that account was printed once, on the first start.
|
||||
|
||||
## The shell
|
||||
|
||||
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 |
|
||||
|---|---|
|
||||
| **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 |
|
||||
| **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) |
|
||||
| **Settings** | your account, appearance, and remote access |
|
||||
|
||||
## Home
|
||||
|
||||
The one screen you leave open. Three 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
|
||||
installation: 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.
|
||||
|
||||
Each flow also has a switch beside it in the list, which starts and stops it.
|
||||
|
||||
### 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
|
||||
- **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.
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
## Flows
|
||||
|
||||
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).
|
||||
|
||||
## 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
|
||||
installation running
|
||||
- [Accounts and the portal](portal.md) — reaching an installation from outside
|
||||
its network
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,131 @@
|
||||
# Secrets, modules and alerts
|
||||
|
||||
Three screens that have nothing to do with each other except that an
|
||||
installation you actually depend on needs all of them.
|
||||
|
||||
## Secrets
|
||||
|
||||
A node never holds a credential. A parameter written as
|
||||
|
||||
```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.
|
||||
|
||||
**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.
|
||||
|
||||
They are encrypted at rest with a key derived from the installation'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.
|
||||
|
||||
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.
|
||||
|
||||
## Modules
|
||||
|
||||
Node code runs in a virtual environment of its own, on the installation'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** is a pip manifest and an **Apply** button. 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.
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
## Alerts
|
||||
|
||||
Everything that goes wrong already travels the engine's event bus. Alerts is
|
||||
where you say who hears about it.
|
||||
|
||||
### Channels
|
||||
|
||||
| Kind | Settings |
|
||||
|---|---|
|
||||
| **ntfy** | server, topic, and a token for a protected topic |
|
||||
| **SMTP** | an address to send to (the installation's mail settings do the rest) |
|
||||
| **Webhook** | a URL to POST to |
|
||||
| **Dashboard** | a message name a notification widget reads |
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
| Event | Fires when |
|
||||
|---|---|
|
||||
| `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 |
|
||||
|
||||
**Test** sends one message through a channel so you find out it is misconfigured
|
||||
now rather than at three in the morning.
|
||||
|
||||
### What it mostly does is not send
|
||||
|
||||
This is the part worth knowing before you decide it is broken:
|
||||
|
||||
- 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. 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.
|
||||
|
||||
## Settings
|
||||
|
||||
Your own account: name, email, password, and appearance (light, dark, or
|
||||
follow the system).
|
||||
|
||||
**Remote access** is where an installation is paired with a portal, remote
|
||||
users are admitted, and the link is cut again. That has [its own
|
||||
page](portal.md).
|
||||
@@ -0,0 +1,117 @@
|
||||
# Accounts and the portal
|
||||
|
||||
Fluksio is self-hosted by default. An installation runs offline, keeps its data
|
||||
on its own disk, and never contacts anything unless you tell it to.
|
||||
|
||||
The **portal** is optional, and it exists to solve two specific problems:
|
||||
|
||||
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` installation
|
||||
has no web server for the dashboard at all.
|
||||
|
||||
An enrolled installation 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.
|
||||
|
||||
## Enrolling
|
||||
|
||||
Two halves, deliberately: whoever performs the second step decides what the
|
||||
installation's owner gets.
|
||||
|
||||
**On the portal** (`hub.${DOMAIN}`, or [fluksio.com](https://fluksio.com) for
|
||||
the hosted one): **Installations → Add installation**, give it a name, and copy
|
||||
the code.
|
||||
|
||||
**On the installation**, 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 installation with no web
|
||||
interface of its own:
|
||||
|
||||
```sh
|
||||
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
|
||||
installation has several superusers.
|
||||
|
||||
Once enrolled, the installation appears under **Installations** with its
|
||||
status, when it was last seen and its version. **Open** takes you to its
|
||||
dashboard at `${DOMAIN}/i/{installation-id}`.
|
||||
|
||||
## What the portal can and cannot do
|
||||
|
||||
The portal holds one credential for your installation and proxies requests down
|
||||
the tunnel. What those requests may do is decided **on the installation**, by
|
||||
the same checks a local session passes.
|
||||
|
||||
The trust anchor is a signing keypair on the portal. Every installation 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.
|
||||
|
||||
## Letting someone else in
|
||||
|
||||
Anyone else on the portal reaches your installation only if a superuser there
|
||||
admits them, and they arrive as a local user of their own rather than as you.
|
||||
|
||||
1. **They**: **Installations → Join an installation**, and copy the code. It is
|
||||
bound to their portal account and expires in fifteen minutes.
|
||||
2. **You**, on the installation: **Settings → Remote access → Add remote
|
||||
user**, and enter the code.
|
||||
3. They now see the installation under **Installations**, marked *Shared by*,
|
||||
with **Open** and nothing else. Renaming, re-keying and removing stay with
|
||||
you.
|
||||
|
||||
The installation 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
|
||||
letting others in.
|
||||
|
||||
On the installation they appear under **Admin → Users**, badged *Portal*, never
|
||||
a superuser and with no password.
|
||||
|
||||
## Cutting it off
|
||||
|
||||
| From | Action | Effect |
|
||||
|---|---|---|
|
||||
| The portal | **New code** | rotates the credential and drops the current link |
|
||||
| The portal | **Remove** | deletes the registration and cuts the connection |
|
||||
| The installation | **Disconnect** | unilateral and immediate — the portal's tokens stop verifying here whatever the portal still has on file |
|
||||
| The installation | 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 installation,
|
||||
including [wall panels paired through it](dashboards.md#a-screen-somewhere-you-cannot-reach).
|
||||
|
||||
The installation'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 installations, and the websocket each one dials in on. Two things
|
||||
about it are load-bearing:
|
||||
|
||||
- **It runs a single process.** It keeps its attached installations 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
|
||||
installation to be enrolled again.
|
||||
|
||||
Websocket upgrades must be enabled on the `hub` hostname in whatever proxy
|
||||
fronts it. Without them every installation sits in a reconnect loop and the
|
||||
portal shows them all offline.
|
||||
|
||||
## 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
|
||||
Reference in New Issue
Block a user