Commit Graph
115 Commits
Author SHA1 Message Date
stroblmeandClaude Opus 5 032c2e3ae6 NOTEPAD: drop what is done, correct what was never true
A sweep against the code. Removed as resolved: the worker pool's `_running`
is keyed by (run, node) now, `WorkItem.kind` documents only the three kinds
that exist, `routeTree.gen.ts` matches its generator, and the host's Node is
well past the version the Vite 7 note was about. Removed as disproved: the
collector's redelivery branch is reachable — Redis reclaims an entry this
process is still running.

Corrected: the demo places six of the fifteen node types, not thirteen — the
count was the widget types. Narrowed to what is left: the header/config sync
now covers settings but only on an untouched scaffold and only one way; a
declared flow input is what the dashboard-input node asked for; the flow
boundary already refits the canvas where node ports do not.

The Playwright hazard is not resolved — both names still resolve to
production — and the fix as written is incomplete: `VITE_API_URL` has to be
overridden to `http://` or every setup call fails the TLS handshake.

Also: one `asText` instead of two, and the queue docstring no longer refers
to a node item that never existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUb8YpL2s3gmN9WTACTt4q
2026-08-20 19:06:48 +02:00
stroblmeandClaude Opus 5 f1e13cdcd4 Flow inputs and outputs are visible and editable in the UI
A flow's inputs are the messages it takes from outside — a dashboard control,
a run, the API — and its outputs are what a batch run reports. Both existed in
the document and in the engine, and neither had any UI: the values looked
hard-coded on the canvas and the Run button always used the declared defaults.

The canvas now draws each as a labelled endpoint, the way it already draws a
dashboard tile or another flow, skipping an input something else already
accounts for. The flow panel edits them — mode, name, type, starting value,
and for a live flow the value it currently holds with a way to put a new one
in. Pressing Run on a batch flow asks for its parameters first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUb8YpL2s3gmN9WTACTt4q
2026-08-20 18:10:16 +02:00
stroblmeandClaude Opus 5 4355c917f8 Node settings arrive as keyword arguments, not a params dict
A python node's settings are constants of its own function, so they are passed
the way its ports are: by name. The controller binds them to the compiled
function, the `params` field is gone from the worker and remote protocols, and
a setting sharing a port's name is reported as a node error rather than
shadowing it. The panel's scaffold follows suit and keeps the header in step
with both ports and settings.

The demo's `pace` moves from a flow input to a setting of the training node,
which is what it always was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUb8YpL2s3gmN9WTACTt4q
2026-08-20 17:47:45 +02:00
stroblmeandClaude Opus 5 43f606969a Point the panel link at the installation, not at the browser's origin
Both links out of the dashboard editor were built root-relative, so a portal
serving the app under `/i/{id}` got a URL to itself: the hub has no route
there and answers a bare 404. That is what a device link and "open what a
wall panel sees" both landed on.

They want different answers. The view link is for the person already looking,
so it takes the router's basepath — `appPath` in `lib/portal` is the same
prefix the router applies to every `Link`, for the places that step outside
it. The device link is for a screen, which cannot go through the portal at
all: the shell is served only to a portal session, and the credential that
page carries is the portal's rather than the panel's. So the server now says
where it answers, and `FRONTEND_HOST` is that answer — the same setting the
password-reset links already use.

Also fixes the panel branch in the query error handler, which compared a raw
pathname and so never fired under a portal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHpLJHozysQXjsxAyU1WHj
2026-08-20 17:24:20 +02:00
stroblmeandClaude Opus 5 c0ff1cd5c5 Panels: per-device dashboard sets, paired by code
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
A panel is one screen and the ordered set of whole dashboards it shows, so a
hallway tablet and a workshop tablet carry different sets without either
dashboard knowing about the other. More than one and the device draws a rail
to switch between them — the same rail the editor puts on screen, because the
wall has it and it takes room off the canvas.

A screen has no keyboard, so it pairs rather than logs in: it shows a
six-character code, somebody approves it against a panel from the dashboards
overview, and the credential that mints reaches that panel's published
dashboards and the message endpoints its widgets speak, and nothing else.
Deleting the panel revokes it.

