Commit Graph
229 Commits
Author SHA1 Message Date
stroblme b8a15e4133 Code editor: CodeMirror 6 replaces Monaco
Node source (Python) and the JSON dialog share one CodeMirror editor with
find and replace, autocomplete, folding, indent guides, comment toggle and a
JSON lint, themed from the CSS tokens so light and dark follow the .dark class
without a second palette. Programmatic value changes no longer fire onChange,
blur ignores focus moving into the find bar, readOnly keeps selection and copy.

The editor chunk is 470 kB (157 kB gzipped) against 2.7 MB (704 kB) for Monaco,
which also pulled in a 1.2 MB JSON mode, two workers and a font. At 4x CPU on
slow 4G a node panel is ready in 5.7 s instead of 10.8 s, the JSON dialog in
1.5 s instead of 6.7 s. Typing without a key delay keeps every character.

monaco-editor, @monaco-editor/react and the dompurify override are removed.
Specs: typed code reaches the node source, a JSON setting saves only text that
parses. Docs name Tab and Ctrl-M.

Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
2026-10-28 17:32:49 +01:00
stroblme 688658fa09 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.

Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
2026-10-28 13:06:58 +01:00
stroblme c24c4b40ca 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.

Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
2026-10-28 12:46:42 +01:00
stroblme 79c74116d1 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".

Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
2026-10-28 12:42:35 +01:00
stroblme 73be72f827 Alerts: configuration, test and search entries are a superuser's; subscribing a browser stays open
Any signed-in account could save an ntfy channel with its own `server` and a
`{"$secret": ...}` token and press Test, which sent the stored secret to that
host. The config GET/PUT, the test route and the global search's alert entries
now answer to superusers, like /secrets. The webpush key and subscribe routes
stay open to every account, and the Alerts page shows a non-superuser only the
button for its own browser. The events table in the docs gains run_finished.

Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
2026-10-28 12:40:44 +01:00
stroblme 265f67394c Search: node settings are indexed, with credentials masked on the server
/search gains a `setting` category, `<node>.<setting>` under its flow with the
node type and the value in a new `detail` field, so Ctrl-K finds which node
uses a broker or a timeout and opens the node. `setting_text` is the one place
that decides what may be said: a reference to a stored secret shows its name
(to superusers only, as the secrets list does), a plain value in an `x-secret`
field or under a key like password, token or api_key is masked and never
indexed, nested credentials in headers or query are masked, long values are
clipped. The global search ranks what contains the query as typed ahead of
loose spellings before it caps a group, which long setting values would
otherwise have crowded out.

Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
2026-10-28 11:07:27 +01:00
stroblme 5e5d2bb081 Node panel: ports read message first on inputs, code name is text until it differs
An input row now starts from its message and ends as the argument the code
takes it as ("as prompt"); an output keeps its key on top and the message
below. The name the code uses is muted text with a pencil while it follows the
message's last segment, and a field only when it differs. Device Follows ports
get the same order (message, then the port driven). A list port's item type
moves to the second row so it no longer squeezes the name, two ports deriving
one name get a warning, and the rows rebuilt use 44px touch targets on phones.
The stored node spec is unchanged.

Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
2026-10-28 11:04:15 +01:00
stroblme d1ff0e051d MCP: mcp 2.3 (MCPServer, transport arguments on streamable_http_app, bearer read in middleware); tests for the 2026-07-28 revision and the resource check
Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
2026-10-28 09:21:01 +01:00
stroblme 5fe143e04f Request log: a hook's secret is masked in uvicorn's lines and the tunnel connector's failure line
Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
2026-10-28 08:41:52 +01:00
stroblme 361ef35d58 SDK: sync and run import the declaring module in a child process
The command line no longer imports a research module, so import-time GPU work
ends with a short-lived child instead of holding a CUDA context for the length
of a followed run. fluksio.sdk.declare starts the child, which writes each
flow's document and shims as JSON; Declared stands in for Flow in sync().
Errors keep their type and message, a child's traceback reaches the user.

Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
2026-10-28 08:11:14 +01:00
stroblme 6b8473f207 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>...

Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
2026-10-28 08:07:35 +01:00
stroblme e487588710 Chat: one phone header button, quick-prompt chips, double-click to reuse a message
The phone header carries a single speech bubble: Control on Home, the
assistant elsewhere. Chips above the input offer the user's most sent
messages per tab, then starters; a tap or a double-click on a sent or
heard message fills the input without sending.

