Commit Graph
113 Commits
Author SHA1 Message Date
stroblmeandClaude Opus 5 683c25b26d Home: one time range across the health block, and failures that follow it
The health screen was fixed at 24 hours everywhere except its lists, which
were fixed at nothing: `failuresQueryOptions` read the newest 100 rows and
`HealthActivity` filtered them client-side, so on a busy engine the failures
list covered whatever few minutes 100 rows happened to span while the chart
beside it spanned a day — and pinning an older minute showed an empty card.

One `RangePicker` now sits on the Health heading and governs the whole block:
the tiles, the flow table, both charts and both lists. Presets are 1h / 6h /
24h / 7d — the collector prunes at `OBS_RETENTION_DAYS` (30), so a week is
behind the last one. The longer windows ask for coarser buckets, since a week
of minute rollups is ten thousand points nobody can see.

Failures get the escape hatch the runs already had: a pinned minute is asked
for with `since`/`until` rather than filtered out of what is held, and the
list itself is bound to the selected range. `RUN_DEPTH`/`EVENT_DEPTH` become
one `LIST_DEPTH`, which now buys coverage of the window on screen instead of
a fixed newest-N — narrowing the range is what makes the same rows reach the
whole of it. The "Failures (24h)" tile counts errors over the selected window
from the rollups the table is drawn from, so tile, column and chart agree.

The node-panel and edge trend curves get no picker. They are a Redis ring of
the last 120 values per message with no window to ask for, so hovering one
reveals what it actually shows — how many readings, and the span they cover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 11:51:42 +02:00
stroblmeandClaude Opus 5 3d73e42313 Roadmap: tick the CI workflows and the broker/InfluxDB dev services
Both were already in the tree — the four workflows under .gitea/workflows and
the mosquitto/influxdb services in docker/compose.dev.yml. The CI line says
the wiring is unproven, since nothing here has watched it run on a push.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 11:49:07 +02:00
stroblmeandClaude Opus 5 0cff521fd2 Roadmap: tick what M5 already delivered
Only the soak harness was still open here: `backend/scripts/soak.py` and
`make soak` exist with all four scenarios, so the Phase 0 item is done —
with the two caveats from NOTEPAD on the line, since the redis scenario
stops a shared container and the engine kill barely stresses redelivery.
The other M5 items (brain graph, modules, out-of-process nodes, engine
history and the health screens) were already ticked and match the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 11:47:41 +02:00
stroblmeandClaude Opus 5 b9e67fa436 Home: spread the activity table's slack, shorten its counts, shorten the fade
The trend column carried `w-full`, which made it swallow every spare pixel:
the five columns beside it huddled in the left 40% with a dead gap before the
curve. It takes a bounded third now, so the auto layout spreads the rest over
the columns that have content to show, and a long flow name still widens its
own column into the card's scroll.

`si` reaches the rollup counts it was written for — 179240 executions reads as
"179k", and the failure badge and the failures tile follow. The plain window
leaves the small ones alone: 615 is still 615. The standing counts (nodes,
flows running, queue depth) stay exact, as does everything acted on digit by
digit.

The fade ran a full 40 of the 100-unit box. In a table cell three times the
width of a panel that washed out readings still worth seeing, so it is 25 —
a hint at the left end rather than a quarter of the series.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 11:38:07 +02:00
stroblmeandClaude Opus 5 64f57b732b Regenerate the SDK for the events time window
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 11:35:03 +02:00
stroblmeandClaude Opus 5 2554488a73 Backend: real alert test results, state cleanup on delete/rename, node trigger errors, queue and collector fixes
`AlertManager.send` swallowed every delivery failure, so the alerts screen's
Test button answered 200 whatever happened — the one thing it exists for. It
takes `raise_on_error` now, which only the test route passes; the per-channel
loop keeps the swallow, because one dead channel must not stop the others
hearing about the same fault. A refused delivery answers 502 with whatever the
sender said.