Closes the per-device view and the kiosk credential; supersedes the
multi-page/multi-section UI, since a page is now a dashboard of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHpLJHozysQXjsxAyU1WHj
2026-08-20 16:23:36 +02:00
stroblmeandClaude Opus 5 cf0cd18263 Read a flow name, a port name and an honest docstring
Give the Home flow-activity name cell a `sm:min-w-32` floor beside its
`max-w-0` cap, so an ordinary name reads in full where there is room
while the phone keeps today's truncation. Name the bar widget's nested
reading by its port rather than its qualified message. Correct
`MarkdownWidget`'s docstring to the subset it actually renders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKL7sUgNWhukDEz95vSMQv
2026-08-20 14:52:51 +02:00
stroblmeandClaude Opus 5 53579df610 Keep the node ring lapping while the node is running
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 arc was one-shot feedback for an emit, so a node that works for minutes
showed nothing after the first half second. A run now holds the ring on the
node and laps it until the node stops.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKL7sUgNWhukDEz95vSMQv
2026-08-20 14:43:31 +02:00
stroblmeandClaude Opus 5 dfe7aae4d2 Keep the theme redraw from wiping a chart's x scale
Reaching Home from another page builds its charts in the same commit their
theme effect first fires in, with the readings already cached. uPlot ranges its
scales in a microtask, so `redraw()` re-set the x scale from the chart's own —
still empty — bounds before that ran, and the pending range taken from the data
was lost: axes without ticks, no lines, and no way back but the range control,
which rebuilds the chart. Redrawing without the paths is all a colour swap
needs and leaves the scales alone.

The cards also carried "Nothing has run yet." while the first readings were
still on their way; they now carry the skeleton the rest of Home uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKL7sUgNWhukDEz95vSMQv
2026-08-20 14:33:17 +02:00
stroblmeandClaude Opus 5 a74fe70de7 Keep a long value inside the panel that shows it
The JSON preview capped a ScrollArea rather than the pre inside it. Radix
sizes that viewport in percent, which resolves to the content's own height
against a box carrying only a max-height, so a list or dict of any length
spilled out of the node panel and the edge popover and painted over the
sections below.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKL7sUgNWhukDEz95vSMQv
2026-08-20 14:03:00 +02:00
stroblmeandClaude Opus 5 69c36155c4 Draw the dashboard selection ring inwards
The canvas is clipped to the panel's exact pixel size, so an outward ring
on a widget in column 0 or row 0 was cut off on that side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKL7sUgNWhukDEz95vSMQv
2026-08-20 13:51:35 +02:00
stroblmeandClaude Opus 5 4b0554d766 Gate the flow node pulse on a real emit
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKL7sUgNWhukDEz95vSMQv
2026-08-20 13:38:59 +02:00
stroblme 32cc23fa6b Show the streaming dash on dashboard edges and without motion 2026-08-20 13:28:40 +02:00
stroblmeandClaude Opus 5 f719816992 Select the edge you click in the flow editor
React Flow's selection changes had nowhere to go: the canvas passed
edges but no onEdgesChange, so no edge ever carried .selected and both
the selected-edge stroke and its rest colour were unreachable. The edges
are derived from the bindings, so the selected ids are held on their own
and marked on after the layout has had the array — a rebuild cannot drop
the selection, and selecting cannot make the graph lay itself out again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKL7sUgNWhukDEz95vSMQv
2026-08-20 13:18:13 +02:00
stroblmeandClaude Opus 5 9c6c106065 Anchor the Playwright login helper on the health heading
The greeting it waited for was removed from Home in 449a147.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SKL7sUgNWhukDEz95vSMQv
2026-08-20 12:56:37 +02:00
stroblme ae5ebcd00c updated name
Signed-off-by: stroblme <stroblme@posteo.de>
2026-08-20 12:08:34 +02:00
stroblmeandClaude Opus 5 656905f8e9 Own the live socket outside React so leaving Home cannot orphan it
The socket belonged to whichever hook instance ran its effect first. Passive
effects run children before parents, so on Home that was the brain graph rather
than the shell: navigating to a sibling route unmounted the graph, which closed
the socket, while the shell kept the reference count above zero. From there the
page was deaf for the rest of its life, with nothing left to reconnect it.

