Commit Graph
178 Commits
Author SHA1 Message Date
stroblme 48d4be6093 Starting a running flow restarts its nodes, so interval and cron Injects keep firing; a params-dict node is told settings arrive by name 2026-11-01 10:59:14 +01:00
stroblme 75b3a93094 Fix work recovery and reduce engine and chart overhead
Docs / docs (push) Successful in 33s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m37s
Playwright Tests / test-playwright (2, 2) (push) Failing after 27s
pre-commit / pre-commit (push) Failing after 2m37s
Test Backend / test-backend (3.12) (push) Successful in 5m7s
Test Backend / test-backend (3.14) (push) Successful in 5m2s
Compose Smoke Test / test-compose (push) Failing after 28s
Playwright Tests / merge-reports (push) Failing after 2m29s
2026-10-29 14:24:17 +01:00
stroblme 5d0ea480bf Alerts: a Telegram channel, one chat or group each, connected by a one-time link
The channel sends through the Bot API with httpx only: HTML with escaped text,
cut to fit a message, silent for a warning, a short 429 waited out once, and
errors that say what to do about a blocked bot, a lost chat or a bad token and
never quote the request URL. The bot token is a stored secret: saving a
channel refuses a literal one, the card's "Store token" writes the secret and
a reference, and no endpoint returns it.

A chat id is never typed. Connect starts a ten minute code, and the chat that
sends `/start <code>` back through the bot is the one connected, so it is a
chat somebody opened a link for. The engine fetches updates only while a code
is waiting (no standing poll, no webhook, no inbound address) and ignores
everything else without a reply. Connect needs a saved form, and the form
takes the connected chat in without touching what was typed since.

The token is part of every Bot API URL, so the httpx logger gets the same
filter the request lines have, which now also masks an httpx.URL argument.
2026-10-28 13:06:58 +01:00
stroblme bfaaa7cd03 Alerts: cooldowns survive a restart, and a connection must stay down a minute before it alerts
The alert manager kept its cooldowns and its ceiling per hour in memory, so a
node that stayed down was announced again by every restart. Each delivered
alert is now a row in the new alert_sent table (additive migration), loaded
for the last seven days when the manager starts and pruned as new ones come
in. A ledger that fails never stops an alert. The manager's clock is wall
time for the same reason.

A node_health "down" alert also waits 60 s (HEALTH_GRACE_S) in a pending task
per node; a later event that is not "down" cancels it, as does stopping the
manager, so a broker restarting with the stack alerts nobody.