Renaming a flow left its values under the old name for good: the delete path
already swept them, the rename path never did. It calls the same `forget_flow`,
which covers the messages and the `__ts__`/`__version__`/`__history__`
bookkeeping keyed by message name. Cleanup, not migration — they repopulate
under the new name on the next run.

Triggering a node by hand ran `Node.__call__` with nothing catching it, so a
node that raised produced a 500 and a stack trace in the server log, and
nothing at all on the canvas. `Pipeline.publish_error` is the reporting half of
`_execute_node` lifted out; both paths go through it, so a manual failure now
reads the same on the canvas and in the metrics as a queued one. The route
answers 400 with the node's error.

`MemoryWorkQueue.stats()` counts claimed-but-unacknowledged work rather than
reporting zero, so the health tile means something without Redis. The metrics
collector's held tracebacks are capped at `DETAIL_CAP` and swept on the same
`RUN_STALE_S` cutoff the open runs use, instead of one untruncated traceback
per node kept for the life of the process — a traceback still survives the
flush between the log and the failure it belongs to.

`GET /observability/events` takes `since`/`until`, the window `/runs` already
took, so a failures list can cover the span the charts beside it are drawn from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 11:33:25 +02:00
stroblmeandClaude Opus 5 69421b857d Brain: hover labels, activity falloff, rim-anchored directed edges
A name under every circle was most of what the graph drew, and none of it was
what someone looks at the brain for. Only the hovered or keyboard-focused
neuron names itself now — a failing one always does, since colour is never the
only carrier of a status. The label is absolute and the collide radius already
held that space, so revealing one moves nothing. Taking it out of flow also
fixed the off-centre edges: the node box was as wide as its widest label, so a
32px circle inside a 140px box sat 54px left of where the layout put it and
every edge aimed at the box rather than at the circle.

Neurons are filled discs rather than `bg-card` outlines, which were a hairline
against `--background` in light and close to nothing in dark. The fill is a
neutral `--muted-foreground` alpha and stays neutral: status still speaks only
through the border and the word beneath. Its lightness carries how recently the
open page saw that neuron publish — brightest just after it fires, decaying over
90s to a floor it never drops below, so a graph left open sorts itself into what
is busy and what is not. Edges do the same on their stroke. The signal is only
what this session has seen on the socket, so a fresh page starts everything at a
neutral middle rather than claiming a history it does not have. Both are CSS
transitions gated on `prefers-reduced-motion`, decayed out of and snapped into,
never the other way round.

`BrainEdge` now carries both circle radii and trims its path back to the rims,
with an arrowhead at the end it flows into. The barbs are two more segments of
the same stroked path rather than an SVG marker, so they dim with the line they
belong to. Neither trim reaches past the midpoint, so neurons closer together
than their radii still get a line pointing the right way.

`edge-pulse` reads its landing colour from `--edge-rest` instead of naming
`--muted-foreground`, so a selected edge — which rests in blue — decays into its
own colour instead of crossing to grey and snapping back at the end.

Obsidian's graph view is the reference for the first three: filled nodes sized
by degree, labels on a fade threshold with hover to recover them, and arrows as
an explicit affordance. The falloff-by-opacity encoding is the standard one in
the dynamic-graph literature.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 11:31:13 +02:00
stroblmeandClaude Opus 5 78c605bd37 Home: one sparkline style with a left fade, SI values, a full-width activity table, roomier panels
The trend curve was two components: a rich one in the node panel and on edges,
and a bare line in the flow table. It is one `Sparkline` now, taking the colour
token, the height and whether the live dot and the readout show. The flow table
draws its rollup in the chart ramp with no dot — the rollups are polled, so the
right edge is the last completed slice rather than this instant — and keeps its
"nothing yet" state, as the panel keeps its three distinct silences.

All of them fade out to the left, through an SVG mask over the curve and its
area. The dot sits outside the mask: the newest reading is the one thing that
must stay solid.