A module-level connection with a real refcount replaces it — connect on the
first subscriber, disconnect on the last — and the hook is a thin subscription
with the same signature, correct under StrictMode's mount/unmount/mount.

A 1008 now reconnects instead of returning silently: the token is read afresh
per attempt, and three consecutive rejections fall through to the caller's auth
handler so a revoked session surfaces rather than spins.

The snapshot's emit counts are read into a store of their own, apart from the
live count, so a graph that connects into a busy engine is drawn as busy without
every neuron claiming it just fired. The neuron and edge pulses now key off a
change seen while they were mounted, so returning to Home no longer replays
every emission of the session.

Home gets a live indicator for the case none of this can fix: quiet while the
socket is up, and named in words when it is down, since HTTP polling keeps the
rest of the page looking current.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
2026-08-20 11:58:49 +02:00
stroblmeandClaude Opus 5 ff1b690449 Send a node's emit pulse round the rim instead of scaling it out
The ring hung off the node itself, and `inset` on an absolute child
resolves against the padding box — so on the brain's thick status ring it
landed inside the outer edge and painted over the status the ring carries.
Both node shapes now hang it off a border-less wrapper, where the same
offsets clear the rim whatever border the node draws (measured: 4px on all
four sides, on a 1px card border and a 7px neuron ring alike).

The shape is a conic-gradient arc masked to the padding band, running one
lap in `--duration-pulse` and going out on the way past.

Note: BrainNode.tsx also carries an unrelated in-flight change from a
concurrent session (the `seen` ref gating the pulse on a real emit); the
two could not be separated in one file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 11:51:21 +02:00
stroblmeandClaude Opus 5 85098eb0dc Widget panel: settings for the bar, icon, forecast and chart axis title
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 four widget types added this round had renderers but no way to configure
them. The panel now offers a bar's nested reading, unit and range (no step —
that is a slider's), the forecast's item count on the agenda's field, a chart's
y axis title beside its y range, and the icon's mapping editor: a value, a
glyph and a colour per row, first match wins, with a fallback glyph below.

Adds tests/widgets.spec.ts, which asserts each of them on /view: the nested bar
inside its outer fill, the glyph following the message, five forecast columns
fading outwards, a clock that reads the wall without being flagged unbound,
a segmented control and a latching button reading back what they published,
and an unbound tile that says so instead of taking the page down. The axis
title is drawn into uPlot's canvas, so it is checked by panel round-trip.
mobile.spec.ts grows a bar and a forecast so the width check covers them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
2026-08-20 09:48:42 +02:00
stroblmeandClaude Opus 5 a00b4eaec2 Flow canvas: mark streaming edges with a marching dash
A port with `stream` set publishes repeatedly during one execution, and
until now nothing on the canvas said so. `deriveEdges` carries the flag
from the producing `MessageSpec` onto the edge, `LiveEdge` turns it into
an `edge-stream` class, and the class draws a dashed stroke whose offset
marches toward the target. The pulse still lands on top: both animations
share one declaration when a value arrives on a streaming edge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
2026-08-20 09:14:44 +02:00
stroblmeandClaude Opus 5 86e02b49a3 Grow the flow editor in from the centre, hide the empty brain band
The editor's mount-time fitView animated from React Flow's default viewport,
which read as the graph swiping in from the corner on every open. The first
fit is instant now, later ones stay animated, and a scaleIn wrapper gives the
same entrance the brain view has — with a re-measure on completion so the
handle bounds are not stored mid-scale.

