Commit Graph
351 Commits
Author SHA1 Message Date
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