`si` replaces `compact` and the ad-hoc "k" the uPlot axis carried. It prefixes
k/M/G and m/µ, but only outside 0.01–1000, where the plain number is already
the shortest thing to read and a written unit ("0.4 ms") stays honest. The
sparkline readout asks for four digits, so two neighbouring readings never
collapse into one string. Exact counts and anything the user acts on — a
payload, a form field, the edge inspector's value — are left unrounded.
`src/lib/utils.check.ts` asserts the rounding cases.

The activity table now spans its card: the flow name anchors the left, the four
numbers read down their own centre, and the trend takes the slack on the right.
Panel rhythm steps up one notch, gap-5 to gap-6 outside and gap-2 to gap-3
within a section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 11:27:54 +02:00
stroblme 5483de13c0 NOTEPAD: components needed for wall-panel parity with the current home dashboard 2026-08-17 09:54:28 +02:00
stroblmeandClaude Opus 5 5fc7bbee34 Home: widen the brain layout, fix its entrance, add a clear button to pinned charts
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
The force layout settled roughly square, so in a wide band the graph sat as a
small island. Vertical centring now pulls nearly three times as hard as
horizontal and the seed sits on an ellipse, which settles at about 2.2:1
without stretching the circles or the edges.

The graph also slid in on load: the rebuild fit ran with a 300ms duration, so
React Flow animated the pan from its default viewport to the content. The fit
is instant now, and the canvas grows in from the centre with `scaleIn` once it
has a layout.

The pinned chart header said "Esc clears", which a phone cannot act on. It
carries the same "× Clear" button as the list header instead; Escape still
works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
2026-08-17 08:33:27 +02:00
stroblmeandClaude Fable 5 0832be32a9 Note the run-list cap a busy pinned minute would hit
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_017MeiWk3Yq12n2pTvnQWYvt
2026-08-17 00:29:02 +02:00
stroblmeandClaude Fable 5 0b91379914 Let a pinned minute read runs from the whole day
/observability/runs gains since/until, so the throughput chart's pin asks
the server for its minute instead of filtering a fixed recent list. This
engine writes ~60 runs a minute, so any minute but the newest read empty.

since is inclusive and until exclusive, matching the minute buckets the
charts are drawn from. Hover stays the client-side preview it was:
scrubbing a day would otherwise be a request per minute rested on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
2026-08-17 00:27:40 +02:00
stroblmeandClaude Fable 5 affb0a5f8c Fold the brain and health screens into Home
Home is the one overview now: the brain graph flat across the top, the flow
switches, then the health sections. The /brain and /health routes and their
sidebar entries are gone.

- charts report their cursor and clicks, so hovering one filters the list
  beside it to that minute and a click pins it until Escape or Clear
- chart values round to about three significant digits, the legend mounts
  under the plot so it can wrap without leaving the card, and axis ticks
  shorten past a thousand
- the embedded brain leaves the wheel to the page rather than zooming
- number fields no longer draw their up/down spinner (NOTEPAD)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
2026-08-17 00:17:14 +02:00
stroblme fe346ea7a3 up
Signed-off-by: stroblme <stroblme@posteo.de>
2026-08-17 00:04:22 +02:00
stroblmeandClaude Fable 5 83c30aa1c7 Take the engine off the path node code imports from, and let it stop
The worker script is handed to the interpreter by path, so app/flow was
sys.path[0] for every node: `import queue` got the engine's. It now drops
its own directory before anything else imports, and runs with the
deployment's credentials scrubbed out of its environment.

Also: reload builds off the event loop, the pool wakes what is blocked on
it when it stops, a refused metrics flush is kept for the next one rather
than dropped, and the cascade events are paired through failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
2026-08-16 23:46:08 +02:00
stroblmeandClaude Fable 5 b33be3fdd4 Load the engine, then take its pieces away and check what survives
A standalone harness, never part of the test run, that drives a real stack
through the durable path — the webhook, which journals every trigger — and
then stops Redis, kills the engine mid-cascade and restarts the broker
under a live subscription. Latest-value-wins with concurrent cascades means
counter equality is not a promise, so what it asserts is that the queue
drains, that state ends on the last value sent, and that nothing reached
the dead-letter stream.