Home only renders the brain band once some flow has nodes, so a fresh install
no longer reserves a screenful of empty space above the flows card.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
2026-08-20 08:43:16 +02:00
stroblmeandClaude Opus 5 cd8fbbcc8c Chart: an optional y axis title
The unit stays on the ticks; `yLabel` names the axis. It is part of the
chart's rebuild identity, so editing the title takes effect at once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
2026-08-20 08:36:36 +02:00
stroblme 49cb5be632 Add the forecast strip widget: N dimming columns over a list message 2026-08-20 08:35:52 +02:00
stroblme 7bba04e1ce Add the icon-by-value widget: first matching rule picks glyph, colour and label 2026-08-20 08:35:34 +02:00
stroblme 757e3836fe Bar widget: horizontal level with its reading on the fill and a nested bar 2026-08-20 08:35:24 +02:00
stroblmeandClaude Opus 5 4c146e2a38 Add the clock widget: local time and date, bound to nothing
One second-tick interval and the browser's own locale formatting, so there
is no 12/24-hour setting to carry and nothing to bind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
2026-08-20 08:35:21 +02:00
stroblmeandClaude Opus 5 f4f8e77f33 Brain graph: thinner ring, integer border, heavier connections
The ring share drops from the wordmark's 0.231 to 0.18 so the disc reads as a
dot at 32px, and both the border and the gap are rounded where they are applied
— a fractional border snaps per edge and left the disc off centre. Connections
and their port dots go up with it, pulse width included, so an arriving message
never draws its edge thinner than it rests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
2026-08-20 08:34:54 +02:00
stroblmeandClaude Opus 5 21b01e2d25 Bump dashboards: bar, icon, forecast and clock widget types
Plumbing only: the widget-type literal and its dtype table on both sides,
the regenerated client, a curated lucide map and four stubs the renderers
are wired to. Also a latching switch and a segmented dropdown, both a
`style` on the control that already publishes and reads back, plus the
option editor a dropdown never had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
2026-08-20 08:31:36 +02:00
stroblmeandClaude Opus 5 c005ea48e1 Health: count only flows the engine acts on, format durations with their unit
The "running" tile counted paused and invalid flows as running, so it read
"5/5" beside "1 flow(s) cannot run". Its note is now additive rather than a
precedence chain, so a quarantine no longer hides the invalid count.

Adds dur() beside si(): a ms reading picks its own unit, so a slow run reads
"1.24 s" instead of "1.2k ms".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTsT1isxUjw5gtkJk8WhuA
2026-08-20 08:24:57 +02:00
stroblmeandClaude Opus 5 8d539a4ba8 Settings: connect this installation to a portal
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
A superuser-only tab that redeems a claim code and shows the link's state. It
names the account a remote session acts as, because that is the thing being
granted and the person granting it should see it spelled out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XtBzdDyLsmDaF1W7DLYtYM
2026-08-19 17:39:14 +02:00
stroblmeandClaude Opus 5 414cda2798 Dashboard: run unchanged when a portal serves it
The same bundle is served by a portal under /i/{id}, so it reads its API base,
credential and router basepath from an injected config instead of the build-time
URL and localStorage. A normal installation finds no config and behaves exactly
as before; the credential deliberately never touches localStorage, since two
installations open in one browser share an origin and would overwrite each
other's session.

The websocket URL was resolving an absolute path against the base, which
discards the base's own path — harmless until the base gained one, then it
aimed the socket at the wrong host entirely.

Connection state gets a store of its own, apart from the engine's: the proxy's
503 carries {offline, last_seen}, which raises a banner naming when the
installation was last heard from and turns a failed mutation into 'not
delivered' rather than a generic error. The screen keeps its last data
underneath, since stale readings with a timestamp beat a blank page. A
reconnecting socket invalidates every query, because whatever happened while it
was down was missed.

Verified in a browser against a real hub and installation: the full UI loads
through the tunnel with no console errors, and killing the installation raises
the banner within a poll.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XtBzdDyLsmDaF1W7DLYtYM
2026-08-19 17:09:53 +02:00
stroblmeandClaude Fable 5 8feabd33c5 A message shows its shape, and its contents when asked
A record or an artifact reference was serialised into the port row, and the
panel widened until the type selects and the buttons beside them were pushed
off its edge — a checkpoint reference is 130 characters of digest, and none of
them are what you want while wiring a flow. What shows now is what the value
*is*: 'artifact · weights.json · 60B', 'record · 3 fields'. A chevron unfolds
the whole of it, wrapped, inside the panel it belongs to.

A scalar still reads as itself, and scrolls its own overflow into view when it
is longer than the room it was given. That behaviour already existed inside
the edge inspector; it moves to Common/Marquee so the panel can have it too,
and the inspector drops its own copy of the raw-JSON block along with it.

