Commit Graph
100 Commits
Author SHA1 Message Date
stroblmeandClaude Opus 5 4a2337f4de Drop the unreachable 404 from save_dashboard
Docs / docs (push) Successful in 31s
Playwright Tests / test-playwright (1, 2) (push) Failing after 2m39s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m40s
pre-commit / pre-commit (push) Failing after 2m54s
Playwright Tests / merge-reports (push) Canceled after 0s
Test Backend / test-backend (push) Canceled after 1m10s
Compose Smoke Test / test-compose (push) Canceled after 0s
write_draft creates a first draft for a name that does not exist yet, so
it raises only StaleVersion — the DashboardNotFound arm never ran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Gf7WaExcJ9bs3kfJXB3nK
2026-08-25 21:36:02 +02:00
stroblmeandClaude Opus 5 49e6bc6c4e Stop the SDK generator reformatting the whole frontend
biome.json excludes src/client, so the trailing format pass never
touched a generated file — it only rewrote unrelated sources on every
backend commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Gf7WaExcJ9bs3kfJXB3nK
2026-08-25 21:33:11 +02:00
stroblmeandClaude Opus 5 d3a0b4c3a4 Panel spec keeps the installation's other panels
Saving panels replaces the whole list, so the spec's setup deleted every
panel it had not created — and with it the credential of the screen on
it. It now appends to the stored list and puts the original back in
teardown, which also removes its own panel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Gf7WaExcJ9bs3kfJXB3nK
2026-08-25 21:30:35 +02:00
stroblmeandClaude Opus 5 65f859c04d Land a paired panel on its own screen instead of a new code
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Gf7WaExcJ9bs3kfJXB3nK
2026-08-25 21:24:41 +02:00
stroblmeandClaude Opus 5 a3a234756c A batch flow's input is a run parameter, not a missing value
Docs / docs (push) Successful in 20s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m59s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m40s
pre-commit / pre-commit (push) Failing after 2m49s
Test Backend / test-backend (push) Successful in 2m20s
Compose Smoke Test / test-compose (push) Successful in 30s
Playwright Tests / merge-reports (push) Failing after 1m6s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UytviPMJbXzD8P84nLvXcq
2026-08-25 18:51:54 +02:00
stroblmeandClaude Opus 5 3503512d05 Report how long a run has been going, not just how long it took
Docs / docs (push) Successful in 20s
Playwright Tests / test-playwright (1, 2) (push) Failing after 2m39s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m39s
pre-commit / pre-commit (push) Failing after 2m51s
Playwright Tests / merge-reports (push) Canceled after 0s
Test Backend / test-backend (push) Canceled after 1m4s
Compose Smoke Test / test-compose (push) Canceled after 0s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UytviPMJbXzD8P84nLvXcq
2026-08-25 18:33:28 +02:00
stroblmeandClaude Opus 5 055f4d7d97 Filter the icon grid, and ask before removing a panel
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UytviPMJbXzD8P84nLvXcq
2026-08-25 18:33:28 +02:00
stroblmeandClaude Opus 5 476dc6a1e9 Make the brand CTA visible and fix two more v3 variable classes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UytviPMJbXzD8P84nLvXcq
2026-08-25 18:33:28 +02:00
stroblmeandClaude Opus 5 df22475f54 Claim only what the cascade pool can run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UytviPMJbXzD8P84nLvXcq
2026-08-25 18:33:28 +02:00
stroblmeandClaude Opus 5 68711e2ee7 Start the docs site with the integrated local stack
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UytviPMJbXzD8P84nLvXcq
2026-08-25 18:33:28 +02:00
stroblmeandClaude Opus 5 8679b0bcf7 Draw a wall panel at the size it was drawn for
Docs / docs (push) Successful in 21s
Playwright Tests / test-playwright (1, 2) (push) Failing after 2m41s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m40s
pre-commit / pre-commit (push) Failing after 3m16s
Test Backend / test-backend (push) Successful in 2m18s
Compose Smoke Test / test-compose (push) Successful in 30s
Playwright Tests / merge-reports (push) Canceled after 0s
A dashboard whose canvas matches the screen came out at 95% of it: the
panel and view routes inset the canvas by a further 16px, on top of the
margin the grid already keeps from its own edges. Two margins, one of
them costing the scale. The padding now applies only to the stacked
phone layout, which has no canvas to shrink.

A slider's tick labels are the third row of a control that has three,
and the first thing a short tile runs out of; `ticks: false` drops them
and keeps the reading and the track. The house's two sliders set it.

The 1024x600 preset is named for what it is rather than for a diagonal
it may not have — the same resolution is sold as a seven-inch panel and
as a ten-inch one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012n6CehUsHYYUXJ48rxaD18
2026-08-25 17:30:37 +02:00
stroblme 4350916bd8 Scroll the runs table, pick a range, and choose what a comparison plots against
Docs / docs (push) Successful in 35s
Playwright Tests / test-playwright (1, 2) (push) Failing after 3m14s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m44s
pre-commit / pre-commit (push) Failing after 3m54s
Test Backend / test-backend (push) Successful in 3m12s
Compose Smoke Test / test-compose (push) Successful in 32s
Playwright Tests / merge-reports (push) Failing after 1m28s
2026-08-25 16:32:34 +02:00
stroblmeandClaude Opus 5 3c15964364 A cached node keeps its curve, and any input can name a run's output
Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Failing after 2m42s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m41s
pre-commit / pre-commit (push) Failing after 2m53s
Test Backend / test-backend (push) Successful in 2m21s
Compose Smoke Test / test-compose (push) Successful in 31s
Playwright Tests / merge-reports (push) Failing after 1m6s
A cache hit still replays no emissions — those values were the story of an
execution that is not happening — but the run they were recorded in is now
written on the row (`run_node.cached_from`), and the metrics endpoints read the
series back from there. So a reused run answers `run.metrics("train.loss")`
with the same points the run that trained did, rather than looking like a run
that produced no numbers at all. Pointed at rather than copied: a sweep of 500
reusing one frozen node would otherwise duplicate its curve 500 times.

That needed the cross-flow restore fixed first. The cache key has no flow in
it while the stored outputs are named for the flow that produced them, so
`quick.prepare` getting a hit from `train` wrote `train.dataset` into `quick`'s
state and the next node was called without its argument. One rule now covers
both halves: `requalify` reads a name owned by one flow as the same name in
another, applied to the restored outputs, to the node id behind the pointer,
and to the series names on the way out. Reuse across flows is kept.