Every docker verb goes through one helper that checks the compose label
before it acts and refuses anything outside this project, because the
machines this runs on host unrelated services.

Two invariants are deliberately looser than they look. The queue belongs to
the whole stack, so "nothing pending" would be measuring other people's
traffic; the harness waits on the age of the oldest unacknowledged entry
instead, which a stuck item always dominates. And the observability tables
are cleared only after the collector's flush interval has passed, since
deleting a flow publishes an audit event of its own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
2026-08-16 23:14:14 +02:00
stroblmeandClaude Fable 5 4bcd38354b Draw every flow as one graph, merged on what it talks to
A node type can now say which outside thing its parameters point at, and
nodes sharing one — a broker topic, a URL, a bucket — are drawn as a single
neuron on a new /brain canvas. That makes the wiring which runs between
flows through a broker visible for the first time; no single flow's canvas
can show it. The key is read off stored parameters, so a credential
reference never reaches an id.

Layout is a d3 force simulation settled once and then frozen, lit by the
socket the editor already listens to: a neuron pulses when any node behind
it publishes, and its connections light as values pass.

Fixes the message pulse while here: interpolating the stroke against the
edge's `color-mix()` resting colour went through oklab and left the gamut,
which turned every pulse on both canvases fluorescent yellow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
2026-08-16 22:59:31 +02:00
stroblmeandClaude Fable 5 af3ba51571 Keep the engine's own history, and a screen that reads it
A second bus subscriber folds executions, errors, timings and queue lag
into per-minute rollups, keeps failures with their traceback and an audit
trail of who published what, and records one row per cascade — manual runs
and previews included, under an id of their own that writes no idempotency
markers. Read back through /observability/*, which always answers 200 so a
degraded engine still renders its own health screen.

Also fixes two things found on the way: node-health alerts read `status`
where the engine publishes `health`, so a device dropping never alerted
anyone, and the Redis queue reported `parked: 0` whatever was held.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
2026-08-16 22:29:32 +02:00
stroblmeandClaude Fable 5 f300c43f3a Run python nodes out of process, with modules of their own
User code no longer execs in the engine. A pool of persistent worker
subprocesses speaks one JSON object per line; the controller installs a
proxy as the node's function, so every execution path funnels through it
and the pipeline is untouched. A crash costs one subprocess, a per-node
timeout is a kill, and cancelling from the canvas is that same kill.

The workers run a venv of the user's own on the data volume, filled from
a pip manifest versioned beside the flows. Applying it retires the
workers and rebuilds, so a package lands without restarting the engine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
2026-08-16 21:43:36 +02:00
stroblme 979c9d3c1f Note that a node's error status clears before anyone sees it 2026-08-16 19:32:10 +02:00
stroblmeandClaude Fable 5 49a3f3da01 Note the canvas follow-ups the review turned up
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 19:16:06 +02:00
stroblmeandClaude Fable 5 949fc0bf7e Ask for the name in a dialog instead of a field that is always there
Create in both overviews now opens a dialog, so the toolbar is a search box
and one button. The specs and the capture script open it first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 19:08:22 +02:00
stroblmeandClaude Fable 5 c2321fe942 Give a dashboard the panel's own size, and put the dots on its grid
A dashboard now carries the canvas it is drawn for (canvas_width /
canvas_height, presets plus two numbers in the settings panel). Editor and
wall panel render that surface at its true pixel size and scale it to fit,
so a side panel opening changes only the scale — never the arrangement
being made. With the width and column count known the dot pitch is exact,
(width + gap) / columns by row height + gap, so a dot sits where every
widget corner snaps. The wall panel shows no dots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 19:08:09 +02:00
stroblmeandClaude Fable 5 fcd43e9ad9 Keep an endpoint's measurement so its drag stops relighting the edges
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
2026-08-16 18:47:17 +02:00
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