The rows are smaller for it: the type select finally fits the word 'artifact',
and a port nothing has come through on says so with a dash rather than a
sentence — nine ports of 'nothing has come through yet' is a panel of prose
about the absence of values.

The e2e check asserts both halves: that the summary is what appears, and that
the panel is still exactly 400px with the value unfolded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AD8SfVhzXBG2nAfFcVh3iD
2026-08-19 13:20:12 +02:00
stroblmeandClaude Fable 5 52c0c5e2ce An example to evaluate: a training run, its dashboard, and two bugs it found
make seed-demo builds demo_training — prepare on the engine, a GPU-bound
train, evaluate back here — and a panel that draws the loss curve while the
training is still going. It is the session's whole argument in one flow: batch
runs with parameters and a result, a generator yielding on a declared port
rather than logging, fluksio.emit from inside a callback, artifacts carrying
the dataset and the weights between machines, and a sweep whose configs are
isolated from each other. The train node prefers its label rather than
requiring it, so it runs before a GPU box exists and says which machine and
which numeric backend it actually used.

Building it turned up two real bugs. A run waited for a worker its flow only
*preferred*, because required_labels ignored device_policy — so the example
hung on a label it did not need. And a run's seed never reached the flow, so
sweeping over seeds ran the same experiment N times; it now fills an input of
that name when the flow declares one, which is what the field looked like it
did all along.

Pressing Run on a batch flow now submits a run rather than taking the old
non-durable path — that button is the first thing anyone evaluating will press,
and it was quietly doing something else.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AD8SfVhzXBG2nAfFcVh3iD
2026-08-18 22:04:14 +02:00
stroblmeandClaude Fable 5 e18f1f6c5f A node's numbers leave through its ports, not a logging call
The first cut had node code call fluksio.log_metric, which was a second,
undeclared way for data to leave a node: invisible to validation, absent from
the canvas, and stored where the graph could not see it. That is precisely the
MLflow discrepancy this framework exists to avoid, so it is gone.

A node that produces values over time is a generator. Every yield is a dict
keyed by output port, published the instant it happens — same port, same type
check, same place on the canvas as any other value — and what it returns is
its result. A port doing this declares stream: true, and a run keeps every
number one takes, so experiment tracking is a consequence of the graph rather
than an API beside it: a chart binds to a training curve the way it binds to a
temperature. fluksio.emit writes the same ports imperatively, for where a
yield cannot reach — inside a training framework's callback.

In a live flow an emission also wakes what is downstream, as a subscriber
publishing does; in a run it does not, because a run's graph is scheduled once
and mid-node cascades would leave 'finished' with nothing to mean. The
enqueued item carries no payload: the value is already in state, and one
carrying it would re-apply an old emission after the node returned.

Verified on the stack: 30 loss values arrived live on the flow socket during a
run, attributed to the node that produced them, and the same node run on the
remote worker streamed its curve back across the socket.

Also caches remote compile results per worker, so attaching a GPU box does not
put a network round trip in every rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AD8SfVhzXBG2nAfFcVh3iD
2026-08-18 20:53:49 +02:00
stroblmeandClaude Fable 5 e302ba1a43 Remote workers: a GPU box dials in and runs the nodes bound to it
The engine runs where the automations are and the GPU is somewhere else,
usually behind a different network — so the worker connects out and the engine
answers over the socket it was given. Nothing has to expose Redis, and the
same connection works through the tunnel the hosted access will use.

What travels is the protocol the local pool already speaks, so a node cannot
tell which kind of worker it is on. A node declares device: gpu and
device_policy, the label is resolved per call (a worker attaching later needs
no rebuild), and a run whose labels nothing carries waits in the queue saying
what it waits for rather than failing — submit from the couch, the GPU box
picks it up when it is switched on.

Two things had to move with it. Compiling now happens on the machine that will
run the node: a node importing torch is correct on the GPU box and a missing
module on the engine, so checking it here failed nodes that were fine. And the
artifact endpoint accepts a worker's own credential, because storing a
checkpoint is exactly what that credential is for — and only that.