Also: `@run:<id>.<output>` and a bare `sha256:` digest resolve on every input,
not only artifacts. Chaining a run's json config into the next one from a shell
meant pasting the whole object inline, and the CLI could not even send the
spelling — `_coerce` died in `json.loads` before the engine saw it. Both
spellings are reserved on every input now, `str` included, and `_from_run`
returns whatever the run's result holds rather than only a reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dp9L6gakMVro1K2C5zdtBE
2026-08-25 15:12:28 +02:00
stroblme aa2bd1e665 Release v0.1.2
Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Failing after 2m11s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m46s
pre-commit / pre-commit (push) Failing after 2m52s
Test Backend / test-backend (push) Successful in 2m21s
Compose Smoke Test / test-compose (push) Successful in 33s
Publish / publish (push) Successful in 49s
Playwright Tests / merge-reports (push) Failing after 1m11s
2026-08-25 12:54:41 +02:00
stroblme 03e12b7647 Document the runs screen, the run context and the generated dashboard
Docs / docs (push) Successful in 25s
Playwright Tests / test-playwright (1, 2) (push) Failing after 2m47s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m42s
Playwright Tests / merge-reports (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 1m45s
Test Backend / test-backend (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
2026-08-25 12:44:19 +02:00
stroblme 7ff29ca939 Make a dashboard its widgets: drop the pages and sections nobody drew 2026-08-25 12:30:22 +02:00
stroblme 2840cc8e2b Photograph the runs screen and a dashboard read against runs 2026-08-25 12:13:58 +02:00
stroblme 77754936c6 Pin a chart to the last few runs of a flow 2026-08-25 11:52:34 +02:00
stroblme 5ece544b14 Let a dashboard be read against runs instead of the live engine 2026-08-25 11:48:15 +02:00
stroblme d2951a325e Add a runs screen: a table, a run in full, and curves side by side 2026-08-25 11:44:13 +02:00
stroblme 7e422c0047 Count runs per flow, and page the run list by offset 2026-08-25 11:34:07 +02:00
stroblmeandClaude Opus 5 60757fa7fa Style the engine's own logs, notice enrolment while serving, say more in status
Docs / docs (push) Successful in 21s
Playwright Tests / test-playwright (1, 2) (push) Failing after 4m24s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m37s
pre-commit / pre-commit (push) Failing after 3m14s
Test Backend / test-backend (push) Successful in 2m15s
Compose Smoke Test / test-compose (push) Successful in 34s
Playwright Tests / merge-reports (push) Failing after 1m3s
Four things from a testing pass.

`fluksio serve` printed its own lines through the root logger, which has no
handler and falls back to `INFO:fluksio.cloud.connector:...` — beside uvicorn's
aligned output it reads like something went wrong. The engine's loggers and
alembic's now use uvicorn's own handler. Named rather than configuring the
root: httpx logs every portal call at INFO and none of that is printed today.

`fluksio enroll` writes its config from another process, so an engine already
serving never learned it had been paired. It now looks for one every few
seconds and dials when it appears. `load()` rather than `exists()`, or a file
that does not parse would be restarted forever.

`fluksio status` says where the installation stands with its portal — never
paired, linked, or paired and unreachable, which is the one worth acting on.

`--seed` and `--timeout` had no help text at all. Both say what they are for
now, and the docs say what a seed is actually for: recorded on the run, part of
its input digest, and passed to an input named `seed` when the flow declares
one, so the number a run is labelled with is the one the code drew from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019V5bsYGNxcgPs4xXmTPx69
2026-08-25 09:29:22 +02:00
stroblmeandClaude Opus 5 7c5b212f43 Let fluksio status read a local installation
`runs --local` already exists for the reason status needs it: the flows and the
history are in this directory, and looking at them should not need a server.
Watching still does — an in-process engine is the command itself, so nothing
could change under it — and says so rather than sitting on a still frame.

Found setting up a sandbox that drives the SDK entirely through --local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019V5bsYGNxcgPs4xXmTPx69
2026-08-25 08:21:27 +02:00
stroblmeandClaude Opus 5 4941c2787c Add fluksio status, and ask for a run's parameters at a terminal
Two halves of the same gap: the CLI could start work but not show you any.

`fluksio status` draws the home screen's top half in a terminal — health and
what is wrong with it, every flow with its state and node count, and the
recent runs and failures under them. `--watch` keeps it there. Rich does the
drawing; it was already installed under fastapi's own CLI, and is named now
because a command depends on it.

`fluksio run` with no parameters at a terminal asks for them, one line per
declared input with its declared value in brackets — so Enter through the lot
is what running the defaults looks like, and an artifact input takes the
`@run:` spelling the engine now resolves. A scripted run is untouched: passing
any parameter, or piping the command, skips the questions, as does --defaults.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019V5bsYGNxcgPs4xXmTPx69
2026-08-25 07:42:09 +02:00
stroblmeandClaude Opus 5 93374a310e Refuse what a node cannot publish, and stop timing out work that is fine
Four things the python SDK turned up, each fixed where every client sees it.

A key no port declares is now an error rather than a silent drop, on the
return, the yield and the emit alike — the contract the docs already stated.
The SDK reads literal yields at sync time, so a typo fails before anything
runs, and an emission of one fails the call rather than being logged where
nobody looks.

NaN and infinity are refused at the port. JSON cannot spell either, so one
that travelled came back as a 500, a socket frame that stopped the canvas, or
a metric batch the database dropped whole.

An artifact input takes `@run:<id>.<output>` or a bare digest, resolved on the
engine — so the CLI, the run dialog and a python caller mean the same thing,
and a sweep can pass one at all.

Node timeouts are off by default. The clock measured silence, which a training
node is full of, and remote workers had already stopped enforcing it — their
heartbeat reset it. Now a heartbeat proves the agent rather than the node,
ninety seconds of nothing fails the call either way, and the engine touches
work it is still running so a long node is not redelivered at sixty seconds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019V5bsYGNxcgPs4xXmTPx69
2026-08-25 07:30:14 +02:00
stroblmeandClaude Opus 5 c33fa404a4 Keep the dev bind mounts out of CI, where they land nowhere
Docs / docs (push) Successful in 21s
Playwright Tests / test-playwright (1, 2) (push) Failing after 2m4s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m36s
pre-commit / pre-commit (push) Successful in 3m0s
Test Backend / test-backend (push) Successful in 2m6s
Compose Smoke Test / test-compose (push) Successful in 30s
Playwright Tests / merge-reports (push) Failing after 1m2s
compose.dev.yml mounts the source over the image's own copy so `--reload`
means something in a detached local stack, and mounts the Playwright report
out to the host. Under act_runner the daemon is a sibling of the job rather
than its host: it resolves those paths against its own filesystem, finds
nothing, and mounts empty directories. So the backend reported "Path does not
exist fluksio/main.py" for a file the image plainly ships, and every shard
uploaded nothing — "No files were found with the provided path:
frontend/blob-report".

compose.ci.yml now puts backend's volumes back to what compose.yml declares
and drops Playwright's entirely; the shard copies its report out of the
container before the teardown instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 21:42:04 +02:00
stroblme bf6cad45ad Release v0.1.1
Docs / docs (push) Successful in 20s
Playwright Tests / test-playwright (1, 2) (push) Failing after 33s
Playwright Tests / test-playwright (2, 2) (push) Failing after 19s
pre-commit / pre-commit (push) Successful in 2m47s
Test Backend / test-backend (push) Successful in 2m7s
Compose Smoke Test / test-compose (push) Failing after 20s
Publish / publish (push) Successful in 48s
Playwright Tests / merge-reports (push) Failing after 1m2s
2026-08-24 21:33:18 +02:00
stroblmeandClaude Opus 5 a96c3d44ea Newline at the end of the two files the hook keeps catching
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 21:33:18 +02:00
stroblmeandClaude Opus 5 400d7d9c5c Stage caching for batch runs, and an engine that lives in the command
Docs / docs (push) Successful in 19s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m5s
Playwright Tests / test-playwright (2, 2) (push) Failing after 20s
pre-commit / pre-commit (push) Failing after 2m33s
Test Backend / test-backend (push) Successful in 2m7s
Compose Smoke Test / test-compose (push) Failing after 20s
Playwright Tests / merge-reports (push) Failing after 1m3s
Publish / publish (push) Failing after 12s
A code node in a batch run is now fingerprinted by its source, its raw
settings and the values it reads — an artifact input counting as its digest,
which is what the content addressing was always for. A run that finds the key
restores what the earlier one returned and skips the node, recorded as
`cached`. The run history is the cache: `run_node.outputs` beside the
`cache_key` the schema already had, no second store. On for code nodes, never
for the built-in and connector types that have side effects; off per node with
`@node(cache=False)` and per run with `--no-cache`.

Emissions are not replayed on a hit, so a cached training node returns its
result without redrawing its curve. Recorded in NOTEPAD.md with the two other
deliberate limits.

`fluksio run --local` boots the real app in the command's own process and
drives it through its ASGI interface behind the ordinary client, so a run no
longer needs a `serve` terminal beside it — same data directory, same history,
and the cache carries between the two. It always waits, because the engine it
starts lives exactly as long as the command.

Also: `fluksio sweep --param lr=0.1,0.01` for the product of the lists,
`run --follow` for a run's numbers as they arrive, Ctrl-C cancelling a waited
run rather than abandoning it, coloured statuses on a terminal, and `name`
made optional on the metrics endpoint so a follower can ask for every series.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 20:31:31 +02:00
stroblme 7a9502883a gc
Docs / docs (push) Successful in 20s
Playwright Tests / test-playwright (1, 2) (push) Failing after 24s
Playwright Tests / test-playwright (2, 2) (push) Failing after 22s
pre-commit / pre-commit (push) Failing after 2m5s
Test Backend / test-backend (push) Successful in 2m6s
Compose Smoke Test / test-compose (push) Failing after 20s
Playwright Tests / merge-reports (push) Failing after 1m1s
Signed-off-by: stroblme <stroblme@posteo.de>
2026-08-24 19:26:43 +02:00
stroblme 00a91c19b8 Move NOTEPAD.md and ROADMAP.md to the workspace root
Consolidates deferred-work tracking now that app is going public, so no
strategy/planning content ships with it.
2026-08-24 19:16:39 +02:00
stroblme 0926dabb99 gc
Signed-off-by: stroblme <stroblme@posteo.de>
2026-08-24 19:06:54 +02:00
stroblmeandClaude Fable 5 47e513b658 Bring the README back in step with the commands it gives
Four things had drifted. It told you to run `fluksio login` two lines above
saying `serve` signs you in — one of the two had to go, and it is the login.
It ran `fluksio sync` before `fluksio run`, which `run` now does itself. The
pipeline snippet passed `prepare` and `evaluate` to `Flow` without importing
them, so copying it got a NameError on the one example that matters. And the
site link was relative, which resolves to nothing on the page this file exists
to be — pypi.org.

Added one sentence, on installing into the environment you already work in,
because it is the reason a reader's own imports keep working and there is
nowhere else on this page they would learn it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ue1tkFWB1bcGy3aWhCKpU
2026-08-24 18:31:51 +02:00
stroblmeandClaude Fable 5 7e4f03369b Three fewer things to remember
**The portal link puts itself back up.** It already retried a connection that
raised, but a session that ended *cleanly* — a portal restarting, a proxy
closing an idle socket — returned normally and went straight back round the
loop with no wait at all, so an engine could spin against a portal that was
merely saying goodbye politely. Every ending now reconnects on a delay, and
the delay turns on whether the attempt got as far as attaching: one that stood
up and dropped is a network event and retries at once, one that never stood up
waits longer each time. Jittered, so a portal coming back is not met by every
installation it serves in the same instant. Ping timeouts are named rather
than defaulted, since they are what bounds how long a suspended laptop's dead
socket looks alive, and the keepalive task is awaited so the reason a link
went reaches the log instead of the garbage collector.

**`fluksio enroll <code>`** is the whole command now; hub.fluksio.com is the
default and `--portal` names another. The one command run before anything
works should not need two flags.

**`fluksio run` syncs first.** The reason a run exists is usually the edit
before it, so remembering to sync was remembering to do something the computer
could do — including the worker refresh, which is what makes an edit to your
own package take effect at all. `--no-sync` opts out for a tight loop.

That last one needed discovery fixed: it only ever looked at top-level `*.py`,
so a repository whose code is in a package — the ordinary shape — found
nothing from its own root. It now descends into the packages it holds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ue1tkFWB1bcGy3aWhCKpU
2026-08-24 18:17:16 +02:00
stroblmeandClaude Fable 5 99f6530698 One installation per project, and no login to reach it
Two things a local install should not have asked for.

`fluksio serve` now signs you in. Logging in to your own machine was a
formality — the password was printed by the same process that would have
checked it, and the database it authenticates against sits in the directory
the token goes into — so `serve` mints the token itself and says where it put
it. `fluksio login` is left for an engine somewhere else.

And an installation is `.fluksio` beside the code, found the way `.git` is,
rather than one `~/.fluksio` for the machine. A repository with its own venv
was already getting its own engine; it now gets its own flows, run history and
token too, instead of three repositories sharing one database and fighting
over one port. `--global` asks for the shared one, `--data-dir` still names
any directory, and when both exist the banner says which you are looking at
and how to reach the other.

The directory ignores itself from within — a `.gitignore` of `*`, the way uv
writes one into `.venv` — because it holds a credential and a database, and
neither belongs in anybody's history. The token is written mode 600. A login
an older version wrote to ~/.config/fluksio is still read, so nothing that
worked stops working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ue1tkFWB1bcGy3aWhCKpU
2026-08-24 16:13:35 +02:00
stroblme e2b25c9d5a wording
Docs / docs (push) Successful in 49s
Playwright Tests / test-playwright (1, 2) (push) Failing after 29s
Playwright Tests / test-playwright (2, 2) (push) Failing after 22s
pre-commit / pre-commit (push) Successful in 3m3s
Test Backend / test-backend (push) Successful in 2m19s
Compose Smoke Test / test-compose (push) Failing after 24s
Playwright Tests / merge-reports (push) Failing after 1m5s
Signed-off-by: stroblme <stroblme@posteo.de>
2026-08-24 15:12:00 +02:00
stroblmeandClaude Opus 5 d4a9406c51 Fix the CI gates: Python 3.13, concurrency groups, hook violations
Docs / docs (push) Successful in 49s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m11s
Playwright Tests / test-playwright (2, 2) (push) Failing after 23s
pre-commit / pre-commit (push) Successful in 3m2s
Test Backend / test-backend (push) Successful in 2m22s
Compose Smoke Test / test-compose (push) Failing after 22s
Playwright Tests / merge-reports (push) Canceled after 1s
The gates have never gone green on the new runners. Three separate reasons:

- backend/Dockerfile shipped Python 3.10 while the code imports typing.Self
  and datetime.UTC, so the container exited on import and the suite could not
  even load its conftest. The image moves to 3.13 and the packages declare
  >=3.12, which is the floor the tests actually pass on; ruff's target follows
  and rewrites timezone.utc and asyncio.TimeoutError accordingly. Relocking
  drops the 3.10 branch, which bumps FastAPI and so regenerates the SDK.
- frontend/README.md had no trailing newline and two dashboard widgets used
  arbitrary text-[…] sizes. Both are em-relative on purpose, so they move to
  the inline style the neighbouring ramp already uses.
- Every commit left its own run queued: without a concurrency group a runner
  that was offline for a while works through a backlog nobody reads. A stack
  that fails to come up now prints its logs before the teardown removes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 14:55:59 +02:00
stroblmeandClaude Opus 5 c34585cd72 Retire a worker whose pipe broke rather than hand it on
poll() still calls a child that has exited but not been reaped alive, so a
node that ended itself with os._exit left its corpse on the idle queue and
the next call died on a broken pipe. Mark it the way the out-of-step paths
already do, and _release replaces it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 14:55:59 +02:00
stroblmeandClaude Opus 5 a4fddb200a Ship the files pyproject.toml points at
Docs / docs (push) Successful in 53s
Playwright Tests / test-playwright (1, 2) (push) Failing after 24s
Playwright Tests / test-playwright (2, 2) (push) Failing after 2m40s
pre-commit / pre-commit (push) Failing after 2m8s
Test Backend / test-backend (push) Failing after 48s
Compose Smoke Test / test-compose (push) Failing after 24s
Playwright Tests / merge-reports (push) Failing after 13s
readme and license-files name README.md and LICENSE, but the image only ever
received pyproject.toml, so hatchling failed metadata validation the moment
the layer cache stopped hiding it:

  OSError: Readme file does not exist: README.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 14:47:44 +02:00
stroblme 49bc8994f3 Add the Fluksio look, and make it the one a dashboard starts in
Docs / docs (push) Successful in 48s
Playwright Tests / test-playwright (1, 2) (push) Failing after 16s
Playwright Tests / test-playwright (2, 2) (push) Failing after 13s
pre-commit / pre-commit (push) Failing after 2m8s
Test Backend / test-backend (push) Failing after 49s
Compose Smoke Test / test-compose (push) Failing after 26s
Playwright Tests / merge-reports (push) Failing after 13s
Two looks were somebody else's language spoken well, and neither was the
product's. A dashboard nobody has dressed yet should look like the rest
of the app, so there is now a third set that follows the root
DESIGN-GUIDELINES.md to the letter — `--card` surfaces told from the page
by a hairline and a low shadow rather than by colour, every control a
pill, 16px panels, frosted floating chrome, one slate-blue accent spent
on what a person can act on — and it is what `look` means when nothing
says otherwise.

That also turns the exemption the other way round. The dashboard is still
allowed to look unlike the product; it just no longer does so by default.
An existing dashboard, which has never named a look, lands on the design
it had before any of this.

Restraint is the style rather than an omission here: no ripple, no glow,
no lift, and a press answered by the colour changing. The one deliberate
departure is the selector, which holds its choice in `--primary` rather
than the `--accent` the segmented rule asks for — that is a decision
about the widget, not about the look, and a control must not change what
it signals when the drawing changes. All three sets hold it the same way.
2026-08-24 14:25:13 +02:00
stroblmeandClaude Fable 5 36575c5855 Say the licence on the two pages PyPI will show
Publish / publish (push) Successful in 55s
Docs / docs (push) Successful in 53s
Playwright Tests / test-playwright (1, 2) (push) Failing after 39s
Playwright Tests / test-playwright (2, 2) (push) Failing after 13s
pre-commit / pre-commit (push) Failing after 2m24s
Test Backend / test-backend (push) Failing after 49s
Compose Smoke Test / test-compose (push) Failing after 14s
Playwright Tests / merge-reports (push) Failing after 22s
Every other README carries the AGPL line; these two did not, and they are the
ones that become pypi.org/project/fluksio and /fluksio-worker — the most
public surface the project has, and the place a copyleft licence is least
useful to have to go looking for. §13 is named outright for the same reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ue1tkFWB1bcGy3aWhCKpU
2026-08-24 12:51:53 +02:00
stroblmeandClaude Opus 5 9746c6ce95 Move the seed scripts out to the private workspace root
Docs / docs (push) Successful in 50s
Playwright Tests / test-playwright (1, 2) (push) Failing after 45s
Playwright Tests / test-playwright (2, 2) (push) Failing after 17s
pre-commit / pre-commit (push) Failing after 2m35s
Test Backend / test-backend (push) Failing after 50s
Compose Smoke Test / test-compose (push) Failing after 14s
Playwright Tests / merge-reports (push) Failing after 1m4s
Several of them describe this installation specifically — its broker, its
aircon, its DMX wiring — and this repository is about to be public. The
test and client-generation scripts stay; CI depends on scripts/test.sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 12:27:30 +02:00
stroblmeandClaude Opus 5 bd2c4b66e9 Publish the connector contract and authoring guide
Adds reference/connector-contract.md and code/connectors.md to the site, with
the node-type reference finally linking the contract it describes. Strict
build passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 12:13:40 +02:00
stroblmeandClaude Opus 5 607f3a814e Point at connectors/skeleton after the skeleton moved
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 12:01:36 +02:00
stroblmeandClaude Opus 5 6000258f7c License the workspace under AGPL-3.0-or-later
Replace the empty LICENSE placeholders with the verbatim GNU AGPL v3 text,
fix the invalid "AGPLv3" SPDX string in the package metadata, and name
Melvin Strobl as the copyright holder wherever the old footers said
"Fluksio ... all rights reserved".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 11:53:54 +02:00
stroblmeandClaude Fable 5 11e60cb6a8 Make both distributions fit to publish
The release workflow was already right; what it would have uploaded was not.
`fluksio` had no readme, so its PyPI page would have been blank — the app
repo's own README is a contributor's map of `frontend/` and `docker/`, which
is the wrong front page for `pip install fluksio`. It now has one of its own,
aimed at somebody who landed on the project page. Both distributions gain
authors, urls, keywords and classifiers; `twine check` passes clean on all
four artifacts where it warned on two before.

The workflow publishes `fluksio-worker` first, because `fluksio` depends on it
and the other order leaves a few seconds — the whole of a first release — in
which the dependency cannot be resolved. `--check-url` makes a re-run skip
what is already uploaded rather than failing on it, which matters because a
version on PyPI can never be replaced.

Licence metadata is deliberately still absent: LICENSE is MIT in somebody
else's name, inherited from the template this was scaffolded from, and whose
it should be is not a decision to make in a commit. NOTEPAD carries it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ue1tkFWB1bcGy3aWhCKpU
2026-08-24 11:21:33 +02:00
stroblme 579eb8e61a Size the rail to what it carries
Stretched end to end, a panel with two dashboards showed a pill nine
tenths empty — which was tolerable while the rail was chrome outside the
canvas and obvious once it moved onto the panel itself. It now hugs its
entries and sits centred in the column it reserves; that column is the
same width either way, so no arrangement moves.

A rail longer than the panel is tall still scrolls, and still draws no
bar: a wall panel is swiped, and there is no room for one.
2026-08-24 11:20:32 +02:00
stroblme 6238728dce Draw the panel as a panel
Five things a wall panel showed that a wall panel should not.

**A tile's body no longer clips.** It scrolled, and a box that scrolls
also cuts whatever crosses its edge — which took the glow off a lit
button at exactly the width where the button filled its tile, and off a
gauge's arc at exactly the height where the dial filled its own. Only
what is written or listed asks for a scroller now; everything else is a
picture drawn to fit, and what overflows is left to the frame, which
clips at the tile's edge where a shadow has already faded out. The
slider's phantom scrollbar goes with it.

**The selector is a selector.** Named for what it does rather than what
it is, and the choice it is holding is held in the dashboard's own
primary — a pill that slides between the options rather than a grey one
that had to be looked for. The stored type is untouched, so no document
changes meaning.

**The arrangement is held off the panel's edges**, by the same distance
it holds between two widgets. The ground is not held off with it: a
background covers the whole panel, and only what is arranged on it has
a margin. No stored panel loses a row to it.

**The rail is drawn on the panel.** It was chrome bolted to the edge of
the screen beside the canvas — in the app's own design rather than the
dashboard's, and on a scaled canvas not even lined up with it. It now
takes a column out of the canvas the way the margin does, scaled with
it and wearing its look. Which cell each widget sits in is unchanged;
only how big a cell is.

Two of these were the same mistake twice: an unlayered rule stating
`position` for everything wearing a class — `.gl-surface` on a rail
placed by a utility, and a blanket lift over every child of a pressable
on a pill placed by `layoutId`. Both now say it one element at a time.
2026-08-24 10:44:53 +02:00
stroblmeandClaude Fable 5 fea57064f9 Run node code on the venv Fluksio was installed into
The workflow this serves: make a venv, install what you work with, then `pip
install fluksio` into the same one. Building a second environment beside it
was exactly wrong — the packages the nodes need are already here, and the
Modules screen was asking for them a second time.

`NODE_VENV=auto` (the default) adopts that venv. It declines in the three
cases where adopting would be wrong: `managed` says otherwise, a managed venv
already exists and may hold packages somebody installed on purpose, or the
engine is not running from a venv at all. The images set `managed`, since the
venv in them holds the app and nothing of anybody else's.

An adopted venv is never written to. `uv pip sync` makes a venv hold exactly
the manifest, so pointed at somebody's own environment it uninstalls their
work and the engine with it — `sync()` refuses outright and `reconcile()`
returns before it can be called at startup, which is where that would have
happened first. The Modules screen lists what is installed and drops its
editor; `pip` is how that environment changes.

`fluksio serve` now names the interpreter node code runs on, which is the
thing a data scientist most needs to know at that moment. `fluksio-worker`
already defaulted `--python` to its own interpreter, so a GPU box works the
same way — that was only ever undocumented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ue1tkFWB1bcGy3aWhCKpU
2026-08-24 10:35:13 +02:00
stroblme 68fa5527b1 Answer five things the panel got wrong
- A tile no longer lifts under the pointer. A finger does not move away
  afterwards the way a cursor does, so whatever hover raised stayed
  raised until something else was touched: a tile stuck, not answering.
- The ground's blobs wander a closed path on their own clock instead of
  sliding back and forth along one line, which read as things moving
  rather than as light in a room.
- A bar is one grid now, its rows borrowing its columns, so names of
  different lengths no longer start and end their tracks in different
  places — two bars that share no baseline cannot be compared, which is
  the one thing a stack of them is for. The names read rightward into
  their tracks, with room either side.
- A reading on its way somewhere is written to as many decimals as the
  value it is heading for. Without that a slider stepping in halves
  passed through 22.37460937 on its way to 24: a number nobody asked
  for, a different width every frame.
- The brightness column is the same control as the slider widget's,
  stood on its end and thicker, and exactly as tall as the disc beside
  it. Getting there meant drawing a slider's rail, fill and handle
  rather than styling `::-webkit-slider-*`: those need one set of rules
  per orientation, each with its own centring quirk, and the handle
  landed off its track when the writing mode turned. The native input
  stays, laid transparent over the top, so the keyboard, the pointer and
  every `aria-` are still its.
2026-08-23 23:28:47 +02:00
stroblme 6ccc6e9e26 Draw the colour disc inside its tile rather than past it
The disc was sized against its parents in percentages, and every box
between it and the tile is sized by what is in it — so the chain never
resolved, the brightness slider grew, and the tile scrolled with half a
disc showing. Container units are the tile's own height whatever sits in
between, so the widget now fits exactly.

Two guards behind it, since both are things a picture can fail silently:
nothing on a panel of pictures may overflow its tile, and Touch has to
actually make a control bigger while leaving what it publishes alone.
2026-08-23 22:07:21 +02:00
stroblme 0b5ce4fcbb Rework the dashboard into two looks over one behaviour
A dashboard is a wall panel somebody hangs in their own hallway, so it
now wears what they choose: a look, and a palette of their own colours.

Two complete component sets live under `Dashboard/ui/` — `glass`
(translucent panes over a slowly moving ground) and `material` (Material
3 tonal cards) — behind one prop contract. Every control's state,
keyboard and `aria-` live in `ui/core` and are shared, so the two sets
are the same dashboard drawn twice rather than two products: a set only
decides what a control looks like while doing it.

Four settings join the channel, each drivable by a flow like any other:
`look`, `palette`, `background` and `touch`. A palette is an ordered list
of hex colours — background, surface, primary, accent, text, then more
chart colours — pasted from a coolors.co link or typed, written onto the
canvas as the token variables everything already reads. Trailing roles
are derived, so three colours are a whole dashboard, and derived text is
held to AA rather than trusted (`theme.check.ts` measures it). A palette
also decides light or dark, since its first colour is the ground.

Widgets are measured against their own tile with container queries rather
than against the viewport, animate through `motion`, and can be drawn
without their title. The three reworks:

- a bar draws a row per reading, up to eight, each in the dashboard's own
  data colours and each able to carry its own scale — replacing readings
  nested in one fill, which could only ever share one colour and stop at
  three. Documents written the old way are read as rows.
- a chart's range picker moved to a column down its right-hand edge, which
  gives the plot back a whole row of a short tile.
- the colour wheel became a disc: hue is the angle and saturation the
  distance from the middle, so a colour is one gesture rather than three,
  with brightness on a slider beside it.

`index.css` and `lib/motion.ts` are untouched — the dashboard overrides
token *values* on its canvas, never the blocks the two repos share.
2026-08-23 21:52:14 +02:00
stroblmeandClaude Fable 5 d4c5af4d5a Note the orphaned Postgres app database
Nothing reads it since the engine moved its state to SQLite, but it still
carries the pre-cutover history and an alembic stamp naming a revision that
commit deleted — which reads like a broken migration chain and is not one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ue1tkFWB1bcGy3aWhCKpU
2026-08-23 21:03:05 +02:00
stroblmeandClaude Fable 5 a38e2745eb Add a Python SDK: flows declared in your own repository
A data scientist keeps their code where it is and decorates it: `@node`
declares a function's ports beside the function, `Flow(name, nodes=[...])`
says which of them make a flow, and `use(fn, wire=..., **settings)` rebinds
one for a single flow. `fluksio sync` uploads the document plus a generated
import shim per node, so the store still holds a complete, runnable,
git-versioned definition while the code it imports stays theirs.

`fluksio login|run|runs` and `flow.submit().wait()` are the client half, over
the run endpoints that already existed. Runs record the user repository's
commit beside the store's, so "what code produced this number" is answerable
on the side that now holds the code.

- `fluksio/sdk/`: ports, decorators, the flow builder and its checks, the shim
  generator, an HTTP client and sync. Standard library only at import, so
  `from fluksio import node` in a training script pulls in no engine.
- `FlowDef.origin` marks a flow code-defined; `Run.origin_commit` carries the
  repository's commit; `POST /modules/refresh` retires the workers without an
  install, which every sync calls — a worker holds the imported package in
  memory, so an edit to it is invisible until the process goes.
- The canvas shows a generated body read-only and names the repository to edit
  instead; a body edited there stops the next sync rather than being discarded.
- The worker's reporter carries inert `Port`, `node`, `use` and `Flow`, since
  the shim imports a module whose first line declares them.
- `examples/myresearch` is the worked example, `make sync-example` uploads it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ue1tkFWB1bcGy3aWhCKpU
2026-08-23 20:16:08 +02:00
stroblmeandClaude Opus 5 775d151307 Mount the worker source in dev so it cannot fall behind the engine
The engine and the worker speak a version-matched protocol, but compose.dev.yml
mounted only backend/fluksio: a reloaded engine talked to the worker baked into
the image. Adding the request id to that protocol therefore left every python
node failing with "the worker answered request None", because the old worker
echoed no id at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
2026-08-23 19:15:06 +02:00
stroblmeandClaude Opus 5 95be9bc092 Pin that a per-flow publish deploys the source it just published
The worst thing moving publish off the whole-pipeline rebuild could do
is quietly keep running the old code, and nothing was watching for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
2026-08-23 19:09:02 +02:00
stroblmeandClaude Opus 5 32bc0a5e66 Say a rebuild has two scopes, and close the notepad items it fixes
The module docstrings and the notepad still described one rebuild that
touches everything. Closes the toggle cost, the seeding cost, the
per-save rebuild, the modules/apply rebuild and the Playwright spec that
could not fit a rebuild into its five seconds; files the follow-ups the
refactor leaves behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
2026-08-23 19:05:23 +02:00
stroblmeandClaude Opus 5 56396732a4 Stop and start a flow without rebuilding anything
A stopped flow's nodes are built like any other flow's — being stopped
means having no subscriptions, schedules or webhooks, not being absent —
so a toggle only ever needed the lifecycle call and the gate that goes
with it. It was doing a whole-pipeline rebuild instead, which on a
populated installation is every node in every flow reconnecting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
2026-08-23 18:35:28 +02:00
stroblmeandClaude Opus 5 c95d0ca8f5 Publish, delete and module installs rebuild only what changed
Publishing or deleting a flow now splices that one flow into the running
graph instead of reconnecting every node in the installation, saving a
shared node's source rebuilds the flows using it, and installing modules
rebuilds only the flows holding a node that would not load.

Renaming stays on the full rebuild — it rewrites message references in
every other flow's document — and so does startup, which has no graph to
splice into.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
2026-08-23 18:32:51 +02:00
stroblmeandClaude Opus 5 a4a9f2adff Give the controller a per-flow rebuild, not yet called by anything
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
2026-08-23 17:51:18 +02:00
stroblmeandClaude Opus 5 9cf4c4e714 Let teardown and activate be asked for one flow
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
2026-08-23 17:44:52 +02:00
stroblmeandClaude Opus 5 3bb23fbd3e Let the supervisor stop one flow's tasks and clear its quarantine
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
2026-08-23 17:43:15 +02:00
stroblmeandClaude Opus 5 294b72d989 Let a pipeline swap one flow's nodes instead of being rebuilt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
2026-08-23 17:41:14 +02:00
stroblmeandClaude Opus 5 084194f77b Warm a worker before the node's clock starts, and refuse to delete a live flow
A node's timeout now covers its body only: the pool loads the source into the
worker it picked, off the node's budget, so imports that outlast the timeout no
longer make a node impossible to run. Draft checks compile without caching, so
saving does not evict what a busy node is serving calls from. Requests carry an
id the worker echoes and the pool checks, a reply is encoded once, and the
remote-exception cache is bounded.

DELETE /flows/{name} answers 409 while the flow has a running or queued run,
which is what was letting run_node rows outlive their run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
2026-08-23 17:11:55 +02:00
stroblmeandClaude Opus 5 ce465e7b0e Stop the flow store leaking git zombies, honour VITE_API_URL from .env
`FlowStore._git` now passes `-c gc.auto=0`, so `git commit` no longer forks a
background `gc --auto` that reparents onto PID 1 and stays there unreaped. With
auto-gc off nothing packs on its own, so `_commit` runs a foreground `git gc`
every 500 commits — the trigger a long-lived seeding session actually reaches.

`docker/compose.yml` takes the frontend's `VITE_API_URL` build arg from the
environment, keeping `https://api.${DOMAIN}` only as the fallback. `setup.sh`
already derives the scheme from `ENVIRONMENT`, so an `up --build` that does not
layer `compose.local.yml` stops shipping a bundle that calls `https://api.localhost`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
2026-08-23 16:49:46 +02:00
stroblmeandClaude Opus 5 c81d6cb21a Bound the pipeline teardown so a stuck node cannot wedge the controller
A node's stop() and a supervised task's cancellation are both waited on
inside the rebuild lock, and neither had a deadline: an MQTT client whose
broker never acknowledges the disconnect leaves aiomqtt's __aexit__
waiting forever, so reload() never returned and every start, stop or
publish behind it hung until the container was restarted.

Each node now gets five seconds to close and is abandoned after that, and
cancel_all reports what is still running rather than waiting on it — it
also no longer swallows a cancellation aimed at the caller, which used to
make the lock holder unkillable. A rebuild asked for by a request gives up
on the lock after fifteen seconds with RebuildBusy, answered as a 503.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
2026-08-23 16:36:48 +02:00
stroblme e9e13371fb Merge branch 'main' of git.stroblme.de:Fluksio/app 2026-08-23 15:35:17 +02:00
stroblme e906d57de0 up
Signed-off-by: stroblme <stroblme@posteo.de>
2026-08-23 15:35:15 +02:00
stroblme 14f90393ad Merge branch 'main' of git.stroblme.de:Fluksio/app 2026-08-23 15:34:29 +02:00
stroblme 63ac0261bb Notepad: a flow toggle rebuilds the whole pipeline
Timed at 8-10s on the tinyhouse installation, which is why the runtime
spec's 5s assertion fails there and passes on a small instance.
2026-08-23 15:05:11 +02:00
stroblmeandClaude Opus 5 e912069938 Correct the chart pointer at the event boundary, not in cursor.move
On a CSS-scaled dashboard panel uPlot reads the pointer in visual pixels
and measures it against its own unscaled plot width. Refining the result
in cursor.move left uPlot's earlier arithmetic wrong: cacheMouse snaps an
offset within 1px of the plot edge to the plot width, and on a panel
scaled up the raw visual offset passes that edge at 1/drawn of the way
across, so the readout stopped advancing partway over the chart and stuck
to the last point.

cursor.bind wraps the three events that carry a position and hands uPlot
a corrected clientX/clientY, so every step after it — the snap, the drag
detection, the redraw path — comes out right by construction. It is also
stateless, which removes the cursor.move memo and the drag.click
workaround that the mousedown/mouseup mismatch needed.

cursor.check.ts now replays uPlot 1.6.32's own pipeline against the
shipped cursor config, for panels scaled both down and up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 14:38:46 +02:00
stroblme 49e71aad28 Notepad: the https api origin is a compose default, not a stale image
Reopened with the actual trigger: the base compose builds the frontend
against https://api.${DOMAIN} and only compose.local.yml overrides it,
so any build without that layer breaks login locally. I closed this
earlier on the mistaken belief a rebuild had settled it.
2026-08-23 14:25:00 +02:00
stroblmeandClaude Opus 5 4ba775edd0 Put .dockerignore where Docker actually reads it
Every service builds with `context: ..`, and Docker only consults the
.dockerignore at the context root — the copies under frontend/ and backend/
never applied, so the host's dist/, node_modules/ and caches were shipped
into each build context.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CB8uwHkH52qxLxAkGN4gTo
2026-08-23 14:14:23 +02:00
stroblme 01b587519e Merge branch 'main' of git.stroblme.de:Fluksio/app 2026-08-23 13:01:26 +02:00
stroblmeandClaude Opus 5 0511dbb86a NOTEPAD: close the CubeLoader bug, note the loader the portal now draws
The invisible loader lived in the index repo only and is fixed there. What is
left for this repo is whether its lucide spinners should become the same brand
mark.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CB8uwHkH52qxLxAkGN4gTo
2026-08-23 12:51:15 +02:00
stroblme 485b8e9db5 Notepad: close the wave-1 items, record what they left behind
Eleven "to be sorted" entries are done. Three that survive are amended
rather than removed: the house panel's chart budget (the range picker
gave its row back), layoutGraph (height is derived now, width is still
guessed), and the dark:-variant scope, which turned out to share a root
with the React Flow colorMode bug. Adds the portal's missing
not-reporting state, and drops the frontend-origin entry the rebuild
settled.
2026-08-23 11:16:50 +02:00
stroblmeandClaude Opus 5 f58acfbd43 Stop uPlot swallowing the click on a chart
uPlot swallows the click that ends a drag, and decides a drag happened by
comparing the position it took at mousedown against the one it holds at
mouseup. It refines the first through `cursor.move` and re-reads the second
raw, never refining it (`mouseUp` -> `cacheMouse(initial: false)`), so with any
correction in place the two never agree: on a scaled panel every click on a
plot read as a drag and was stopped before it reached the page. A chart tile
could not be selected in the dashboard editor by clicking the chart, and a
Health chart could not be clicked to pin a moment.

Not the cursor correction being applied twice — that was a separate defect,
and idempotence could not have fixed this one, since the mouseup path never
applies the correction at all. Not `cursor.y` either: that only decides
whether the y cursor element is created, and both axes are read from the event
regardless.

Drag-to-zoom goes with it, and loses nothing: `setData` re-ranges the scales
from the data on every render, so a dragged range was erased by the next
reading. With no drag there is no click to protect from one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 11:07:54 +02:00
stroblme 92fc2e7397 up
Signed-off-by: stroblme <stroblme@posteo.de>
2026-08-23 11:02:08 +02:00
stroblmeandClaude Opus 5 d5fc3fa8e8 Spread a chart's default colours across the ramp
A chart that named no palette took the ramp in order, so three lines drew
slots 1, 2 and 3 — adjacent steps of a ramp that carries identity by lightness
alone, which at a 2px stroke read as close to one picture. Each count now
takes the slots furthest apart that the ramp allows: 1 -> 1, 2 -> 1/5,
3 -> 1/3/5, 4 -> 1/2/4/5. Five lines are unchanged.

Moving a default is only safe because the mechanism around it is inert: a
palette a dashboard wrote down is still drawn exactly as written, so no stored
document means anything different than it did. `palette.check.ts` now pins the
spread table and says which of the two properties is permanent.

An unnamed palette is empty rather than the whole ramp, so the settings panel
shows nothing picked when a dashboard is on automatic — and deselecting the
last colour is now the way back to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 10:53:28 +02:00
stroblmeandClaude Opus 5 98bf8fb7b0 Tell React Flow which theme it is drawing in
React Flow stamps `colorMode` on its wrapper as a class and defaults it to
`light`. The app's own token scopes are named `.light` / `.dark` — the classes
that let a dashboard be forced to one theme inside a shell on the other — so
every canvas was silently redeclaring the light palette on its own subtree:
white node cards and a white canvas in dark mode, with the inherited
`--foreground` text still near-white and therefore invisible. Home's neurons
had the same fault, drawn with white ring gaps and the light `--primary`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 09:29:30 +02:00
stroblmeandClaude Opus 5 e25351f515 Correct the flow.css header: the canvas does pass colorMode
The comment said the canvas follows the theme without a `colorMode` prop. It
does not — React Flow stamps that prop on the wrapper as a class and defaults
it to `light`, which collided with the app's own `.light` token scope and
re-themed the whole canvas. The prop landed in 4e1898a; this is its reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 09:28:42 +02:00
stroblmeandClaude Opus 5 163847cdc1 Give the traceback button its width back
Reserving a slot for all three status controls left 72px for a node's title,
against about 140px before. Only the stop button and the dot come and go on
their own; the traceback button appears when a node newly fails and goes when
someone acknowledges it, which is a thing to notice rather than a flicker. It
goes back in the row, and the title gets 106px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 09:28:25 +02:00
stroblmeandClaude Opus 5 b9922e7a7d Apply the chart cursor correction exactly once
uPlot writes what `cursor.move` returns back into the value it hands in next
time and calls it again on every redraw — and a chart sets its data on every
render. The correction was therefore applied repeatedly: the cursor walked
left while the pointer stood still, and the position taken at mousedown no
longer matched the one held at mouseup, which uPlot reads as a drag and
answers by swallowing the click. That is what stopped a chart tile being
selectable in the dashboard editor.

The refiner now returns its previous answer unchanged when handed it back, so
a redraw is a no-op. Lifted out of the config as `cursorRefiner` so the branch
can be checked without a browser; `cursor.check.ts` replays uPlot's own
sequences, including the press-is-not-a-drag one that regressed.

The Playwright hover test now also asserts the cursor holds its place across a
redraw, which is what the first version missed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 09:13:03 +02:00
stroblmeandClaude Opus 5 2fcaf96a83 Dashboard-level chart palette
A dashboard names the data colours its charts draw with: an ordered, distinct
subset of the `--chart-1…5` ramp, stored on the settings channel that already
carries `theme` and `locked`. No backend or client change — `SettingDef.value`
is free-form and a name this build does not wire up is left alone rather than
refused.

Naming none is the whole ramp, which resolves to the identical token per
series as the `--chart-${(index % 5) + 1}` charts drew with before, so every
existing dashboard is unaffected. `palette.check.ts` asserts that equivalence
rather than trusting it.

Distinct slots, not free assignment with repeats: only slot 1 against slot 5
clears 3:1 within the ramp, so two traces on one slot could not be told apart.
Status colour is deliberately outside the palette — a fault is `--destructive`
because of what it means, not because of where it sits.

The provider is mounted by the editor as well as the view, so picking a
palette repaints the charts beside the panel instead of describing them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 08:45:31 +02:00
stroblmeandClaude Opus 5 148d50f2bd Grow a node with its ports, stop it flickering, draw what it reaches out to
- A node's height follows the ports on its busiest side. It is a function of
  the document, so `layoutGraph` reserves exactly what is drawn and nothing
  measured is fed back into the layout.
- The three status controls now sit in slots that are there whether the
  control is or not. A node running many times a second mounted and unmounted
  the stop button on every execution, resizing the card each time.
- A port bound to another flow's message is drawn as a label, naming the node
  at the far end and its type. Only the opposite direction was answered
  before. The scan behind both is now cached on the store's commit counter
  rather than reading every flow per request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 06:35:28 +02:00
stroblmeandClaude Opus 5 680c6053b9 Chart cursor, range picker in the header, line smoothing
A dashboard canvas is CSS-scaled to fit its panel while uPlot maps the
pointer against its own unscaled plot width, so the cursor drifted further
right the further into a chart it went. A `cursor.move` refiner divides the
visual offset back into layout pixels; unscaled hosts get a no-op.

A querying chart's range picker moves onto the frame's title line through a
new `useHeaderSlot`, giving the plot back the row it spent. The editor's drag
handle is the header, so the picker is exempted from it.

Charts can be drawn as a monotone cubic spline — uPlot's own path builder,
monotone so a smoothed line never invents a reading between two real ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 06:27:48 +02:00
stroblmeandClaude Opus 5 a225b48d0d Send the portal the failure count its own tile shows
The portal card's "Failures 24h" had read 0 since failures_24h left
HealthSummary: that commit noted nothing read the field, which was true of this
repo and not of the portal in index/, where health is an opaque JSON blob with
no schema to catch the removal.

Restored on the connector rather than in /summary — no screen here reads it, and
it is folded from /observability/flows?hours=24, the same endpoint and window the
app's Home tile sums, so the two cannot drift apart again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 06:24:58 +02:00
stroblmeandClaude Opus 5 5cbde1000f Draw the in-progress connection in the selected stroke
`--xy-connectionline-stroke` was never set, so the line dragged between two
ports took xyflow's `#b1b1b7` literal in both themes and was a hair thinner
than the edge it became. It is a selection-like state, so it takes `--primary`
and the canvas's own edge width.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 06:22:32 +02:00
stroblmeandClaude Opus 5 d240fa1f3e Brain view: slower edge falloff, chase arc in the neuron's gap
--brain-decay 90s -> 150s and the wire floor 32% -> 16%, so a quiet
connection dims further and takes longer doing it. The pulse ring's inset
and band become custom properties (defaults unchanged for the editor's
card), and the neuron passes its own ring and gap so the arc runs the full
width of the gap; it moves after the disc, which otherwise paints over it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 06:21:26 +02:00
stroblmeandClaude Opus 5 b2ade12518 Give the canvas dot grid the token colour React Flow actually reads
`flow.css` set `--xy-background-pattern-dots-color`, which nothing reads:
xyflow's `.react-flow__background-pattern.dots` reads
`--xy-background-pattern-color` and only falls back to the `-dots-color-default`
literal. The grid was therefore pinned to `#91919a` in both themes — and with no
`colorMode` prop the `.react-flow.dark` default never applies either, so it read
as a light-mode grid on a dark canvas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
2026-08-23 06:20:26 +02:00
stroblme ece8e0cce8 up
Signed-off-by: stroblme <stroblme@posteo.de>
2026-08-23 00:11:08 +02:00
stroblmeandClaude Opus 5 12057d83aa Panels for a ten-inch screen, and a motor button that says where it is
Both screens the house is looked at on are 1280x800, so that is what the three
dashboards are laid out for: twelve columns of 96px, twelve rows of 51px, and
nothing past the bottom, because a panel does not scroll.

The motors are one control each instead of three buttons. A button could only
publish; a segmented control reads back as well — so the motor writes what it
is doing to the same message the control sets, and the segment that is held is
the direction it actually went. Up, Stop, Down for the shutters; Close/Open for
the window and In/Out for the awning, which is what those two are for.

A run stopped part way now leaves the position unknown rather than claiming the
target it never reached, so the next command in either direction moves it.

The preflight gained the two checks this needed. One runs each sample shape
past the port that would receive it. The other is arithmetic: every tile inside
the panel and none on top of another — both silent failures on a screen with no
scrollbar, and both caught before anything is written.

Sizes were settled by looking. A slider needs three rows or its tick labels
fall off; a status icon needs three or it loses the word under the glyph; a
gauge in two rows has no arc worth reading, so the battery is a bar on Home and
a gauge on Energy where there is height for one. A chart spends eighty pixels
on its chrome whatever it is given, so two of them read on this panel and three
did not — the temperature history is the one that went, and `history` still
answers for it.

`capture-panels.mjs` is how that was checked: the three panels at the screen's
own pixels, in both themes, reporting whether anything spilled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 18:50:03 +02:00
stroblmeandClaude Opus 5 44f2d3614c NOTEPAD: the house-control rig is deleted, not running
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 16:38:25 +02:00
stroblmeandClaude Opus 5 06efbec018 history: one Flux script per chart, and rows are json
Two things the installation found that the checks did not.

A script with three `from()` statements in it produces three results all
called `_result`, and InfluxDB refuses that outright — so the measurements go
into one filter and the rows come back tagged with which one they are.

And the answer a database node hands back holds a *list* of rows, which a
record may not: a record is flat scalars. It was declared one, so every chart
failed on the type check the moment a real answer arrived.

The second one is now caught before anything is pushed: the preflight runs
each sample shape past the port that would receive it, which is what turns
"expected record, got dict" from a runtime surprise into a line of output.

Also records the two engine faults this seeding session surfaced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 16:24:38 +02:00
stroblmeandClaude Opus 5 72b2e0365f NOTEPAD: what the house port settled, and what it left
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:46:08 +02:00
stroblmeandClaude Opus 5 2538246242 Seed the TinyHouse: nineteen flows in place of eight hundred nodes
The Node-RED installation this replaces is 865 nodes across three tabs, and
roughly a fifth of it is unreachable — the pellet stove's controller, the
scene engine and the awning's logic were all disconnected from the heartbeat
they ran on. What is here is the intent rather than the wiring: nineteen named
flows, 109 nodes, and no heartbeat at all. A sensor value is the event.

The device layer moves with it. `actor/*` and `light/*` were never a device
interface — Node-RED subscribed to its own topics, stamped a DMX channel on
each and encoded one Art-Net universe — so those topics retire with it and the
encoders are five nodes in the `dmx` flow.

Two shared library nodes carry what every actuator needs.

`arbiter` answers the thing this design was missing: a value someone sets on a
screen is not undone by the next evaluation. A manual value wins for a hold,
the house takes over when it expires, and a schedule can force past both — so
"off at two in the morning" still means off. The control binds to the message
the arbiter writes back, so one tile shows what reached the fixture and
setting it is the override.

`motor` is why a stop is now commanded once. A rollershutter has no position
sensor, so time is the only feedback: it says how long to run and a trigger
sends the single STOP that ends it. The reference sent STOP forever.

Everything is seeded stopped, the Art-Net node does not transmit and the heat
pump does not accept commands until house.json says so.

`--dry` checks the whole set without an installation: names nothing provides,
loops, type disagreements, widgets bound to nothing, and every Python node run
once on values of the shape it declared — including whether what it returns
goes anywhere. That last one has already caught a typo that would have
published into silence.

house.json holds this installation's addresses, MAC addresses and DMX map and
is git-ignored, as the Node-RED inventory is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:44:21 +02:00
stroblme 4d36da27e1 Merge branch 'main' of git.stroblme.de:Fluksio/app 2026-08-22 14:18:23 +02:00
stroblmeandClaude Opus 5 ba707c8051 nodes: what porting the house needed from the vocabulary
Four small things, each with a device behind it.

An MQTT filter now routes what it subscribed to. `+` and `#` reached the
broker and were then looked up in an exact-match dict, so every message a
wildcard subscription received was dropped in silence.

`json_key` lifts a value out of the object a device wraps it in — Victron
publishes `{"value": 47}` on every path, which was otherwise a Python node
per port.

The trigger node learned `passthrough` and `wait_port`, because how long to
wait can be a value rather than a constant: a rollershutter takes 26 seconds
up and 28 down. A wait of zero sends nothing afterwards and still cancels
what the last message scheduled, which is how a stop is commanded once
instead of forever.

The HTTP sender takes fixed `query` parameters, so an API key is a secret
reference rather than a message on the canvas, and `send_inputs` off for a
request whose inputs are only a trigger.

Also: `delay` accepts fractional seconds, and `TZ` reaches the container, so
a cron expression means local time. Left unset it is UTC, as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:16:08 +02:00
stroblme 25a1900481 NOTEPAD: clear what wave 4 closed, record what it left 2026-08-22 13:19:41 +02:00
stroblmeandClaude Opus 5 d958d7cde6 Add the dashboard settings channel, wired for theme and lock
A dashboard could only ever receive as a set of tiles. This adds the dashboard
itself as a receiver: `settings` maps a name to a value plus an optional
binding. Unbound, the setting is simply its value — a wall panel that is always
dark costs no flow. Bound, a flow drives it live and the value is the fallback.

Two settings are wired: `theme` (system/light/dark) and `locked` (read-only).
There is no schedule field on purpose — a node publishing to the bound message
on a cron is what a schedule is here, which is the point of a channel.

- `messages_for()` now walks a dashboard's bound settings as well as its
  widgets' bindings. Without this a paired screen is refused its own theme
  message, on the one surface the setting exists for; it bounds the socket too.
- `locked` is gated in `usePublish`, so every control inherits it, and each
  control also draws itself disabled — a dead button reads as broken otherwise.
  The panel surface says Read-only in the corner.
- The theme is a class on the dashboard's own surface, never the root: inside
  the app shell it must not flip the chrome. `.light` gains the tokens `.dark`
  already had (mirrored in the index repo) so both directions work on a subtree.
- Settings bindings are type-checked from the document alone, the rule widget
  bindings follow, and mirrored on the server.
- A bound setting is drawn on the flow canvas as a dashboard-level endpoint.
- The demo's house flow now publishes `home.panel_theme`, which the demo
  dashboard's theme binds to: the panel goes dark after sunset, at no tile cost.

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