Commit Graph
239 Commits
Author SHA1 Message Date
stroblmeandClaude Fable 5 63531fa36e Record what the package sweep landed and what it turned up
Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 17:28:55 +02:00
stroblmeandClaude Fable 5 a1572f91e3 Stop the capture script seeding a node into a populated flow
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 17:25:28 +02:00
stroblmeandClaude Fable 5 d5e1466d8c Let a paused flow be stepped from the dock
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 17:16:53 +02:00
stroblmeandClaude Fable 5 63d3183518 Regenerate the client for step and dashboard columns
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 17:15:14 +02:00
stroblmeandClaude Fable 5 0af09eedbe Rework the dashboards onto the flow canvas
One shell for both editors. Flows and dashboards each get a searchable
overview under the padded shell, their editors move to the full-bleed
canvas, and the floating chrome is shared: a title bar that only says
what you are looking at, and a bottom dock carrying everything else —
the flow bar's status, settings and Publish moved down there, the
add-flow button moved to the overview.

Dashboards gain the rest of M4's visualization work:

- widgets are picked by clicking them, with the header as the drag
  handle so a slider still slides and a switch still flips while
  editing; settings moved into the flows' SidePanel
- react-grid-layout for drag and edge-resize, so the stored x/y finally
  mean something; a dashboard nobody arranged is shelf-packed once
- a per-dashboard grid size, so a panel can be matched to its screen
- the chart widget, drawn with uPlot: several messages on one axis, fed
  from the stored history plus the live socket tail, coloured from the
  new --chart-1..5 ramp
- /view/{name}: the URL a wall panel is pointed at — no sidebar, no
  footer, no editing, and no editor code, since routes are split
- a widget wired to a payload type it cannot carry, or wired to nothing
  at all, carries the same red dot a failing node does; the picker
  records the type it bound and WidgetDef refuses a mismatch on save

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 17:13:10 +02:00
stroblmeandClaude Fable 5 74bb956805 Let the capture script take a host-resolver mapping
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:59:47 +02:00
stroblmeandClaude Fable 5 761a006a2d Offer settings from other nodes, help with cron, follow the ports
Suggest a parameter's value from the nodes of the same type in every flow,
the way message names are already offered; secrets stay out of it. Explain
the cron fields, and derive the expression an interval asks for. Keep an
untouched function scaffold in step with the node's ports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:58:19 +02:00
stroblmeandClaude Fable 5 b5949938f0 Format four files ruff had drifted from
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:47:16 +02:00
stroblmeandClaude Fable 5 2613fbead4 Allow the standalone Vite origin through CORS
`make dev-frontend` serves the SPA from localhost:5173 and talks to the
stack's backend, which refused it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:46:35 +02:00
stroblmeandClaude Fable 5 82df345bf0 Give the dev stack a broker, a time-series database, and live backend source
The mqtt and influx node types had nothing to talk to locally; mosquitto and
influxdb fill that in, on the compose network only — no host port, since 1883
usually belongs to a real broker on the machine.

The same file's --reload only ever reloaded the source baked into the image:
develop.watch needs `docker compose watch`, and the dev stack starts detached.
A bind mount is what the standard flow actually reads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:46:35 +02:00
stroblmeandClaude Fable 5 b4d5aa6186 Let two backend CI runs share a runner host
The job kept the fixed container names and the fixed 5432 that compose.ci.yml
resets for every other job, so a second run could not start. Layering that
file leaves the db on a port Docker assigns, which the suite is told about,
and the project name is now per run so one teardown cannot take another run's
database with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:46:25 +02:00
stroblmeandClaude Fable 5 75f68655ef Point the backend suite at the db container while the stack is up
The integrated stack drops every published host port, so make test-backend
could not reach Postgres on localhost:5432 whenever the stack it shares a
machine with was running. It now asks Docker where the container is; with the
stack down, .env still stands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:46:25 +02:00
stroblmeandClaude Fable 5 32b35255d0 Check the four integration nodes under strict mypy
mqtt, http, influx and delay carried the prototype's annotations and were
excluded wholesale. Annotating them leaves nothing to exclude: what the two
untyped dependencies need is an implicit-reexport allowance for
influxdb_client and the import-site ignore croniter already gets elsewhere.