Verified against the real split: the training ran on this host (its checkpoint
names the machine and a numpy the engine does not have), streamed 40 metric
points back mid-run, and the evaluate node read the checkpoint on the engine.
Cancel kills the remote training; pulling the worker fails the run in six
seconds instead of waiting out its ten-minute timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AD8SfVhzXBG2nAfFcVh3iD
2026-08-18 17:52:42 +02:00
stroblmeandClaude Fable 5 b1cb8b41bc Artifacts: bytes a node produced, addressed by their content
A checkpoint is not a message. DType.ARTIFACT carries a reference — digest,
size, media type, name — so everything on the wire stays JSON and thirty
megabytes never sit in Redis, which answers the vision's open binary-payload
question by narrowing it: inline codecs would only serve payloads too small to
be worth a round trip, and nothing asks for that.

The store is content-addressed rather than per-run, for three reasons that all
pay later: a sweep whose fifty configs share one preprocessed input stores it
once, a reference stays valid however it is passed around because it names
content instead of a location, and the digest is what a stage cache will
compare — so building it in now is what keeps that from being a change to the
message contract.

Node code calls fluksio.save_artifact/load_artifact and cannot tell whether it
is writing the engine's own directory or putting bytes over HTTP, which is
what will let the same flow run on a remote worker unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AD8SfVhzXBG2nAfFcVh3iD
2026-08-18 17:09:27 +02:00
Melvin Strobl 76dbedd0f7 link
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
Signed-off-by: Melvin Strobl <lc3267@kit.edu>
2026-08-18 14:22:47 +02:00
stroblme 449a1472cd gc
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
Signed-off-by: stroblme <stroblme@posteo.de>
2026-08-17 20:07:56 +02:00
stroblmeandClaude Opus 5 eb1ee65c3a Cap the long lists, and wrap a wide rank on a phone
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
Two things the first pass got wrong.

The Changes and "Given up on" lists had no cap, so they ran as long as the
audit trail did — and worse, a truncating flex row still offers its whole
unwrapped line as a min-content contribution, so the card sized itself to the
longest entry and took the page sideways with it. Both now use the same
capped, scrolling box the runs and failures lists already had, which fixes the
length and the width together.

Running downwards, a rank of eight nodes was two thousand pixels across
because a node box is landscape: siblings cost a rank four times as much
across as they do down it. A rank wider than two now wraps onto the ranks
below, settling over a few passes since wrapping one rank pushes what was
under it up into the room that freed. The value chip on an edge no longer
reserves its width there either — that width is spent across the screen rather
than along the flow, and the rank gap it would widen is already wider than the
chip is tall. The same flow that laid out 2040x216 is now 803x722; a chain and
a diamond are unchanged, and so is every desktop layout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDSXaRhvqHYNevgDGmNAto
2026-08-17 18:22:15 +02:00
stroblmeandClaude Opus 5 39ee0e0aa5 Computed flow layout, and mobile written into the design
The canvas lays itself out: a layered graph, left to right on a desktop and
top to bottom on a phone, with room reserved for the value each edge carries.
Nodes cannot be dragged and `NodeDef.position` is gone from the document —
a graph nobody can arrange is one worth keeping small, which is what keeps
flows atomic. Endpoints join the same layout, so their lanes and the
localStorage that remembered where they were dragged go too.

Mobile, per the new Responsive section of DESIGN-GUIDELINES.md: the dock caps
its width and wraps instead of running off the screen, the dashboard stacks
into one column rather than shrinking a wall panel to a fifth of its size, and
Home stops widening its grid track past the viewport. A Playwright project at
a phone's width fails the build when a screen no longer fits.

Along the way: publish is the checkmark that was already there rather than a
button that appears and disappears, with discard beside it on both the flow
and the dashboard; the brain reveals a neuron's name on the first tap; and the
port sparklines get room to breathe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDSXaRhvqHYNevgDGmNAto
2026-08-17 17:35:14 +02:00
stroblmeandClaude Opus 5 e7a1466d7b dashboard: pace a querying chart by its window, and an example to evaluate it
A chart is drawn in buckets, and nothing it can show changes until the
bucket it is drawing closes — so the resolution sets the refresh rather
than a flat five-second floor. A week at quarter-hour buckets now asks
four times an hour instead of sixty, for the same picture. Leaving the
field empty follows the window; a slower rate is still honoured.