Claude-Session: https://claude.ai/code/session_01SSi5AvZ7pFmySK8zy3NPMh
2026-10-27 17:39:36 +01:00
Melvin Strobl 79142b16d8 connector: wanted(port) says whether anyone watches or reads a port
Docs / docs (push) Successful in 31s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m5s
Playwright Tests / test-playwright (2, 2) (push) Failing after 20s
pre-commit / pre-commit (push) Failing after 2m30s
Test Backend / test-backend (3.12) (push) Failing after 4m52s
Test Backend / test-backend (3.14) (push) Failing after 4m36s
Compose Smoke Test / test-compose (push) Failing after 24s
Playwright Tests / merge-reports (push) Failing after 2m25s
2026-10-27 15:20:24 +01:00
Melvin Strobl 90f1f288d2 wren node: calibration setting, passed to wren's training as the installation's calibration records
Docs / docs (push) Successful in 21s
Playwright Tests / test-playwright (1, 2) (push) Failing after 36s
Playwright Tests / test-playwright (2, 2) (push) Failing after 21s
pre-commit / pre-commit (push) Failing after 2m32s
Test Backend / test-backend (3.12) (push) Failing after 5m15s
Test Backend / test-backend (3.14) (push) Failing after 5m6s
Compose Smoke Test / test-compose (push) Failing after 28s
Playwright Tests / merge-reports (push) Failing after 2m27s
2026-10-26 22:17:48 +01:00
Melvin Strobl c366ef2ed7 wren node: install hints and docs pin the public v2.0.0 tag
Docs / docs (push) Successful in 25s
Playwright Tests / test-playwright (1, 2) (push) Failing after 33s
Playwright Tests / test-playwright (2, 2) (push) Failing after 18s
pre-commit / pre-commit (push) Failing after 2m50s
Test Backend / test-backend (3.12) (push) Failing after 4m49s
Test Backend / test-backend (3.14) (push) Failing after 4m48s
Compose Smoke Test / test-compose (push) Failing after 24s
Playwright Tests / merge-reports (push) Failing after 2m23s
2026-10-25 17:17:17 +01:00
Melvin Strobl 50a9b8fe2b up
Docs / docs (push) Successful in 25s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m59s
Playwright Tests / test-playwright (2, 2) (push) Failing after 24s
pre-commit / pre-commit (push) Failing after 2m44s
Test Backend / test-backend (3.12) (push) Failing after 6m3s
Test Backend / test-backend (3.14) (push) Failing after 5m32s
Compose Smoke Test / test-compose (push) Failing after 29s
Playwright Tests / merge-reports (push) Failing after 2m26s
2026-10-25 14:56:25 +01:00
Melvin Strobl bdb18b99da Make documentation standalone and generalize examples 2026-10-25 14:43:20 +01:00
Melvin Strobl 13bf0f76d7 wren node: untrained targets left out of the prompt, a read beside a held-back set goes on whole
Docs / docs (push) Successful in 27s
Playwright Tests / test-playwright (1, 2) (push) Failing after 47s
Playwright Tests / test-playwright (2, 2) (push) Failing after 36s
pre-commit / pre-commit (push) Failing after 2m55s
Test Backend / test-backend (3.12) (push) Failing after 5m12s
Test Backend / test-backend (3.14) (push) Failing after 5m16s
Compose Smoke Test / test-compose (push) Failing after 36s
Playwright Tests / merge-reports (push) Failing after 2m33s
2026-10-25 12:09:37 +01:00
stroblme e91015965a wren node: Train in the panel, as a run of wren's training flow
Docs / docs (push) Successful in 38s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m44s
Playwright Tests / test-playwright (2, 2) (push) Failing after 31s
pre-commit / pre-commit (push) Failing after 3m1s
Test Backend / test-backend (3.12) (push) Failing after 5m2s
Test Backend / test-backend (3.14) (push) Failing after 4m42s
Compose Smoke Test / test-compose (push) Failing after 25s
Playwright Tests / merge-reports (push) Failing after 5m6s
POST/GET /flows/{name}/nodes/{id}/train submit wren_train on the node's
targets, examples and tests and read its latest run (run.for_node,
cause "train"); "Use this model" writes `model` into the draft.