InfluxDbNode.inject now takes the durable flag its supertype passes, and the
cron loop reads its stop event once instead of dereferencing a field that
stop_cron() may have cleared.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:46:15 +02:00
stroblmeandClaude Fable 5 143395358a Delete the flows and users the Playwright specs create
The specs run against a development stack, so every run left a test_flow_*
in someone's flow list. Each spec now tears down what it made in an afterAll,
which runs whether or not the tests passed, and the three copies of the API
helper move into tests/utils/api.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:46:07 +02:00
stroblmeandClaude Fable 5 a733e0d582 Format the provenance test so ruff stops failing on it
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:44:23 +02:00
stroblmeandClaude Fable 5 93b4a9a0b1 Step a paused flow, and deliver what a rate limit held back
Three things a pause and an interval were quietly losing:

- A rebuild builds a fresh pipeline, so nothing is paused any more and no
  resume ever comes for what the old one parked. Release it on rebuild.
- POST /flows/{name}/step takes the oldest parked item and runs that one wave
  while the flow stays paused, so a held-back cascade can be walked through.
  Nothing parked answers plainly rather than failing.
- A per-port interval was leading-edge only: a producer going quiet inside the
  window left the consumer on the value before it. The held value is kept and
  a flush item scheduled on the queue's existing timer, so the window ends with
  a delivery. One timer in flight per node, and none without a queue to run it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:44:17 +02:00
stroblmeandClaude Fable 5 0b2d8e587a Say what a node returned when it is not a dict of ports
Outputs are keyed by port, so a bare value cannot be one. The single mapping
point every caller routes through raised a bare AttributeError from
retval.items(); it now names the problem, and the message reaches the node the
way its other errors do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:44:06 +02:00
stroblmeandClaude Fable 5 fa12ffd323 Copy nodes, bind the keys, and keep publish within reach
The floating bars used to make way for a side panel, which hid Publish
exactly when a node had just been edited. They keep their lane now and
the panel is inset out of it; the enlarged code editor stays a floating
surface between the two bars rather than covering them.

Shortcuts are one small registry (`lib/shortcuts.ts`): undo, redo, ⌘K,
copy, paste, and ⌘S — which publishes the flow, or applies the code when
the editor has focus. Copying carries a node's own source through
localStorage, so a paste works in another flow too.

A flow is now named where a node is, at the top of its panel and only
there, and both take effect on Confirm. Flow-level edits go through the
undo stack with everything else, clicking the canvas puts the flow panel
away, a failing node opens the logs filtered to its own traceback, and
the panel carries its test id on a phone as well.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:43:41 +02:00
stroblmeandClaude Fable 5 929bb56662 Let an edge pulse end where it started, and draw what it carried
The pulse named its own end colour, so a selected edge decayed to the
neutral stroke and snapped back to blue — a second pulse. Dropping the
`to` keyframe lets the animation land on whatever the edge rests at.

Clicking an edge now shows the same sparkline the node panel draws for
that message, so a single value is read against how it has been moving.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:43:26 +02:00
stroblmeandClaude Fable 5 2303c1f92d Show and revoke registered agents under Admin
An agent that registered itself was invisible, and withdrawing one meant
deleting rows or rotating the signing key. It now sits next to Users, with
the live-token count that tells an approved agent from one that only
registered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:36:48 +02:00
stroblmeandClaude Fable 5 45392d31d9 Add the Secrets and Alerts screens
Two backends that had no way in: secrets could only be set through the API,
and alerting could only be configured by hand-editing its file. Both are
engine-wide operator settings rather than personal ones, so they get sidebar
entries of their own instead of tabs under the per-user Settings — and the
missing-secret error now names the place the page actually is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:36:41 +02:00
stroblmeandClaude Fable 5 f239c884b6 Add OAuth client list and revoke endpoints
Superuser-only management for agents that registered themselves: list them
with whether anyone approved them, and withdraw one without rotating the
signing key and cutting off every other agent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:29:47 +02:00
stroblmeandClaude Fable 5 19970b0ad8 Group the notepad into work packages, move two reliability items to M5
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 16:20:48 +02:00
stroblmeandClaude Fable 5 b1e1b4d501 Make an endpoint follow the pointer, and recede further at rest
Two things from using it. The label only jumped to its new place on
release: its position comes from a memo of ours rather than React Flow's
own store, so nothing moved it until the drag had already finished. It is
now updated on every drag frame, with the write to storage still happening
once at the end rather than once per pixel.