The cooldown used to be stamped before the ceiling per hour was checked, so
an alert the ceiling held back still spent its cooldown. It is stamped only
once the alert is sent now.
2026-10-28 12:46:42 +01:00
stroblme 00842fa175 Health summary: a node is failing after two failures in a row; one is a warning, a good run ends both
The summary read degraded for 15 minutes after any single node_error, even
once the node had run well again and including one bad input from a visitor.
The controller now counts a node's failures since it last ran well and notes
when it recovered; both carry across rebuilds and an acknowledgement resets
the count. Two in a row inside the 15 minute window is "failing" (a problem),
one is listed under warnings, and nodes.failing keeps meaning "failing now".
2026-10-28 12:42:35 +01:00
stroblme 34243e6902 SDK: artifacts work outside a node, run --follow prints status and queue waits, fluksio cancel
save_artifact/load_artifact outside a node use the worker's own store code in
$FLUKSIO_ARTIFACT_DIR or <tmp>/fluksio-artifacts, so a node can be called by
hand. --follow prints status changes and each node's resource wait; the
placer's waiting snapshot now carries the run. fluksio cancel <run>...
2026-10-28 08:07:35 +01:00
stroblme f52d9c3077 Service node ends its call when the loop cancels every task; test order and timing leaks
_serve_once treated a cancelled call future as finished, so an event loop
cancelling all tasks at once (shutdown) never sent the worker a cancel and the
call's thread kept asyncio.run waiting for SILENCE_S. The cameras audio test
now removes its voice flow and device, the unpaired-frame test waits for its
first frame before the unpair, and test_connector no longer inherits the
controller an app test bound to demand.
2026-10-27 21:23:40 +01:00
Melvin Strobl 00cda9d2f1 connector: wanted(port) says whether anyone watches or reads a port 2026-10-27 15:20:24 +01:00
Melvin Strobl 82c64a10b6 Make documentation standalone and generalize examples 2026-10-25 14:43:20 +01:00
Melvin Strobl 96ba512d40 wren node: untrained targets left out of the prompt, a read beside a held-back set goes on whole 2026-10-25 12:09:37 +01:00
stroblme 07fa5a2dc2 wren node: targets from the wiring, on wren 2
Every output is a target and every input besides prompt a reading. A
targets setting gives descriptions, values, ranges and per-target
guards, model takes a trained pack's artifact digest, min_confidence
defaults to the model's threshold. A command is applied whole or not at
all, and targets the model was not trained on read degraded. The home
path (area/kind ports, groups, brightness, temperature answers) is gone.
2026-10-24 21:28:42 +02:00
Melvin Strobl a34d47b24f wren node writes the house brightness from lights.set/dim/brighten (wren s11) 2026-10-22 22:43:33 +02:00
Melvin Strobl 613eb8a744 wren node takes switches (plug, socket, appliances), refused when the model lacks the kind 2026-10-22 17:48:41 +02:00
Melvin Strobl 6b575a4989 wren applies all/every commands itself, answers temperatures, reads aliases and number words; tts says units and clock times
wren's model answers "turn off all lights" with one call, and at 0.98 a
living room light switched on. The node now applies such a command to every
wired target of the kind. Number words become digits ("in five minutes"
lost its delay), aliases put the house's words and known mishearings in the
model's, and sensors.temperature is answered from <area>_temperature or
outdoor. tts writes out units and clock times before speaking.
2026-10-22 12:34:40 +02:00
stroblme bf63b3d6d4 wren leaves unnamed areas to the assistant; assistant targets take a closed set of values
wren drops a call on an area the command does not name (other than the
user's or, with none named, the living room): "switch on the appliances"
turned on the bathroom light at 0.97. The assistant's `values` setting gives
a str target its only values, explains them in the tool and confirms in the
word set for each ("Canopy open.").
2026-10-22 10:56:20 +02:00
Melvin Strobl f1452f3119 wren sees every area and reads the command plain
Shown only the wired areas, the model took "turn on the bathroom light"
for the living room's light and "the office light" for the kitchen's,
both above 0.93: 5 of the 28 recorded 7B commands switched a wrong light.
Every area is shown now, with a light each, so such a call names an
unwired device and goes on to `unhandled`.

With every area shown, a transcript's "Turn on the kitchen light." fell
to 0.80. The command is passed lower case without sentence marks, as the
model was trained, which brings it to 0.99.
2026-10-20 08:34:07 +02:00
Melvin Strobl 1a8f6875c4 Assistant confirms a command from the values it set, without a second round
run_turn takes answer_tools: false ends the turn once the tools have
answered. The assistant node uses it and says what was set ("Kitchen light
off, living room light off."), which saves one model round, about 1.1 s on
the P100. A turn without a call still streams the model's own words.

rebuild-frontend keeps the running stack's FRONTEND_HOST, so the token
issuer no longer flips between the LAN address and app.localhost.
2026-10-20 08:26:14 +02:00
Melvin Strobl a286cf4ceb Assistant node, and the Control tab as one conversation (openpanel V5)
run_turn takes its tools, system prompt and tool caller; the chat passes the
MCP tools as before. The assistant node offers one tool per wired target,
reads its other inputs as context, streams its reply a sentence at a time,
warms the provider on `listening` and says so when the provider is not there.
Completions share the per-server HTTP client. wren replies when nothing reads
`unhandled`, stt shows a note when nothing was heard. /ai/control lists one
conversation per entry node with the messages to watch.
2026-10-19 23:23:25 +02:00
Melvin Strobl 4df7bdb057 Speech calls keep one connection per server, opened when listening starts
transcribe and speak share an httpx client per speech server with a 300 s
keep-alive, and run on the engine's loop. stt warms it with GET /v1/models
when an utterance opens, so a reply's first sentence finds the connection
open: a new one over a lossy link cost up to a second (openpanel V4).
2026-10-19 22:06:08 +02:00
Melvin Strobl f50d8b9570 tts speaks MP3: a seventh of WAV's bytes over a slow link to the server
A sentence's length for `append` comes from the Xing header LAME writes into
the first frame. The cache key carries the format, so no WAV is reused.
2026-10-19 21:55:17 +02:00
Melvin Strobl 16383ea6e2 Voice nodes (openpanel V4): stt, tts, wren reply/unhandled, socket publish
- `stt`: one realtime transcription session per utterance on the speech
  server's url, opened at `listening` true or the first chunk, committed at
  false; a whole clip goes to /v1/audio/transcriptions. Returns at once and
  publishes `partial` and `text` when the server answers.
- `tts`: one /v1/audio/speech call per sentence, cached by voice and text in
  node state; a reference is marked `append` while earlier ones still play.
- wren: `reply` says what was applied, `unhandled` carries the prompt when
  nothing was. The install hint names the git pin (PyPI's `wren` is another
  package).
- ai/client: transcribe, speak and transcribe_stream beside stream_completion.
- A device socket publishes scalar `in` ports as {"type": "publish"} text,
  in order with its binary frames.
- Pipeline.publish walks only what its values reach: a node downstream of a
  consumer that published nothing no longer runs again (wren re-acted on
  every microphone chunk).
2026-10-19 21:34:02 +02:00
Melvin Strobl 85dad39973 Service node: a server kept running on a worker (openpanel V2)
- `service` node type: command, port, worker label, health path, env and
  weights (an artifact digest the worker fetches as {weights}). Its flow
  starts it; it restarts with backoff, reads degraded while starting or
  without a worker and down after three starts that never got healthy, and
  publishes http://<worker>:<port> once /health answers 200, "" when it ends.
- Worker protocol 3: a `serve` call, every call in its own process group,
  killed when the engine ends the call, the socket drops or the agent gets
  SIGTERM. `--service-host` overrides the address the engine sees.
- Services and compiles take no `--parallel` slot, and a flow's detail is
  built off the event loop, so a save no longer stalls the engine.
2026-10-19 18:43:04 +02:00
Melvin Strobl 72c1d73e96 Camera permission and review fixes (openpanel C3, C4)
C4: a device's `viewers` (superusers by default, or members) covers the
image and video messages its ports publish. Anyone else gets none of their
values on the socket, in the catalogue or a flow's state, 403 for their
history, for the frames they hold (the last 8 included) and for TURN
credentials, no bytes when asking for them, and `bye: forbidden` for a live
view. A device credential stays bounded by its dashboards. The device page
sets it in its Camera section, and a tile the viewer may not see says
"No access to this camera" and asks the camera for nothing.

C3:
- A volatile upload is held to MAX_FRAME_BYTES (4 MiB, 413 above it, read
  without extra copies), and so is a frame on a device's socket.
- A frame the engine holds in memory is written to the ring directory
  before a local worker is handed it, so load_artifact finds it.
- A socket the bus dropped events for catches up with a snapshot, demand
  included, and a device's credential is checked again per frame, cached by
  the flow store revision and its nonce, so a lost event never keeps an
  unpaired device's frames coming.
- A report is at most 64 KB once its named config_schemas are put in place.
- A device away for 60 s has its true bool messages set to false.
- Refused frames are logged by fixed keys, a frame is named after its
  message, `fluksio serve` waits 5 s for open connections, a tile with a
  stream URL creates no demand, the 8-name cap keeps the newest names.
- Every container's log rotates (json-file, 3 x 10 MB).
2026-10-17 09:20:04 +02:00
Melvin Strobl ecc4bee4bb Cameras on devices (openpanel C1): frames in memory, a binary uplink, demand
- Volatile frames are held in memory with a byte-bounded queue instead of
  the /dev/shm ring scanned on every put; workers keep writing to the
  directory, which is trimmed on a timer. A volatile upload no longer spools
  to disk. ARTIFACT_VOLATILE_BYTES=0 turns the ring off.
- A device sends camera frames as binary messages on its socket, held and
  published in one step and credited to its device node; refused frames are
  logged and dropped. Its snapshot carries demand and the uplink (binary, or
  http through the portal).
- One demand registry for every socket's wanted media, the tunnel's
  included, plus what running flows read, mapped onto on-demand ports and
  published as device_demand; at most 8 media names per socket.
- Per-message deflate off in serve and dev; a device may fetch the last 8
  digests of a message; published dashboards cached per store revision; a
  revoked device socket is closed with 1008.
- Media tiles want bytes only while on screen; the Devices tab previews a
  wired camera; the device node warns when a flow reads an on-demand port.
2026-10-16 20:43:14 +02:00
Melvin Strobl c604cfdf3b Device security and robustness (openpanel C0): check code, pnc, lenient report
- POST /devices/pair answers a 2-character `check`, listed by /devices/pending
  and shown beside each waiting entry; five codes a minute per address, and at
  50 waiting the oldest gives way instead of a 429
- portal-minted device tokens are asked for with the device's nonce (`pnc`) and
  held against it, so unpair and delete revoke them; one without `pnc` stands
  only while the nonce is 0
- a device credential acts as the device (`device_principal`), not as the
  approving superuser, so deleting that account unpairs nothing
- the report is validated port by port: unknown enum values read as `unknown`,
  unreadable ports and sections are left out and logged, `config` may name an
  entry of `config_schemas`; 422 only for no schema 1 or no object
- devices.json is parsed once per change (cached by stat), delete drops the
  report and keeps the nonce, a busy recheck runs in the background
- scope and io only from enabled flows (start/stop tells the devices), a port
  bound twice gives one io row
- the Devices tab's saves cancel and hold back device refetches until settled
2026-10-16 19:26:05 +02:00
Melvin Strobl 13d23f6baf Restart safety: transient credential checks are 1013/503, not a refusal
- A device credential that cannot be checked now (device store not running,
  nonces unreadable) raises DeviceUnavailable: 503 over HTTP, and the
  /flows/ws handshake and the tunnel stream accept and close with 1013.
  1008 (a 403 before accepting) stays for a revoked or invalid one
- Device files are written through a temporary file, so a read mid-save no
  longer looks like a deleted device
- The first report turns Touch friendly on only for a device paired since
  pairings are noted (DeviceState.paired_at), so devices paired before keep
  the switch they were given
- NodeDef reads `device`/`device_policy` from older clients as the worker
  label, for a release
- Tests: transient vs revoked socket, the touch default, the old worker keys,
  and a restart with a device node and a node_health rule sending no push
2026-10-16 18:18:42 +02:00
Melvin Strobl 4286acadfd Capability report: PUT /devices/{id}/report, a Capabilities section, offline devices degraded
A device reports its board, screen, camera and ports from its own
credential (16 KB, schema 1, replacing the last report), and the flows
are validated against it again. The node panel turns a reported port's
config schema into a form, a reported screen is a canvas preset and a
touch screen's first report turns Touch friendly on. An offline device
node is degraded: amber on the canvas, a warning on Home, never a fault.
2026-10-16 17:37:49 +02:00
Melvin Strobl 1dc891aa1b A node's worker label is worker, so a device is only a paired one
- NodeDef `device`/`device_policy` become `worker`/`worker_policy`, the
  issue `ignored_device` becomes `ignored_worker`, and the SDK's `@node`
  takes `worker=` and `worker_policy=`
- FlowStore._migrate renames the keys in published and draft documents
  once, bumping their version; a queued run's `needs` reads the old key
- placement passes the label as `label`, client regenerated
- docs: worker placement wording, MCP issuer without DOMAIN, device rows in
  the CLI data directory and the API table, device links in compose comments
2026-10-16 16:56:05 +02:00
Melvin Strobl 29d3ec467f Device node: a device's ports are wired in flows
- flow/nodes/device.py: `device` node with params `device` and `config`;
  provides the device's `in` ports, requires its `out` ports without
  waking anything, never run (Node.passive), down "Offline" from presence
- DeviceDef.io and DevicePort go; GET /devices/{id} computes `io` and
  `bindings` from the device nodes of published flows, in a fixed order,
  cached by FlowStore.revision; the credential's scope comes from them
- issues `unknown_device` and `device_port` (wired twice; against a report
  the wrong direction or dtype, or unlisted as advisory), rechecked when a
  device is created or deleted; ValidationIssue.advisory is a field
- a device publishing on a bound port is credited to its node by the
  server; the panel endpoint kind is gone from `_endpoints` and the canvas
- a rebuild that moves a device's bindings sends `dashboard_changed` with
  an empty name, which the firmware refetches on
- startup moves each device's `io` rows into device nodes in the flows
  declaring their messages, draft and published, one commit; rows no
  flow declares are logged and left out
- frontend: Device node settings (device, Publishes, Follows, a port
  picker from a report), palette shortcuts per reported port, DeviceDetail
  lists bindings with "Use in flow" in place of the I/O editor
- docs: devices, flow editor, node types, issues, API
2026-10-16 16:40:31 +02:00
Melvin Strobl 28a565e501 Panels become devices: per-device documents in the flow repo, a Devices tab
- flow/devices.py: DeviceDef in FLOWS_DIR/_devices/<id>.json, committed under
  the flow store's write lock, 409 on a stale version; nonces in DEVICES_FILE
  outside git; in-memory presence announced as device_status
- api/routes/devices.py: CRUD, pairing with an optional {name, board,
  firmware}, a pending list adopted without a code from the LAN only, unpair;
  /panels/pair, /panels/pair/{code} and GET /panels/{id} kept as aliases,
  PUT /panels/ gone
- _device_may replaces _panel_may, JWT claims unchanged
- panels.json moves into devices once at startup, then .migrated
- MCP list_devices, get_device; search category device
- Devices tab (list, detail with presence, pairing, ordered dashboards,
  touch, I/O), PanelsDialog removed, serialised optimistic saves
2026-10-16 15:48:36 +02:00
stroblme 07ecbaab3a Duplicate flow: references to its own messages under its own name follow the copy 2026-10-16 09:22:57 +02:00
stroblme 6ab5062f59 Duplicate flows and dashboards: POST /flows/{name}/duplicate and /dashboards/{name}/duplicate copy the working copy into {name}_copy (then _copy_2, ...) as an unpublished draft with " (copy)" on the title. A flow copy takes its node sources as drafts and leaves origin and runtime.json behind; copying code nodes is a superuser's (the save check, now taking the replaced copy). Client regenerated 2026-10-16 09:21:30 +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 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 fa8b178866 Panels dialog saves a ready I/O port draft on close and keeps an unfinished one; one qualified message-name rule on both sides 2026-10-15 16:49:26 +02:00
stroblme 2f780ad570 Flow canvas draws panel I/O ports as panel:<id>:<port> endpoints that open their panel in the Panels dialog 2026-10-15 16:31:33 +02:00
stroblme 08adc9ef1d RemoteWorker.cancel makes no coroutine once the loop is closed
run_coroutine_threadsafe refused it and it was never awaited, a RuntimeWarning
under pytest 9. The cancel still logs that it could not reach the worker.
2026-10-15 16:17:30 +02:00
stroblme a34f8f436c Draw a round dashboard in the mosaic as its circle with a dot per face
DashboardSummary carries the dashboard's shape, so the list no longer lays
a round dashboard's faces out at grid places they never use.
2026-10-15 15:54:47 +02:00
stroblme 0562753d9c http sender drops reply keys no output port declares
A sender without outputs no longer fails on a {"status": "ok"} reply.
2026-10-15 09:57:51 +02:00
stroblme 5e5f1836cb wren runs its model on one BLAS thread
threadpoolctl scopes the limit to wren.act, so other numpy calls in the
engine keep their threads.
2026-10-15 09:55:02 +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 ae81d95fd7 Warn at startup when worker or cascade limits are far past the cores
More than four times os.cpu_count() logs one warning; the value is kept.
2026-10-15 09:51:38 +02:00
stroblme 9a029a85f8 Engine timings move to FLOW_* settings
NODE_STOP_TIMEOUT, REBUILD_WAIT, FLOW_REBUILD_WAIT, BACKOFF and
CANCEL_GRACE keep their defaults and are read from Settings.
2026-10-15 09:50:18 +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 96f4fa5a38 Warn when a node that runs no code names a device
A device on a node without source was accepted and silently ignored. The
build now reports it as ignored_device, an advisory issue.
2026-10-15 09:45:24 +02:00
stroblme c9fda50601 Connection nodes speak of listener and sender, and of send_queue_size
mqtt and http take websocket's vocabulary: their Mode is LISTENER/SENDER,
default names read mqtt_listener_… and http_listener_…, and mqtt's
publish_queue_size is send_queue_size. No alias stays behind, so the flow
store rewrites the old setting in every published and draft mqtt node when it
opens, in the same migration as trigger → debounce. Every mqtt and http
setting carries a one-line description for the editor, and the node-types
reference uses one wording for direction and says that an http sender
publishes a JSON object response on its outputs.
2026-10-15 09:44:22 +02:00
stroblme c032898135 A supervised loop that crashes reports its node down
A push session that failed before it listened left only a task_crashed
event. The wrapper in _run_supervised now reports down with the exception,
unless the loop already said why it is down.
2026-10-15 09:42:01 +02:00
stroblme cfb9e53d68 Rename the trigger node type to debounce, migrating stored flows
The store rewrites `"type": "trigger"` to `debounce` in every published and
draft flow when it opens, bumps the version and commits once. No alias stays
behind, so `trigger` is now an unknown type.
2026-10-15 09:34:40 +02:00
stroblme 4a776e52c4 Delay node: hold the latest value inside the interval and send it at the window's end
The interval now rides the output ports, so the pipeline's own hold and flush
keep what arrives inside the window instead of the node dropping it.
2026-10-15 09:32:35 +02:00