Claude-Session: https://claude.ai/code/session_01H89mFby73wwhthP9htfcuB
2026-10-25 01:18:54 +02:00
stroblme 23bb9aa983 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.

Claude-Session: https://claude.ai/code/session_01H89mFby73wwhthP9htfcuB
2026-10-24 21:28:42 +02:00
stroblme 2a09f0b765 Artifact sweep keeps digests named in node settings
A Service's weights or a wren node's model held only in a setting were
swept once the run that produced them was deleted.

Claude-Session: https://claude.ai/code/session_01H89mFby73wwhthP9htfcuB
2026-10-24 21:28:35 +02:00
Melvin Strobl ea781c923c docs: no wren model makes brightness calls yet
Docs / docs (push) Successful in 26s
pre-commit / pre-commit (push) Failing after 2m22s
Test Backend / test-backend (3.12) (push) Failing after 4m56s
Test Backend / test-backend (3.14) (push) Failing after 4m42s
2026-10-23 12:21:43 +02:00
Melvin Strobl 3c057cf75f wren node writes the house brightness from lights.set/dim/brighten (wren s11)
Docs / docs (push) Successful in 25s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m0s
Playwright Tests / test-playwright (2, 2) (push) Failing after 21s
pre-commit / pre-commit (push) Failing after 2m27s
Test Backend / test-backend (3.12) (push) Failing after 4m51s
Test Backend / test-backend (3.14) (push) Failing after 4m31s
Compose Smoke Test / test-compose (push) Failing after 23s
Playwright Tests / merge-reports (push) Failing after 2m51s
2026-10-22 22:43:33 +02:00
Melvin Strobl 0d4ff89302 wren node takes switches (plug, socket, appliances), refused when the model lacks the kind
Docs / docs (push) Successful in 24s
Playwright Tests / test-playwright (1, 2) (push) Failing after 32s
Playwright Tests / test-playwright (2, 2) (push) Failing after 21s
pre-commit / pre-commit (push) Failing after 2m56s
Test Backend / test-backend (3.12) (push) Failing after 4m53s
Test Backend / test-backend (3.14) (push) Failing after 4m37s
Compose Smoke Test / test-compose (push) Failing after 24s
Playwright Tests / merge-reports (push) Failing after 2m47s
2026-10-22 17:48:41 +02:00
Melvin Strobl a7a18bf19f wren applies all/every commands itself, answers temperatures, reads aliases and number words; tts says units and clock times
Docs / docs (push) Successful in 25s
Playwright Tests / test-playwright (1, 2) (push) Failing after 2m52s
Playwright Tests / test-playwright (2, 2) (push) Failing after 21s
pre-commit / pre-commit (push) Failing after 2m28s
Test Backend / test-backend (3.12) (push) Failing after 5m8s
Test Backend / test-backend (3.14) (push) Failing after 4m38s
Compose Smoke Test / test-compose (push) Failing after 25s
Playwright Tests / merge-reports (push) Failing after 2m24s
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
stroblmeandClaude Opus 5.5 b2493636a6 wren leaves unnamed areas to the assistant; assistant targets take a closed set of values
Docs / docs (push) Successful in 34s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m22s
Playwright Tests / test-playwright (2, 2) (push) Failing after 21s
pre-commit / pre-commit (push) Failing after 2m30s
Test Backend / test-backend (3.12) (push) Failing after 4m53s
Test Backend / test-backend (3.14) (push) Failing after 4m34s
Compose Smoke Test / test-compose (push) Failing after 29s
Playwright Tests / merge-reports (push) Failing after 2m9s
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.").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:56:20 +02:00
Melvin StroblandClaude Opus 5.5 54bc6b1512 wren sees every area and reads the command plain
Docs / docs (push) Successful in 41s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m51s
Playwright Tests / test-playwright (2, 2) (push) Failing after 31s
pre-commit / pre-commit (push) Failing after 2m34s
Test Backend / test-backend (3.12) (push) Failing after 5m4s
Test Backend / test-backend (3.14) (push) Failing after 4m43s
Compose Smoke Test / test-compose (push) Failing after 27s
Playwright Tests / merge-reports (push) Failing after 2m31s
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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ljdk7nDaguh9Uhi3zhcSjn
2026-10-20 08:34:07 +02:00
Melvin StroblandClaude Opus 5.5 5235ac59c2 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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ljdk7nDaguh9Uhi3zhcSjn
2026-10-20 08:26:14 +02:00
Melvin StroblandClaude Opus 5.5 75e257d9e4 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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ljdk7nDaguh9Uhi3zhcSjn
2026-10-19 23:23:25 +02:00
Melvin StroblandClaude Opus 5.5 0d8c02a9ef 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).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ljdk7nDaguh9Uhi3zhcSjn
2026-10-19 22:06:08 +02:00
Melvin StroblandClaude Opus 5.5 a12bc5a2ee 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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ljdk7nDaguh9Uhi3zhcSjn
2026-10-19 21:55:17 +02:00
Melvin StroblandClaude Opus 5.5 200509c1bd 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).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ljdk7nDaguh9Uhi3zhcSjn
2026-10-19 21:34:02 +02:00
Melvin StroblandClaude Opus 5.5 560c19962a 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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ljdk7nDaguh9Uhi3zhcSjn
2026-10-19 18:43:04 +02:00
Melvin StroblandClaude Opus 5.5 23b0861a9b Binary uplink takes audio chunks beside image frames (openpanel VS4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:04:39 +02:00
Melvin StroblandClaude Opus 5.5 aca1ea7971 Docs: a camera's snapshot_min_s, hold_s at least 2, person on connect
Docs / docs (push) Successful in 24s
Playwright Tests / test-playwright (1, 2) (push) Failing after 4m10s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m7s
pre-commit / pre-commit (push) Failing after 2m23s
Test Backend / test-backend (3.12) (push) Successful in 4m48s
Test Backend / test-backend (3.14) (push) Successful in 4m32s
Compose Smoke Test / test-compose (push) Successful in 29s
Playwright Tests / merge-reports (push) Successful in 1m47s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
2026-10-17 10:44:14 +02:00
Melvin StroblandClaude Opus 5.5 f679264b3a 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).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
2026-10-17 09:20:04 +02:00
Melvin StroblandClaude Opus 5.5 02b736c9e8 Live view over WebRTC (openpanel L1, server and browser)
Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Failing after 4m37s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m19s
pre-commit / pre-commit (push) Failing after 2m25s
Test Backend / test-backend (3.12) (push) Successful in 4m57s
Test Backend / test-backend (3.14) (push) Successful in 4m41s
Compose Smoke Test / test-compose (push) Successful in 28s
Playwright Tests / merge-reports (push) Successful in 1m38s
A device reporting a `live` port streams its camera to the browser's own
RTCPeerConnection. The flows socket carries the signalling as text, so it
crosses the portal: `live` messages on a person's socket, `signal` on a
device's (start, sdp, candidate, bye, busy). The server routes by session,
holds a device to two viewers, ends a session when either socket closes or
no answer comes within 10 s, and gives each socket an outbox of its own
that ends a session rather than drop a message. TURN credentials are
minted per session in coturn's REST scheme (STUN_URLS, TURN_URLS,
TURN_SECRET), the viewer's with the device's offer; GET /devices/{id}/ice
hands them out too. The tunnel keeps a stream's early frames instead of
dropping them past eight and forgets them when the stream or link goes.