The edges deliberately do not depend on that position — an endpoint's
edges follow from which messages it touches, never from where it sits — so
dragging one no longer rebuilds the edge array on every frame.

And they sit further back at rest, so the nodes read first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 16:14:37 +02:00
stroblmeandClaude Fable 5 c70359af67 Let the canvas endpoints be moved, and lift them on hover
Three things they were missing: they could not be dragged out of the lane
they were placed in, they stayed the same weight whether or not you were
reaching for one, and the label sat right against its connector dot.

Where one has been dragged to is a view preference, not part of the flow —
writing a position for a dashboard widget into flow.json would be a lie
about what the document holds — so it lives in the browser, keyed by flow.

Also folds the dashboard shot into `make verify` and drops the one-off
scripts that had accumulated beside it; the durable coverage is the
Playwright specs, which all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 16:07:07 +02:00
stroblmeandClaude Fable 5 75c26ef000 Say what caused a value, and draw what is not a node
Moving a dashboard slider lit up an edge between two nodes that had done
nothing. The canvas pulsed on the message's timestamp alone, and a message
has no idea who published it — so it credited whichever node happened to
be drawn as a producer.

That was never only about dashboards. Two nodes producing one message
pulsed both their edges whichever fired, and a message produced in another
flow changed with nothing on screen to account for it at all.

Values now carry their cause: a node, a dashboard widget, another flow, an
agent or an API caller. An edge pulses only for the producer that actually
published, and the edge inspector says where a value came from when it did
not come from a node.

What is not a node in this flow is now drawn as one — a label rather than
a card, because a dashboard with twenty tiles would otherwise bury the
logic the canvas exists to show. That covers cross-flow wiring too, which
is the link in/out affordance that has been missing.

