Commit Graph
83 Commits
Author SHA1 Message Date
stroblme dea25ec08d One image for the whole app: engine, API and dashboard on one port
The backend image now builds the SPA and serves it on the API's own origin
(fluksio/spa.py), so an instance is a single container with no proxy, DNS,
port 80 or .env: `docker compose up -d` with the new compose.yml runs it on
localhost:8080. It starts on fluksio serve (persisted secret key, first
admin printed once) and brings its own Redis when REDIS_HOST is unset.

Slimmer: python:3.13-slim, no dev group, uv once, fastapi without the cloud
CLI. Optional features are per-feature extras (mqtt, influx, email, sentry,
ml, server for all) picked with the EXTRAS build argument; numpy moved to
`ml`. ffmpeg left the image for imageio-ffmpeg via the Modules screen.

The operator stack runs the same image with app. and api. routed to it; the
nginx frontend service, the LAN nginx proxy and DOMAIN in the backend are
gone, and the MCP issuer is FRONTEND_HOST.
2026-10-16 11:40:02 +02:00
stroblme c37a191c82 Overview lists: a duplicate button beside the trash while selecting copies the picked flows or dashboards as drafts, one request each, then clears the selection. Publish, delete and duplicate share one batch helper with their own toasts. Docs 2026-10-16 09:21:42 +02:00
stroblme ce2ce4e3e7 Header removals wait for the code editor to be left: autosave saves with drop=false, so follow_header adds and renames from the header but keeps a port or setting whose parameter is gone, and leaving the editor (blur or closing the panel) saves once more with removals; source saves of a flow run one at a time 2026-10-15 21:31:04 +02:00
stroblme abfb03c441 Header of process and node ports follow each other both ways: a panel edit rewrites only the def line of edited code, a code save reads the header back into input ports and settings (draft and response); Space types in the node editor again 2026-10-15 18:19:19 +02:00
stroblme 1b5a16f636 Assistant streams its answer token by token
ai/client.py streams the completion and puts the chunks back together
(Assembler), covering OpenAI's piecewise tool-call arguments, Ollama's whole
calls, OpenRouter's comment lines, unnumbered calls and in-stream errors.
The turn emits a delta event per piece and a message per round as before,
and the chat grows the last answer as deltas arrive.
2026-10-15 17:37:56 +02:00
stroblme 8f8330c9e0 Assistant answers render as markdown
react-markdown, lazy-loaded with the first answer (37 kB gzip chunk, main
bundle unchanged). No raw HTML, images dropped, links open in a new tab.
The system prompt asks for lists, inline code and fenced code blocks.
2026-10-15 17:18:12 +02:00
stroblme 4b216dcbcf Modules resolve the manifest with uv pip compile before syncing, so listed packages bring their dependencies 2026-10-15 17:06:20 +02:00
stroblme d3b52bd7d6 Docs: speech-to-text and text-to-speech recipes for a panel's mic and speaker
faster-whisper and piper as plain Function nodes, the mic -> STT -> logic ->
TTS -> speaker wiring, the Modules manifest they need (uv pip compile, av<19)
and measured sizes, memory and CPU times. Modules docs now say uv pip sync
installs no dependencies.
2026-10-15 17:01:25 +02:00
stroblme 96618e40a2 Opt-in WITH_FFMPEG build arg installs Debian's ffmpeg in the backend image
Off by default, since it adds about 400 MB. Documented with the clip recipe
and in the configuration reference.
2026-10-15 15:55:02 +02:00
stroblme 3a9fc43707 Workers screen shows each agent's version and protocol, and who is behind
The hello carries the fluksio-worker version. An agent refused for an older
protocol stays listed as Needs upgrade until it attaches again. Docs gain an
Upgrading workers section.
2026-10-15 11:09:54 +02:00
stroblme f569f325a7 Serve MCP and hooks with a secret through the portal tunnel
The connector now serves /mcp, its OAuth documents and /hooks/ besides
/api/v1/, refuses any dot segment (httpx resolves them, which carried
/api/v1/../../mcp onto the MCP mount), addresses calls to the hub's host,
and bridges served sockets by calling the app with a websocket scope.
A hook without a secret answers the tunnel with the 404 (or 1008) of a
wrong secret. Requests through the hub get the hub's issuer, resource and
consent page in the OAuth metadata and the 401, and the MCP host check
admits the hub while enrolled.
2026-10-15 11:00:19 +02:00
stroblme 4c20f913e8 Pin the UTC offset on the time a flow version carries
Every datetime the API emits for versions (flow summaries' updated_at,
runs' created/started/finished with flow_version) is already ISO 8601
with Z or +00:00. A test holds it and the API docs say so.
2026-10-15 10:57:41 +02:00
stroblme b9078dd19f fluksio run takes inputs after -- as name=value
So an input named like a flag of run, timeout or sync, can still be set.
Flags before -- keep naming inputs as they did.
2026-10-15 10:56:46 +02:00
stroblme e597c34823 Refuse an origin.repo of /, the home directory or a relative path
Every run hashes the python files below origin.repo, so saving or syncing
a flow with one of those now answers 422 with what to do instead.
2026-10-15 10:55:40 +02:00
stroblme 6fa44bacf3 Flow delete offers to delete its runs, history included
DELETE /runs?flow= removes every Run row of the flow but left its flow_run
rows, so a deleted flow kept appearing in the history. history=true now
drops those too, and the flow delete dialogs (editor and bulk) send it when
"Also delete its runs" is ticked.
2026-10-15 10:52:33 +02:00
stroblme 7ea53ccf17 DELETE /artifacts/{digest}: superuser only, 409 while referenced
The keep-set the hourly sweep uses is now referenced_artifacts, shared by
the sweep and the route. The volatile ring is not touched.
2026-10-15 10:48:59 +02:00
stroblme 21cfed7d39 GET /runs/{id}/nodes/{node}/outputs serves what a node returned
The stage cache's stored outputs, as kept (up to 256 kB of JSON). A node
that kept none answers 404 saying why. Listed in the API docs.
2026-10-15 10:23:39 +02:00
stroblme 43498037be PUT /panels/ carries a version and refuses a stale one with 409
The Panels dialog re-reads the list on 409. A panels.json without a
version reads as 0.
2026-10-15 10:07:50 +02:00
stroblme f2a823218a GPUs follow the engine's own CUDA_VISIBLE_DEVICES
Its entries are the cards present, and a local child's allocation is
named through them, so under =2,3 card 0 is device 2. Remote workers
keep mapping against their own mask.
2026-10-15 09:53:38 +02:00
stroblme 290b9fa705 Superuser route to flush the metrics collector now
POST /observability/flush handles what the collector has queued and writes
it, so the soak harness no longer sleeps FLUSH_INTERVAL_S before clearing
its own rows.
2026-10-15 09:53:19 +02:00
stroblme 1f213460ad Audit shared-library source writes
A shared source goes live in every flow using it the moment it is saved, so
the write is recorded in the audit trail like a publish. Private source saves
stay drafts and are not audited.
2026-10-15 09:50:04 +02:00
stroblme 0004e4b9bd Retire a shared node in use with ?detach=true
DELETE /flows/library/{name} still answers 409 while nodes use the source.
With detach=true each of them keeps a copy of the code, in the published
flow and the draft, the source is deleted and the running users rebuild.
2026-10-15 09:48:47 +02:00
stroblme 1813b5d722 Runs export: a truncated one counts the selection into X-Total-Count; the SDK and fluksio export warn how many runs were left out 2026-10-14 23:29:48 +02:00
Melvin Strobl 0264310d4f Defer ffmpeg in the image; record the decision 2026-10-14 07:33:59 +02:00
Melvin Strobl 1964c5b2e8 Clip transcoding: ffmpeg in the backend image, panel recipe in the node docs 2026-10-14 07:30:36 +02:00
stroblme 80e595c02e docs 2026-10-13 10:02:20 +02:00
stroblme 1424e1e8a8 Drop --token from the hand-started worker examples
The worker reads FLUKSIO_WORKER_TOKEN itself; on the command line the
token shows in ps. The --token row in the options table says so.
2026-10-07 09:35:00 +02:00
stroblme ff3a31e515 Hand a provisioned worker its token in the environment, not argv
The generated batch script exported nothing and passed `--token`, which
any user on a shared compute node reads from `ps`. It now exports
FLUKSIO_WORKER_TOKEN after `prerun`, which the worker already reads.
2026-10-07 09:32:01 +02:00
stroblme ed8aee2387 Authenticate sockets by header or one-time ticket instead of ?token= 2026-10-07 08:58:22 +02:00
stroblme 953a983919 Bind a worker's name and labels into its token; list and revoke tokens by jti 2026-10-07 08:42:14 +02:00
stroblme 243d39b591 Give connectors a store that survives restarts, and a way to clear it
`ConnectorNode.store` keeps short strings per connector package in the
engine's database (`connector_state`, migration f8c3e5a91d27), so the nodes of
one package share them and `fluksio serve` needs nothing new: get, set and
delete, at most 64 kB a value and 256 keys a package. A superuser lists the
keys (never the values) and clears them through `/connectors/{node_type}/state`,
or under Stored state in a connector node's panel. Documented in the connector
contract.
2026-10-06 17:19:48 +02:00
stroblme 07d62e3ae6 Use a placeholder password in the docs; ignore *.pem and *.key 2026-10-06 12:30:57 +02:00
stroblme 3758415f21 Enrol over http only to loopback; serve the API docs to this machine
The enrolment exemption covered any *.local host, which mDNS answers with
whoever claims it; it is now localhost, 127.0.0.0/8 and ::1. /docs, /redoc
and openapi.json stay closed in production and otherwise answer only a
loopback caller unless API_DOCS_ENABLED is set, which the dev compose does,
so `fluksio serve --host 0.0.0.0` no longer hands the schema to the network.
2026-10-06 12:20:13 +02:00
stroblme db045770e8 Harden the remote worker: TLS off loopback, full digests, filtered env
fluksio-worker now refuses ws:// and http:// engine and artifact URLs unless
they are loopback or --insecure is given; the engine still accepts both.
An artifact reference must be sha256: plus 64 hex digits before it is joined
into a path, and node code gets the host environment through the engine's
credential denylist instead of all of it.
2026-10-06 12:18:30 +02:00
stroblme 6802cd2203 Make changing engine-run code superuser-only
Writing node source, installing modules and any flow save that adds,
retypes or repoints a code node (Function, Command), edits a Command
node's settings or changes a node's resources.env now answer 403 for
everyone but a superuser. MCP tools and the assistant reach the same
routes, so they inherit the gate. Node types report runs_code.

The editor shows code read-only with a note, disables a Command node's
settings, leaves code nodes and reusable nodes out of the palette and
paste, and the Modules page offers no install.
2026-10-06 11:36:30 +02:00
stroblme aa7d9eece8 Document the wren node and the chat's Control tab 2026-10-05 20:59:23 +02:00
stroblme f09e0ffc44 Spell the API host as api.${DOMAIN} in the worker and login examples 2026-10-05 16:54:09 +02:00
stroblme 0649062327 Bound warm workers' host memory and account local RAM
A worker over FLOW_WORKER_MAX_RSS_MB (--worker-max-rss) when its node
returns is retired instead of kept warm, in the shared pool and in every
declared one; 0, the default, keeps them all. The engine now accounts its
own memory (FLOW_RAM / --ram, else the box or the cgroup limit less 1 GB),
so a node's `ram` declaration holds locally. A declared environment the
shared pool already has is that pool rather than a duplicate set of warm
workers.
2026-09-27 16:11:13 +02:00
stroblme 7240b7b823 Add the assistant: a chat that drives the MCP tools in-process
A provider set under Settings (OpenAI-compatible; key encrypted in
flow-data/ai.json) turns on an Assistant panel in the sidebar. Each turn runs
the FastMCP tools in-process as the person chatting, streams the rounds over
SSE and keeps one transcript per user. The panel floats on any screen and
shares the editor's lane: a settings panel keeps the top half, the chat the
bottom. The MCP client is now built once, for the endpoint and the chat.
2026-09-16 23:28:23 +02:00
stroblme af18d134a4 Add POST /runs/import for runs executed elsewhere
A history recorded before the move (MLflow experiments) had nowhere to
go, since runs were only made by running a flow. Each entry is stored as
it ended: parameters completed and checked like a submit, cause
"import", empty commit and code digest, metrics and artifacts written
as given, nothing queued or announced. Run.external_id (new migration)
makes a second import a no-op and is an export column so a row joins
back. The request is all or nothing and capped at 200 entries;
Client.import_runs pages a longer history.
2026-09-16 17:01:07 +02:00
stroblme 831bda9c46 Sync only the declaring module before a run; skip git-ignored paths
`fluksio run` walked the working directory and imported every module in
it, which ran a research repository's analysis scripts before the run was
submitted. A flow's origin now records the file and module name that
declared it, and run/sweep import just that, under the same name so the
generated node bodies do not change. A flow with no record still walks,
now skipping what .gitignore covers.

Every code-defined flow reports `flow` changed once on its next sync,
since the origin gained two fields.
2026-09-16 16:55:02 +02:00
stroblme f950cfbf6c Bind serve's socket before writing client.json
A serve that lost the port race had already signed in, so client.json
named a port another engine held and every client talked to that one.
The socket is now bound first and handed to uvicorn.
2026-09-16 16:47:11 +02:00
stroblme 02907bbe27 Leave a dotted key out of the runs export header
_leaves named a column for a key like `-1.0` that _dig can never read
back, so the export showed an always-empty column. Skip such a key.
2026-09-16 16:43:52 +02:00
stroblme c9172adfaa Keep a flow's runs when the flow goes
A run record is a record of what ran, so deleting a flow no longer sweeps its
history: `_forget_runs` is gone from the flow-delete path, and `DELETE /runs/{id}`
is the only thing that removes a run one at a time. The in-flight guard stays —
that is about work, not history.

`DELETE /runs?flow=` is the counterpart to the list's flow filter and what a
reseed needs, sharing the four statements with the single-run delete and
refusing the same way while a run of that flow is still going.

A run whose flow is gone reads as one: `useFlowInputs` reports the 404 rather
than an empty declaration set, so the run page says "flow deleted", explains it,
and disables Retry, which the route would refuse anyway.
2026-09-06 14:53:23 +02:00
stroblme fb98b04c46 Drop the unused Logo icon variant, poll the Home runs tile at 10s
The icon variant lost its last caller when the responsive variant stopped
carrying a second cropped image. runOverviewQueryOptions now takes the
refetch interval, defaulting to the 30s the Runs screen and the dashboard
panel want; Home passes 10s so a short run does not linger on the tile.
agents.md spells the API host as api.${DOMAIN}, which is what the rest of
the docs use.
2026-09-06 14:24:28 +02:00
stroblme 5c97baa285 Close eight open SDK tasks: the pidfile, the log, cards, names and a live curve
Each was a loose end recorded under `### SDK` in the notepad.

`serve` takes its own pidfile down on SIGTERM. uvicorn restores the handler it
found and re-raises the signal it stopped on, so the default handler ended the
process without unwinding and the `finally` never ran — which is what a stop
sends, and what left `serve.pid` behind.

`serve.log` is cut back past 5 MB by the engine rather than by the screen that
started it, so an adopted engine is bounded too. Gated on its own stdout being
an appended regular file, which is what makes the cut safe: the kernel then
puts the next write at the new end.

Cards are counted from `/dev/nvidia[0-9]*`, so `FLOW_GPUS`/`--gpus` of 0 means
"work it out" the way `FLOW_CPUS` always has. The engine counts, not the
accountant — a remote worker builds one of those from its own inventory, and
detecting there would hand it the engine host's cards. The worker counts last:
what a batch job says it was granted still wins.

`GET /runs/metrics/names` is the distinct over a selection that `--list` and
the terminal's metric picker were approximating by reading the newest run that
had measured anything, which missed a name only an older run ever wrote.

`MetricSink` announces each batch it has written (`run_metric`, carrying the
names). Not a per-point event: one covers up to 500 points or two seconds of
them, and the rows stay the record. The terminal comparison fills in as the
first readings land instead of staying blank until reopened, and the browser
refetches the run and any comparison rather than the list behind them.

`retry --group` pages the list route by `before` instead of stopping at 500.

The terminal dashboard takes the terminal's colours (`ansi-dark`), and the web
UI can re-pair from Settings without disconnecting first.
2026-09-02 16:40:51 +02:00
stroblme 40864d5ca5 Push a frame instead of storing and fetching it
The rate the media dtypes could carry was one frame every second or two: each
was a file on the data volume, an event on the socket, and a request back for
the bytes. This closes both halves of that, and they are one feature.

`save_artifact(..., volatile=True)` writes to a `VolatileStore` — the same
content-addressed store, in `/dev/shm`, bounded by size with the oldest falling
out (`ARTIFACT_VOLATILE_BYTES`, 48 MB under the container's raised `shm_size`).
Nothing sweeps it: a frame nobody kept is not worth walking the store to find.
`ArtifactStore.path` falls through to it, which is what lets a volatile frame be
an ordinary reference everywhere else — the dtype check, a panel's digest scope,
`load_artifact` in a node, and the widget's own fetch all work on one unchanged.
`adopt` copies one into the store when a run records it, so "returned media is
kept, emitted media is not" stays true.

The bytes then go down the flows websocket as a length-prefixed binary frame,
sent just ahead of the `message_value` naming them, so a tile has the frame when
it hears the value moved. Nothing is pushed unasked: a client names the messages
it is drawing (`{"type":"media","names":[…]}`), a panel's list is intersected
with the scope it already had, and only the newest frame per name in a batch is
sent — a client that fell behind is not handed frames it would draw over. The
tunnel relays text only, so a screen reached through a portal falls back to
fetching, which is why the rate table now has two rows.

Around the edges: the remote worker's fetch cache is bounded at last
(`FLUKSIO_ARTIFACT_CACHE_BYTES`), since content addressing means nothing in it
ever expires and a media stream fills it with chunks nothing asks for twice; a
port carrying an image draws the frame in the node panel rather than only
saying `image/png · frame.png · 1.79kB`; and an edge chip says that much instead
of a line of hash. The media screenshot stops waiting for `networkidle` — a
camera is a socket that never goes quiet, which is the point of it.
2026-09-02 10:15:14 +02:00
stroblme c781c199f3 Close the nine open SDK tasks: one engine per directory, a tabbed dashboard, re-pairing, run recovery
serve: refuse a second engine for one data directory whatever port it was
asked for, using the pidfile and a token this directory signed. The check
runs before the database is touched and before the credential is written,
which is what left every later CLI call pointing at a dead port.

The terminal dashboard is three tabs (Overview, Runs, Logs) with the toolbar
following the focused pane, the engine's output goes to serve.log rather than
down a pipe, and closing the screen stops both reader threads so the prompt
comes back. It adopts a running engine on every start, so stop/start and
restart work on one it did not start, and a stop waits for the process to be
gone before the next start. Enrolment reports itself in the modal.

enroll: a new claim code replaces the pairing instead of being refused. The
code is redeemed before anything is written, mappings to a portal being left
are cleared, and a running engine redials when the stored enrolment changes.

runs: an engine re-queues the runs left `queued` by the one before it, and
`fluksio retry <id>` / `retry --group <sweep>` submits an interrupted run
again with the same inputs and group, recorded through Run.parent_id.
2026-08-31 17:37:13 +02:00
stroblme 3b541d6dbc Make the docs state things rather than argue them
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`.
2026-08-31 10:49:58 +02:00
stroblme 6c8a101d24 Rename Installation to Instance
Follows the portal: the noun is "instance" everywhere the app says it —
UI strings, CLI output, error details, docs and comments. The wire keys
(`instance_id`, `instance_token`) and the hub route this calls move with it.

An existing cloud.json is adopted rather than refused: without the key
alias the dataclass fails to parse, which the caller swallows and reads as
"never enrolled" instead of "reconnect".

`instance_key` on a node type becomes `target_key`. It means the outside
thing a node points at, which is a different sense of the word, and keeping
both would put two meanings of "instance" in one codebase.
2026-08-31 10:12:01 +02:00