A media tile on a device's camera, and the Devices tab's preview, play
live when tapped, pause frames meanwhile, and fall back to frames with a
short note on busy, failure or no picture within 10 s. coturn is an
optional compose service behind the `turn` profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
2026-10-17 08:37:39 +02:00
Melvin StroblandClaude Opus 5.5 9ee0731bdd Docs: keeping a camera's snapshot
Docs / docs (push) Successful in 23s
Playwright Tests / test-playwright (1, 2) (push) Failing after 4m9s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m6s
pre-commit / pre-commit (push) Failing after 2m19s
Test Backend / test-backend (3.12) (push) Successful in 4m34s
Test Backend / test-backend (3.14) (push) Successful in 4m23s
Compose Smoke Test / test-compose (push) Successful in 29s
Playwright Tests / merge-reports (push) Successful in 1m28s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
2026-10-16 21:15:15 +02:00
Melvin StroblandClaude Opus 5.5 62c0b915f3 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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
2026-10-16 20:43:14 +02:00
Melvin StroblandClaude Opus 5.5 20251a18b4 Pairing limits: no shared bucket for portal calls, a cap per side
Docs / docs (push) Successful in 25s
Playwright Tests / test-playwright (1, 2) (push) Failing after 4m35s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m21s
pre-commit / pre-commit (push) Failing after 2m24s
Test Backend / test-backend (3.12) (push) Successful in 4m41s
Test Backend / test-backend (3.14) (push) Successful in 4m20s
Compose Smoke Test / test-compose (push) Successful in 34s
Playwright Tests / merge-reports (push) Successful in 1m35s
Every call through the portal arrives off the tunnel, so the per-address limit
skips those (the hub limits them by the real address), and the cap of 50 counts
and evicts portal and local requests apart, so a portal flood never pushes out
a device waiting on this network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
2026-10-16 19:33:33 +02:00
Melvin StroblandClaude Opus 5.5 4701e3a5fb 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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
2026-10-16 19:26:05 +02:00
Melvin StroblandClaude Opus 5.5 1e920c5bad Restart safety: transient credential checks are 1013/503, not a refusal
Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Failing after 4m29s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m21s
pre-commit / pre-commit (push) Failing after 2m53s
Test Backend / test-backend (3.12) (push) Successful in 4m31s
Test Backend / test-backend (3.14) (push) Successful in 4m19s
Compose Smoke Test / test-compose (push) Successful in 36s
Playwright Tests / merge-reports (push) Successful in 1m35s
- 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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
2026-10-16 18:18:42 +02:00
Melvin StroblandClaude Opus 5.5 ad76200d3d 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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
2026-10-16 17:37:49 +02:00
Melvin StroblandClaude Opus 5.5 a9d453c9d9 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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 16:56:05 +02:00
Melvin StroblandClaude Opus 5.5 f8011d2a8a 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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 16:40:31 +02:00
Melvin StroblandClaude Opus 5.5 ed06bdec6b 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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:48:36 +02:00
stroblmeandClaude Opus 5.5 01495aed25 Docs: x86 and arm64, Raspberry Pi included; one container in the start pages
Docs / docs (push) Successful in 23s
pre-commit / pre-commit (push) Failing after 2m28s
Test Backend / test-backend (3.12) (push) Successful in 4m51s
Test Backend / test-backend (3.14) (push) Successful in 4m49s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:56:08 +02:00
stroblmeandClaude Opus 5.5 48aac3855b 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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:40:02 +02:00
stroblmeandClaude Opus 5.5 5ddd02d88b Panels dialog: opens without focusing a field, a panel title saves on blur, Enter or Escape instead of per keystroke, and a new panel's name previews its URL. Docs
Docs / docs (push) Successful in 23s
Playwright Tests / test-playwright (1, 2) (push) Failing after 4m10s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m14s
pre-commit / pre-commit (push) Failing after 2m37s
Test Backend / test-backend (3.12) (push) Successful in 4m53s
Test Backend / test-backend (3.14) (push) Successful in 4m10s
Compose Smoke Test / test-compose (push) Successful in 34s
Playwright Tests / merge-reports (push) Successful in 1m35s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:25:34 +02:00
stroblmeandClaude Opus 5.5 0319842ab4 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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:21:42 +02:00