They are never part of the document. They join at render, after everything
that reads or writes the canvas nodes, so an autosave, an undo or a delete
cannot reach them — with a Playwright test that drags a node and asserts
the stored flow still holds exactly what it did.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 15:51:54 +02:00
stroblmeandClaude Fable 5 3fa9141eb9 Scope the node-type fixture check to the built-ins
Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Test Backend / test-backend (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
Installing the connectors made this fail, which is the test working: it
insists every offered type is exercised. But a connector is a separate
package with its own tests, so it is not this suite's to cover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 11:16:41 +02:00
stroblmeandClaude Fable 5 85abf58337 Count flows rather than nodes in the health report
is keyed by node id, so a four-node flow reported four flows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 11:15:49 +02:00
stroblmeandClaude Fable 5 42075a6dae Give a connector node an icon that is not a code icon
A device read over a proprietary protocol was showing the fallback used
for a function node. A connector's type cannot be in the built-in icon
map — that is the point of it being a connector — so they share a plug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 11:14:35 +02:00
stroblmeandClaude Fable 5 4de3cd7c66 Record the connectors and what is still read-only
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 10:00:10 +02:00
stroblmeandClaude Fable 5 be6d4fc13c Build dashboards you can actually look at and press
Widgets bind to a message name and read it live off the socket the editor
already had — lifted out of the flow editor so a dashboard route gets the
same values, which also gives the home page live data for free.

The input widgets close the loop the other way: a slider publishes into
the graph and whatever consumes that message runs. Verified end to end in
the running app — moving a slider set a flow input, and the stat bound to
what the flow computed from it followed.

View mode is plain CSS grid. A wall panel that only displays should not
download the code that lets someone drag things around, and it now does
not. Editing is a widget picker, a per-widget width control and a
settings card fed by the message catalog.

No new dependencies: the slider is a range input, the gauge is an arc,
and the markdown is a five-line subset. Charts are the one widget still
missing — they need a charting library and the chart tokens the design
guidelines reserved — so they are stored and validated but not offered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 09:33:02 +02:00
stroblmeandClaude Fable 5 895bd89b2b Dashboards as documents, and messages as something to bind to
A dashboard is its own document rather than widgets placed in a flow.
Node-RED's dashboard tab is 260 nodes, about forty of them pure layout,
which is exactly what the small-graph principle exists to avoid — and
since the graph is already wired by message name, a widget can bind to a
name without belonging to any flow.

Stored beside the flows in the same repository, sharing their write lock
and commit, under a directory the flow listing ignores. No draft/publish
split: nothing executes a dashboard, so edit mode is its own staging area.

Two things it needs from the engine. A message catalog spanning every
flow, because a wall panel shows the heating next to the solar and the
flow-scoped API is the wrong shape for that. And a way to put a value in
without owning a node — a slider is a real value that happened to come
from a person — which runs whatever consumes it and applies the same type
check a node's output gets. Only a message some flow declares can be
published to; flows own the namespace.

Charts also need more past than the 120 points a sparkline wanted, so a
chart widget declares its depth and the engine keeps that message's
series that deep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 09:20:49 +02:00
root 80acf342f4 Merge branch 'main' of git.stroblme.de:Fluksio/app
Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Test Backend / test-backend (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
2026-08-16 08:59:41 +02:00
rootandClaude Fable 5 fef0545ae4 Let a function node's own settings be edited
A python node's params reach process() as whatever its author put there,
but there was no way to put anything there: the settings form is built
from a type's declared schema, and a function node declares none.

Node types now say whether they take settings beyond their schema, and
the panel offers a key/value editor for the ones that do — named, typed
as text, number, on/off or JSON, and laid out like the port list beside
it. Rows are keyed by position rather than by name, so renaming a setting
does not remount the row and lose what was being typed into it.

Verified in the running app in both themes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 08:20:17 +02:00
rootandClaude Fable 5 55bdc49510 Record the vocabulary node types in the roadmap
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 08:08:49 +02:00
rootandClaude Fable 5 d9150c118d Add the vocabulary a real installation is mostly made of
Every one of these could be written as a function node, and until now had
to be. But nine node types account for most of what an actual Node-RED
installation contains — inject alone is placed 58 times, change 62 — and
a rule you fill in reads better on a canvas, and survives editing better,
than the same five lines repeated eighty times.

Inject, switch, change, filter-unchanged, join, trigger, command, file
and ntfy. They configure themselves through the editor's generated form,
so none of them needed frontend work beyond an icon.

The stateful ones (filter-unchanged, trigger) keep what they remember in
the engine's own state under a reserved prefix, never as a message. That
is the line: code you write is a pure function of its inputs, node types
the engine ships may remember things.

Verified against a live instance — a hook feeding change into
filter-unchanged into switch scales a raw reading, swallows a change
inside the deadband, and routes the rest to the right branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 08:08:40 +02:00
rootandClaude Fable 5 dbdbcc1091 Tell someone when the engine breaks
Everything that goes wrong already travelled the event bus, but the only
subscriber was the editor's websocket — so a flow quarantined at three in
the morning was invisible until someone opened the browser.

An alert manager now watches the same bus and forwards failures to ntfy,
email or a webhook. Most of what it does is decline to send: the same
node failing every second is one alert with a count of what followed, a
connection flapping up and down is muted until it settles, and nothing
gets past ten notifications an hour. Verified against a live instance —
six identical failures produced one alert carrying the real traceback
message.

Channels and rules are configured through the API, with a test send so a
channel can be proven before anything depends on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 08:01:50 +02:00
rootandClaude Fable 5 04329149b3 Journal work before running it, so a crash stops losing messages
Execution was fire-and-forget: an MQTT message or webhook ran a cascade
on a ThreadPoolExecutor built for that one wave, and an engine that died
halfway through simply lost whatever was in flight. Concurrent triggers
each built their own pool, so load meant unbounded threads.

Every external trigger is now journaled to a Redis Streams queue before
anything runs, and acknowledged only once its cascade finishes. A
consumer thread drives cascades on one long-lived pool while node bodies
run on another, so a cascade cannot starve the nodes it is waiting for.
A reaper reclaims what a dead consumer never acknowledged — verified end
to end: work journaled while the engine was stopped runs on restart, and
work abandoned mid-cascade comes back as a second delivery.

At-least-once needs a guard, so nodes that reach outside are marked
non-idempotent and skipped on a redelivery they already completed.
Without Redis the queue degrades to an in-memory one that does not
pretend to be durable, and interactive callers still run inline.

Also fixes two things this turned up: a delay node was sleeping on a
worker thread, where a handful of them could occupy the whole pool, and
webhooks 404'd whenever MCP was enabled because the app mounted at /
answered first for every path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 07:57:07 +02:00
rootandClaude Fable 5 2cc25970e3 Let a flow keep state in the messages it already has
Logic nodes are pure functions with no state handle, but real automations
count things and remember the last reading. The shape for that is a
message a node both reads and writes: the graph already declines to make
a node depend on itself, so this worked by accident. It is now defined,
tested, and checked — a node that is the only writer of a message it
reads is told at edit time that it needs a starting value, rather than
silently never running.

Feeding a value back between two nodes was still a cycle, and rejected.
An input can now be marked non-triggering: read when the node runs, never
the reason it runs, and no dependency either way. That is what a back
edge actually means.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 07:37:46 +02:00
rootandClaude Fable 5 f8693daad6 Split the node types into a package, and stop reconnecting per message
nodes.py had grown to 2k lines holding every integration behind a single
blanket mypy exemption. It is now a package split by the outside world
each node talks to, so the exemption shrinks to the four integration
modules; base and mlp are type-checked, which turned up a dozen missing
annotations.

The senders opened a fresh connection — and, in the MQTT case, a fresh
thread pool and event loop — for every single message. HTTP senders now
share one pooled client, and a publisher holds one broker connection for
its lifetime, fed from a bounded queue that drops the oldest value when
the broker cannot keep up.

An HTTP sender also no longer trips over a JSON reply that is not an
object: outputs are keyed by port, so a bare scalar is a valid reply with
nothing to publish.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 07:33:10 +02:00
rootandClaude Fable 5 e7e48c4f13 Supervise the loops a flow starts, and give up loudly
A node's subscription, schedule or poll loop was a bare asyncio task: one
that raised outside its own retry handling was simply gone, and the node
went on being listed as running while nothing listened any more.

Those loops now run under a supervisor that restarts them with a growing
delay and quarantines a flow that burns through five restarts in five
minutes — a flow crash-looping every second is worse than one that is
visibly stopped, and the dashboard can now say which.

The MQTT subscription loses its private five-second reconnect in the
process: one backoff policy per socket, and it belongs to the supervisor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 07:23:02 +02:00
rootandClaude Fable 5 5462842b8a Supervise the engine's host: deep health, loop watchdog, one worker
The API image ran four uvicorn workers, and each one built a full flow
controller — four sets of MQTT subscriptions, cron ticks and webhooks.
Runs one worker now; scaling out is the worker split, not more processes.

Adds a loop-lag watchdog and a deep /utils/health/ that fails when the
event loop is wedged or Redis is unreachable, the two failure modes a
process-alive check never sees. Autoheal restarts on that signal, behind
a compose profile because it mounts the Docker socket.

The private user-seeding routes now need an explicit opt-in rather than
just ENVIRONMENT=local, so a deployment that kept the default never
exposes them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
2026-08-16 07:14:28 +02:00
Melvin Strobl f6e6f1b0d5 comments
pre-commit / pre-commit (push) Canceled after 0s
Test Backend / test-backend (push) Canceled after 0s
Signed-off-by: Melvin Strobl <lc3267@kit.edu>
2026-08-16 00:34:45 +02:00
Melvin StroblandClaude Fable 5 8d82d6c4ec Let agents drive the flow API over MCP
Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Test Backend / test-backend (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
The engine now speaks MCP at /mcp, with a built-in OAuth 2.1 authorization
server in front of it: an agent registers itself, sends a human to the browser
to approve it, and exchanges the resulting code for a token. PKCE is required,
codes are single-use and stored only as hashes, the browser is redirected to
the URI that was registered rather than the one asked for, and refresh tokens
rotate so that replaying a spent one revokes the whole line.

Twenty tools cover reading, building, publishing and running flows, and each
one calls the same REST endpoint the dashboard calls, in-process, carrying the
caller's own token. That keeps one description of what a flow is and what may
be done to it — validation, the draft/publish split, the version check — and
means an agent can do nothing a person could not do in the browser.

Agent tokens are RS256 with a keypair of their own rather than the secret that
signs browser sessions, so deleting the key withdraws every agent without
logging anyone out, and deps.decode_token grew the branch that trusting a
second issuer will need when the hosted login arrives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 00:22:41 +02:00
Melvin StroblandClaude Fable 5 3724b68f23 Add the connector contract, reusable nodes and per-port intervals
Connectors are the device-facing node class third parties write, so the
surface they build against is versioned and documented: ConnectorNode carries
a declared contract version, a polling loop that publishes only what changed
and reports health around it, and parameters whose credential fields are
marked x-secret so the editor offers the secrets store instead of a text box.
They are found through the fluksio.node_types entry point group, with the
package's own metadata as the manifest. docs/connectors/ has the contract and
the authoring guide; connector-skeleton/ is a working one to copy.

The controller no longer knows what any node type is: start, stop and
report_health are protocol methods on Node, and the built-ins were migrated to
them first, so the hooks a connector implements are the ones the engine has
been driving all along.

Marking a node reusable moves its source to _lib/ and points the node at it by
name. Other flows instantiate it with their own ports and settings, one fix
reaches all of them, and a shared source still in use cannot be deleted.

Ports gained an interval: an output publishes, and an input wakes its node, at
most every n seconds. State keeps the latest value, so only the delivery is
skipped, and pressing Run is never throttled.

Also fixes autosave sending no version on its first save of a session, which
made every flow saved more than once conflict with itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:57:44 +02:00
Melvin StroblandClaude Fable 5 7344eac262 Start, stop and pause flows, and show what their nodes print
Flows can now be taken off the engine and put back. Stopped state lives in a
runtime.json beside the flow, not in the flow document: the canvas autosaves
that document, so a stopped flow would otherwise start itself again on the
next edit. A stopped flow gets no subscriptions, schedules or webhooks, its
nodes are skipped by the scheduler, and running it answers 409. Pausing holds
a flow's nodes while its values keep arriving, so the canvas still shows what
is coming in.

Node code is user code and print is how it says things, so stdout is teed
through a contextvar sink active only during a node execution — one event per
execution, capped, so a chatty node cannot outrun the stream. A node that
fails sends its traceback the same way, trimmed to the author's own frames.
The dock gains a logs panel and a pause control; the dashboard replaces its
placeholder with what is running, stopped or failing; the edge inspector can
send the last message again.

Single-stepping is deferred and noted: the scheduler keeps no progress between
calls, so a step button would re-run the same node rather than advance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:35:08 +02:00
Melvin StroblandClaude Fable 5 606ab3c423 Separate editing from running with a draft/publish split
Edits autosave to flow.draft.json and nodes.draft/ instead of the files the
engine reads, so the pipeline keeps running the published version until
someone publishes. Every save carries the version it was based on: a second
client editing the same flow is refused with 409 and offered the choice
between their version and its own, rather than silently overwriting.

Draft saves no longer rebuild the pipeline; validation and node status for a
draft come from a throwaway build that never touches live state.

Also fixes a latent bug where an empty state backend is falsy, so Pipeline
quietly built itself a second, private state and left message history empty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:13:15 +02:00
Melvin StroblandClaude Opus 5 36be6f1081 Drop the InfluxDB rotation item, note the orphaned flow state
Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Test Backend / test-backend (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
The token does not need rotating. Deleting a flow does leave its value,
timestamp, version and history keys behind in Redis.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkmeRiyeYmVZqJVwuyHq9o
2026-08-15 22:07:31 +02:00
Melvin StroblandClaude Opus 5 881d5c1cf2 Clear the notepad of what this pass closed
Records what the work turned up on the way: no way to reach a stored
secret from the params form, flow-level edits outside the undo stack,
the mobile panel having no test id, and the Playwright specs leaving
their users and flows in the development database.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkmeRiyeYmVZqJVwuyHq9o
2026-08-15 22:00:18 +02:00
Melvin StroblandClaude Opus 5 e3b3d234ce Sparkline every message a node names
A number in the panel says where a value is, not where it has been. Each
named port in Consumes and Provides now carries a sparkline under it,
fetched once on open and extended from the socket as values arrive, held
to the same 120-point window the server keeps.

Both lists render through the shared port row, so a sink node shows what
flows through it rather than nothing. A flat series draws its mid line
and drops the redundant range label, a lone reading is just its dot, and
a series spanning decades switches to a log scale with the labels left
in real units.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkmeRiyeYmVZqJVwuyHq9o
2026-08-15 21:59:30 +02:00