Make the docs state things rather than argue them
Docs / docs (push) Successful in 37s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m35s
Playwright Tests / test-playwright (2, 2) (push) Failing after 17s
pre-commit / pre-commit (push) Failing after 2m8s
Test Backend / test-backend (push) Failing after 2m48s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m25s
Docs / docs (push) Successful in 37s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m35s
Playwright Tests / test-playwright (2, 2) (push) Failing after 17s
pre-commit / pre-commit (push) Failing after 2m8s
Test Backend / test-backend (push) Failing after 2m48s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m25s
The site read as a design journal: rationale paragraphs, hedges
("deliberately", "on purpose", "genuinely"), meta-commentary about the docs
themselves, and one em-dash every ten lines carrying an aside.
Roughly twenty rationale blocks are gone or reduced to what a reader needs
in order to use the thing. Em-dashes go from 507 to 135, and what is left is
structural rather than prose: list and definition separators, table cells,
and four inside code blocks that quote what the CLI actually prints.
Also: api.example.com becomes api.fluksio.com (the emails stay, since
bootstrap.py really defaults to admin@example.com and RFC 2606 reserves it);
the mqtt table gains the two settings it had drifted behind on and inject's
wording matches the engine; llms.txt lists the two connector pages that were
in the nav but not in it; and the two device/device_policy notes now agree.
Builds clean under `zensical build --strict`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
This commit is contained in:
@@ -17,7 +17,7 @@ Anything already exported wins over the file.
|
||||
| `DATA_DIR` | `flow-data` (`./.fluksio` via the CLI; `~/.fluksio` with `--global`) | everything below derives from this |
|
||||
| `DATABASE_URL` | SQLite in `DATA_DIR` | any SQLAlchemy URL |
|
||||
| `FLOWS_DIR` | `$DATA_DIR/flows` | the git repository holding flows |
|
||||
| `SECRETS_FILE` | `$DATA_DIR/secrets.enc` | encrypted credentials, deliberately outside the repo |
|
||||
| `SECRETS_FILE` | `$DATA_DIR/secrets.enc` | encrypted credentials, kept outside the repo |
|
||||
| `ALERTS_FILE` | `$DATA_DIR/alerts.json` | channels and rules |
|
||||
| `PROVISIONERS_FILE` | `$DATA_DIR/provisioners.json` | clusters a machine can be started from; absent means none |
|
||||
| `PANELS_FILE` | `$DATA_DIR/panels.json` | wall-panel pairings |
|
||||
@@ -25,7 +25,7 @@ Anything already exported wins over the file.
|
||||
| `CLOUD_CONFIG_FILE` | `$DATA_DIR/cloud.json` | the portal enrolment, if any |
|
||||
| `NODE_VENV` | `auto` | which interpreter node code runs on — see below |
|
||||
|
||||
Set `DATA_DIR` and the rest follow. Set one explicitly and it wins — which is
|
||||
Set `DATA_DIR` and the rest follow. Set one explicitly and it wins, which is
|
||||
what the container images do to pin everything onto `/data`.
|
||||
|
||||
`NODE_VENV` is the exception, being about an environment rather than a path:
|
||||
@@ -43,7 +43,7 @@ An instance that already has a managed venv keeps it on upgrade under
|
||||
|
||||
`secrets.enc`, `alerts.json`, `panels.json` and `oauth-key.pem` are written
|
||||
at runtime. In a container, anything not on a volume lands in the writable
|
||||
layer and is lost on the next rebuild — un-pairing every screen and
|
||||
layer and is lost on the next rebuild, un-pairing every screen and
|
||||
revoking every agent.
|
||||
|
||||
## State
|
||||
@@ -54,8 +54,7 @@ An instance that already has a managed venv keeps it on upgrade under
|
||||
| `REDIS_PORT` | `6379` | |
|
||||
|
||||
Flow state is the last value of every message, node memory, and the run queue.
|
||||
Redis here is persistence, not a cache — run it with append-only persistence
|
||||
on.
|
||||
Redis here is persistence, not a cache. Run it with append-only persistence on.
|
||||
|
||||
## Identity and access
|
||||
|
||||
@@ -85,7 +84,7 @@ there to satisfy.
|
||||
|
||||
Empty is the useful value: the interface then addresses the API relative to
|
||||
whichever origin served the page, so one image answers on a hostname, on a
|
||||
`http://<host-ip>:<port>`, and through an ssh tunnel alike — and no origin has
|
||||
`http://<host-ip>:<port>`, and through an ssh tunnel alike, and no origin has
|
||||
to be added to `BACKEND_CORS_ORIGINS`, because there is only one.
|
||||
`docker/compose.lan.yml` builds it that way and puts an `/api` proxy in front
|
||||
of the backend to complete it; see
|
||||
@@ -115,7 +114,7 @@ o'clock nor the same hour in summer as in winter. Set it to where the
|
||||
instance is.
|
||||
|
||||
`production` closes `/docs`, `/redoc` and the OpenAPI document, because the
|
||||
schema enumerates every endpoint the instance serves — including the paths
|
||||
schema enumerates every endpoint the instance serves, including the paths
|
||||
webhook nodes mounted at runtime. It also turns a `changethis` secret from a
|
||||
warning into a refusal to start.
|
||||
|
||||
@@ -133,8 +132,9 @@ warning into a refusal to start.
|
||||
| `ARTIFACT_GC_INTERVAL_S` | `3600` | how often artifact bytes nothing refers to are swept away; 0 never sweeps |
|
||||
| `ARTIFACT_GC_GRACE_S` | `3600` | how long a freshly written artifact is spared, whatever refers to it |
|
||||
|
||||
The three concurrency limits are also flags on `fluksio serve` — `--max-workers`,
|
||||
`--max-cascades`, `--max-runs` — as is the card count, `--gpus`. The flags
|
||||
The three concurrency limits are also flags on `fluksio serve`
|
||||
(`--max-workers`, `--max-cascades`, `--max-runs`), as is the card count,
|
||||
`--gpus`. The flags
|
||||
outrank the file, and the engine says which numbers it started with in its
|
||||
first lines. Each pool size must be at least 1 and the card count at least 0:
|
||||
a number below that is refused as a flag error naming it, rather than read as
|
||||
@@ -153,7 +153,7 @@ several at once cannot each size themselves to the whole machine. Setting
|
||||
that default.
|
||||
|
||||
These two are this machine's figures. An attached worker reports its own when
|
||||
it dials in, and a node goes to whichever machine can grant what it asked for —
|
||||
it dials in, and a node goes to whichever machine can grant what it asked for,
|
||||
so a GPU on a worker needs no `FLOW_GPUS` here. What every machine has free, and
|
||||
which nodes are queued, is `GET /api/v1/workers/resources` and the Workers
|
||||
screen. Named sizes live in the database and are `GET /api/v1/flavors`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# The connector contract
|
||||
|
||||
A connector is the device-facing node class. It talks to something outside the
|
||||
engine — a device, a service, a protocol — and publishes what it finds as
|
||||
engine (a device, a service, a protocol) and publishes what it finds as
|
||||
ordinary typed messages, so the rest of a flow cannot tell the difference
|
||||
between a reading from a heat pump and one from a `print` statement.
|
||||
|
||||
@@ -75,7 +75,7 @@ class Params(ConnectorNode.Params):
|
||||
```
|
||||
|
||||
- Booleans render as switches, numbers and strings as inputs. Objects and
|
||||
arrays are not rendered — keep settings flat.
|
||||
arrays are not rendered, so keep settings flat.
|
||||
- A field carrying **`x-secret`** renders as a picker over the stored secrets
|
||||
and writes a reference, `{"$secret": "name"}`, rather than the value. The
|
||||
engine resolves it when the node is built, so a credential never lands in
|
||||
@@ -98,13 +98,13 @@ optional `interval`.
|
||||
at most that often. A connector should poll at the rate the device is
|
||||
comfortable with and leave delivery rates to whoever wires it up.
|
||||
- A key no port declares is an error, and it fails the whole reading rather
|
||||
than the one value — a mistyped metric name is how a training curve goes
|
||||
than the one value. A mistyped metric name is how a training curve goes
|
||||
missing.
|
||||
|
||||
### When the device decides what the ports are
|
||||
|
||||
A connector for a device whose readings vary by model — which components a
|
||||
relay has, which entities were flashed onto a board — cannot know its ports in
|
||||
A connector for a device whose readings vary by model (which components a
|
||||
relay has, which entities were flashed onto a board) cannot know its ports in
|
||||
advance, and returning everything the device reports would fail on the first
|
||||
value nobody bound. Narrow the reading to the ports that were declared:
|
||||
|
||||
@@ -115,7 +115,7 @@ return {port: value for port, value in reading.items() if port in declared}
|
||||
|
||||
`spec.port` is the local, unqualified name and stays that way for the node's
|
||||
whole life, so the set can be taken fresh each time. Say something in the log
|
||||
when a declared port is not one the device has — once, not once per poll: from
|
||||
when a declared port is not one the device has, once rather than once per poll: from
|
||||
the canvas a renamed entity and a typo look the same, and both leave a port
|
||||
silent forever.
|
||||
|
||||
@@ -131,7 +131,7 @@ async def poll(self) -> dict[str, Any] | None:
|
||||
- Return `None` when there is nothing new.
|
||||
- **Only changed values are published.** A device polled every few seconds
|
||||
usually says the same thing, and every publication wakes everything
|
||||
downstream, so the loop compares against what it last published — what it
|
||||
downstream, so the loop compares against what it last published, meaning what it
|
||||
actually published, so a publication that failed is retried next tick rather
|
||||
than counting as said.
|
||||
- Raising is not fatal: it is reported as a health problem and retried on the
|
||||
@@ -182,7 +182,7 @@ async def poll(self) -> dict[str, Any] | None:
|
||||
```
|
||||
|
||||
`save_artifact` stores the bytes and returns what an `image`, `audio` or
|
||||
`video` port carries — the media type has to match the port's type. It only
|
||||
`video` port carries: the media type has to match the port's type. It only
|
||||
works once the node has started, since the store is the engine's and is handed
|
||||
over then.
|
||||
|
||||
@@ -200,7 +200,7 @@ async def stop(self, app=None) -> None: ...
|
||||
|
||||
- `start` is called when the connector's flow starts, and after every rebuild.
|
||||
- `stop` is called before a rebuild and when the flow is stopped. **It must be
|
||||
idempotent** — it is called whether or not `start` succeeded.
|
||||
idempotent**: it is called whether or not `start` succeeded.
|
||||
- Overriding either means calling `super()` if the polling loop is also wanted.
|
||||
- The `app` argument is the FastAPI application, for the rare connector that
|
||||
needs to mount a route. Most ignore it.
|
||||
@@ -216,7 +216,7 @@ self.report_health("down", str(exc))
|
||||
```
|
||||
|
||||
Three values, `ok`, `degraded` and `down`, plus an optional detail string.
|
||||
Reporting the same status twice is free — only changes are published. A node
|
||||
Reporting the same status twice is free, since only changes are published. A node
|
||||
reporting `down` is named among its flow's issues and counted on the health
|
||||
summary on Home; `degraded` means still working, and is not. The polling loop
|
||||
already reports around `poll()`; a connector managing its own connection should
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
Every type the canvas can place. Each is configured by filling in a form the
|
||||
editor generates from its parameter schema, so they all behave the same way.
|
||||
|
||||
Anything here could be written as a **Function** node — that is what the
|
||||
function node is for. These exist because the same handful of shapes account
|
||||
for most of a real instance, and a rule you fill in is easier to read on a
|
||||
canvas, and to change, than five lines of code repeated eighty times.
|
||||
Anything here can also be written as a **Function** node. These types cover the
|
||||
shapes that come up most often, as a form to fill in rather than repeated code.
|
||||
|
||||
`GET /flows/node-types` returns this list with each type's full parameter
|
||||
schema, including any [connectors](#connectors) installed on your instance.
|
||||
@@ -40,13 +38,15 @@ A node with *outputs only* subscribes; a node with *inputs* publishes.
|
||||
| `qos` | `0` | 0, 1 or 2 |
|
||||
| `retain` | `false` | on published messages |
|
||||
| `keepalive` | `60` | seconds |
|
||||
| `timeout` | `10.0` | seconds before a broker operation is given up on |
|
||||
| `publish_queue_size` | `256` | payloads that may wait for the broker; past this the oldest is dropped and the node reports degraded |
|
||||
| `json_key` | — | key to lift out of an object payload; one for every port, or `{"port": "key"}` |
|
||||
|
||||
A topic may be a filter: `+` matches one level, `#` the rest. Everything a
|
||||
filter matches lands on the same port, so use one port per topic where the
|
||||
difference matters.
|
||||
|
||||
`json_key` is for a device that wraps its reading — Victron publishes
|
||||
`json_key` is for a device that wraps its reading: Victron publishes
|
||||
`{"value": 47}` on every path. Without it, a payload object is unwrapped only
|
||||
when it happens to carry the port's own name as a key.
|
||||
|
||||
@@ -100,10 +100,8 @@ Outputs only makes it a **webhook**: the engine mounts a route at
|
||||
written, and the rows come back on the first output port as
|
||||
`{"rows": [{ts, value, field, measurement, tags}], ...}`.
|
||||
|
||||
That is what keeps a database node a database node: it holds the credentials
|
||||
and the connection and nothing else. Building a query and shaping its rows are
|
||||
ordinary Function nodes on either side, so a dashboard widget never learns which
|
||||
database answered it.
|
||||
A database node holds the credentials and the connection, nothing else.
|
||||
Building a query and shaping its rows are Function nodes on either side.
|
||||
|
||||
### Notification
|
||||
|
||||
@@ -126,7 +124,7 @@ thing configured elsewhere.
|
||||
|
||||
### Inject
|
||||
|
||||
**`inject`** — emit a value on request, on a schedule, or when the flow starts.
|
||||
**`inject`** — emit a value on request, on a timer, or when the flow starts.
|
||||
|
||||
| Setting | Default | Notes |
|
||||
|---|---|---|
|
||||
@@ -138,8 +136,8 @@ thing configured elsewhere.
|
||||
| `start_delay` | `1.0` | how long to wait before that first emission |
|
||||
|
||||
The scheduler: a `cron` expression here is what makes a flow run by the clock.
|
||||
It is also the most-placed node in a real instance — mostly as a button
|
||||
someone presses.
|
||||
It is also the most-placed node in a real instance, mostly as a button someone
|
||||
presses.
|
||||
|
||||
### Delay & schedule
|
||||
|
||||
@@ -165,7 +163,7 @@ needs a busy cascade slot waits for one.
|
||||
!!! note "Not in a batch flow"
|
||||
|
||||
A rate limit holds a value back for a timer to release, and a run has no
|
||||
timer — the value would be dropped rather than delayed. Submitting a batch
|
||||
timer, since the value would be dropped rather than delayed. Submitting a batch
|
||||
flow with a rate-limited port is refused instead.
|
||||
|
||||
### Trigger
|
||||
@@ -183,13 +181,13 @@ starts from a value arriving. For a cron tick, see [`inject`](#inject).
|
||||
| `passthrough` | `false` | send the incoming value instead of `first` |
|
||||
| `wait_port` | — | an input carrying the wait, when it differs per message |
|
||||
|
||||
The shape this exists for: *the door opened — turn the light on, and off again
|
||||
in two minutes unless it opens again.*
|
||||
The shape this exists for: *the door opened, so turn the light on, and off
|
||||
again in two minutes unless it opens again.*
|
||||
|
||||
`wait_port` covers the case where how long to wait is itself a value: a
|
||||
rollershutter takes 26 seconds up and 28 down, so the node that decides the
|
||||
direction says how long to run for as well. A wait of zero or less sends
|
||||
nothing afterwards — and still cancels whatever the last message scheduled,
|
||||
nothing afterwards, and still cancels whatever the last message scheduled,
|
||||
which is how a *stop* is commanded exactly once.
|
||||
|
||||
## Logic
|
||||
@@ -289,13 +287,12 @@ reloads identically.
|
||||
|---|---|---|
|
||||
| `seed` | `0` | |
|
||||
|
||||
Kept as a worked example of numeric logic rather than as a modelling tool. If
|
||||
you are training something, that is a [batch flow](../concepts/runs.md) and a
|
||||
Function node.
|
||||
A worked example of numeric logic, not a modelling tool. To train something,
|
||||
use a [batch flow](../concepts/runs.md) and a Function node.
|
||||
|
||||
## Connectors
|
||||
|
||||
Anything else in the palette came from an installed **connector** package — a
|
||||
Anything else in the palette came from an installed **connector** package: a
|
||||
node type written against a published contract and discovered through the
|
||||
`fluksio.node_types` entry point group. The editor shows which package supplied
|
||||
it.
|
||||
|
||||
@@ -20,11 +20,10 @@ the state backend, the work queue and the worker protocol unchanged.
|
||||
| `str` | a string |
|
||||
| `bool` | exactly `true` or `false` |
|
||||
|
||||
`bool` is an `int` subclass in Python and deliberately not a number here: a flag
|
||||
is not a measurement, and a switch bound to a temperature is a mistake worth
|
||||
catching.
|
||||
`bool` is an `int` subclass in Python but is not a number here, so a switch
|
||||
cannot bind to a temperature.
|
||||
|
||||
`NaN` and infinity are refused, wherever they sit — including inside a `json`,
|
||||
`NaN` and infinity are refused, wherever they sit, including inside a `json`,
|
||||
`record`, `series` or `list`. JSON cannot spell either, so one that travelled
|
||||
would come back as a response nobody can parse and a row the database rejects,
|
||||
a long way from the node that made it. An empty subset or a division with no
|
||||
@@ -43,8 +42,8 @@ Flat named scalars.
|
||||
{"title": "Boiler", "body": "Pressure low", "severity": "warning"}
|
||||
```
|
||||
|
||||
Nesting is deliberately out: a record that can contain a record is a schema
|
||||
language, and the shape stops being readable from the declaration alone.
|
||||
A record cannot contain a record, so its shape is readable from the declaration
|
||||
alone.
|
||||
|
||||
Read by the **Notification** widget. It is also what an alert channel of kind
|
||||
*dashboard* writes.
|
||||
@@ -64,7 +63,7 @@ point.
|
||||
|
||||
### `series`
|
||||
|
||||
Labelled lines of `(timestamp, value)` pairs — what a chart draws.
|
||||
Labelled lines of `(timestamp, value)` pairs, which is what a chart draws.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -91,7 +90,7 @@ A reference to stored bytes.
|
||||
{"digest": "sha256:…", "size": 4194304, "media_type": "application/octet-stream", "name": "weights.pt"}
|
||||
```
|
||||
|
||||
Binary payloads — tensors, checkpoints, images — never travel as a message. The
|
||||
Binary payloads (tensors, checkpoints, images) never travel as a message. The
|
||||
bytes go to a content-addressed store and the message carries this. A
|
||||
thirty-megabyte checkpoint never sits in the state backend, and the reference
|
||||
stays valid wherever the store is reachable from, including on another machine.
|
||||
@@ -110,14 +109,14 @@ The same reference, narrowed to a kind of media by its `media_type`.
|
||||
|
||||
An `audio` port takes `audio/*` and refuses anything else, so a speech
|
||||
recogniser declares what it eats rather than taking any bytes at all and
|
||||
finding out. An `artifact` port still accepts all three — media narrows
|
||||
finding out. An `artifact` port still accepts all three: media narrows
|
||||
artifact, not the other way round.
|
||||
|
||||
`meta` is optional and nothing here reads it: sample rates, dimensions and
|
||||
sequence numbers are for whoever consumes the media.
|
||||
|
||||
Bytes still never travel as a message. A camera publishes one reference per
|
||||
frame and a microphone one per chunk — which makes a media stream an ordinary
|
||||
frame and a microphone one per chunk, which makes a media stream an ordinary
|
||||
[streaming port](../concepts/flows.md#streaming-ports), and each frame an
|
||||
artifact. What that costs is worth knowing before pointing a camera at it:
|
||||
|
||||
@@ -143,8 +142,8 @@ next author nothing.
|
||||
## Naming a run's output
|
||||
|
||||
Any *run parameter* is also accepted as text, since nobody wants to paste an
|
||||
object into a shell. `@run:<id>.<output>` names what an earlier run produced —
|
||||
whatever its type, an `artifact` reference or a `json` config alike — and a bare
|
||||
object into a shell. `@run:<id>.<output>` names what an earlier run produced,
|
||||
whatever its type, an `artifact` reference or a `json` config alike. A bare
|
||||
`sha256:…` digest names content in the artifact store. Both resolve before the
|
||||
run starts, so the CLI, the run dialog and a python caller all mean the same
|
||||
thing by the same string.
|
||||
@@ -171,8 +170,8 @@ Enforced on the server as well as in the editor.
|
||||
|
||||
A value that does not match its port's declaration raises on the node that
|
||||
published it, naming the port and what arrived. It does not get published, and
|
||||
it does not reach anything downstream — a wrong value stopping at its source is
|
||||
much easier to diagnose than one propagating.
|
||||
it does not reach anything downstream. A wrong value stopping at its source is
|
||||
easier to diagnose than one propagating.
|
||||
|
||||
## See also
|
||||
|
||||
|
||||
Reference in New Issue
Block a user