`make seed-example` builds the thing to evaluate it with: a flow that
logs a temperature to InfluxDB, a flow that answers a chart's request by
turning the window into Flux and the rows back into a series, and a
dashboard holding the chart. The reading flow declares the request as an
input with a starting value, which is how a flow says a value reaches it
from a panel rather than from a node upstream.

Axis labels keep enough decimals to stay distinct — `si` rounds to three
figures, so every tick of a chart living inside one degree read "19".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:34:32 +02:00
stroblmeandClaude Opus 5 19c196b7e6 dashboard: the agenda reads from the top of its card
The frame centres a single reading, which is right for a gauge and wrong
for a list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:14:00 +02:00
stroblmeandClaude Opus 5 59295b1646 Brain: terracotta for fault, a ring set off from the fill, and a dotted ground
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 15:07:30 +02:00
stroblmeandClaude Opus 5 e19776aed1 dashboard: charts that query
A chart can now ask for what it draws instead of reading the ring the
engine keeps. It publishes a request — the window and the resolution —
exactly as a slider publishes a value, and draws the series a flow
answers with. What serves the request is the flow's business, so the
widget never learns which database was behind it.

The answer says what it was computed for and one computed for another
window is ignored, so two charts on one node cost a duplicate query
rather than overwriting each other's picture. Identical requests still
in flight are asked once per tab, and the refresh has a floor under it.

The panel gains the presentation the document could already hold: the
per-series label, a unit, and a y axis that can be pinned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:07:18 +02:00
stroblmeandClaude Opus 5 413501c6ce flow: structured dtypes, and the widgets that read them
A series, record or list message declares its shape instead of riding
DType.JSON, so a widget binds a shape rather than some JSON and a wrong
binding is refused before anything runs. A list declares its item type,
which is what keeps list[float] expressible for a pipeline.

On top of that: an agenda over a list, a notification over a record, and
a dashboard alert channel that publishes engine faults as one — so a
panel can show what went wrong without a flow wiring it by hand.

Also: only None means a node published nothing, a falsy value of the
wrong shape is now the named error it always should have been; and the
gauge's readout says its size is viewBox geometry rather than type scale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:06:45 +02:00
stroblmeandClaude Opus 5 18837e8880 Brain: neurons in the mark's blue, ringed in destructive when they cannot run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 14:50:39 +02:00
stroblmeandClaude Opus 5 a1265450df Health: a flow that cannot run says so, and the brain marks which neurons
A dependency loop is flagged on the canvas and was invisible everywhere else:
/observability/summary answered "ok" with an empty problems list while the
published flow could not run at all. It now reports the flows validation
blocks, and the brain graph carries the reason on each neuron the issue names
so the view built to find broken wiring can show it.

Node errors stay counted once, as the nodes that failed to load, and an
advisory like an unauthenticated webhook marks nothing — it is worth saying,
but the flow still runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 14:39:07 +02:00
stroblmeandClaude Opus 5 3b5241bb9a Overviews: create before publish-all; Home: the trend curves keep their dot
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 Home table drew the one sparkline without its live dot, on the grounds
that a polled rollup's right edge is the last complete slice rather than this
instant. Identical curves reading differently is the worse trade, so the dot
is back everywhere and the cell keeps room for the half that hangs past the
curve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 12:58:35 +02:00
stroblmeandClaude Opus 5 1a589570b1 Brain: dots where a connection meets a neuron, inbound hollow and outbound filled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 12:52:31 +02:00
stroblmeandClaude Opus 5 aabe7bb147 Let the Home capture wait for the sections that load on their own
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
networkidle now falls between Home's independent fetches, so make verify was
photographing the skeletons: no name, no Admin nav, "No flows yet" and zeroed
tiles on an instance with three flows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 12:08:32 +02:00
stroblmeandClaude Opus 5 2ba56f57b2 Publish the dashboard the endpoint test asserts on
A dashboard edit is a draft now, and the flow canvas draws the controls the
panels actually carry, so the fixture has to publish it the same way it
already publishes the flow. The assertion is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 12:03:54 +02:00