Commit Graph
49 Commits
Author SHA1 Message Date
stroblmeandClaude Opus 5 a3cb9cc817 Drop the unused Logo icon variant, poll the Home runs tile at 10s
The icon variant lost its last caller when the responsive variant stopped
carrying a second cropped image. runOverviewQueryOptions now takes the
refetch interval, defaulting to the 30s the Runs screen and the dashboard
panel want; Home passes 10s so a short run does not linger on the tile.
agents.md spells the API host as api.${DOMAIN}, which is what the rest of
the docs use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CL9zvnnvcp1mvA8o7impxk
2026-09-06 14:24:28 +02:00
stroblmeandClaude Opus 5 d01a8dad37 Rename Installation to Instance
Follows the portal: the noun is "instance" everywhere the app says it —
UI strings, CLI output, error details, docs and comments. The wire keys
(`instance_id`, `instance_token`) and the hub route this calls move with it.

An existing cloud.json is adopted rather than refused: without the key
alias the dataclass fails to parse, which the caller swallows and reads as
"never enrolled" instead of "reconnect".

`instance_key` on a node type becomes `target_key`. It means the outside
thing a node points at, which is a different sense of the word, and keeping
both would put two meanings of "instance" in one codebase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
2026-08-31 10:12:01 +02:00
stroblmeandClaude Opus 5 671452e5eb Let a Home tile lead where its figure does, and drop the logo nobody saw
`Tile` takes an optional link: Nodes and Flows running to /flows, Runs
running to /runs, Failures down to the list further down the same page.
Queue backlog and loop lag have no screen of their own and stay plain.

`Logo`'s responsive variant carried a second `<img>` for the collapsed
rail, but AppSidebar hides the whole logo when collapsed, so an ancestor
`display: none` meant it never rendered in any state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KYM38KSb4V4v2T71eifnZv
2026-08-30 17:18:01 +02:00
stroblmeandClaude Opus 5 1873787ee6 Compress what the browser downloads, and stop one tile taking the page
**nginx served the bundle uncompressed and uncacheable.** The base image
ships gzip commented out and nothing set `Cache-Control`, so every load
carried the whole thing and every reload cost a 304 per asset. Measured on
the built image: the entry chunk 838 kB → 311 kB, the Monaco chunk 2.66 MB
→ 832 kB, and the ~50 content-hashed assets are now immutable for a year.
`index.html` is explicitly `no-cache`, since it is what names the rest.

**A widget that throws no longer blanks the screen.** There was one error
boundary in the app, on the root route, so anything that threw replaced
everything including the navigation — on `/view/{name}`, an unattended wall
panel with no way back. Each tile has its own boundary now, and the app
shell has one inside it so a screen that fails leaves the sidebar standing.
`react-error-boundary` was already a dependency and imported nowhere.

**`localStorage` cannot take the app down.** Reaching it raises where the
browser blocks site data, and `setItem` raises once the origin's quota is
full — which the flow editor's node clipboard, carrying whole Python
sources, can genuinely reach. Thrown from a key handler that escaped to
`window.onerror`, which the single root boundary then turned into a blank
page. `lib/safeStorage.ts` is the guarded pair the pre-paint theme script in
`index.html` was already using; a copy too large to store now says so.

Queries default to `staleTime: 5000` — below every poll interval on any
screen, so nothing polls less often than it did, but a route mounting twice
in a few seconds stops refetching everything it touches. Window-focus
refetching is off: the socket pushes what changes and a reconnect
invalidates what it feeds, so a focus event has nothing of its own to say.
Home alone reads about ten queries on every one of those.

Render cost, two that showed up in the audit:

- `LogsPanel` was rendered unconditionally by the dock and decided inside
  itself whether to draw, so with the panel *shut* it still subscribed to
  the log store and re-filtered five hundred lines per line a flow
  published. It returns before any of that now.
- `HealthActivity` subscribed to the whole engine-event array and used one
  number from it, so a flapping node re-rendered the component that draws
  Home's two uPlot charts — each of which rebuilds its series on every
  render by design. It subscribes to that number.
- the global search bucketed the index nine times per keystroke, once per
  group. One pass, and each group offers at most twenty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6hPWS6YEbT1P8LxhhFb2T
2026-08-29 21:00:02 +02:00
stroblmeandClaude Opus 5 7e506b26c0 Act on a run, and read Home top-down
Docs / docs (push) Successful in 23s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m18s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m49s
pre-commit / pre-commit (push) Failing after 2m5s
Test Backend / test-backend (push) Successful in 2m39s
Compose Smoke Test / test-compose (push) Successful in 33s
Playwright Tests / merge-reports (push) Successful in 1m11s
Runs: a run can now be deleted (DELETE /runs/{id}, cancelling a live one
first), exported as csv from the screen's own filters, and its flow label
opens the flow. Its "Parameters" panel became "Inputs" and lists every
input the flow declares, marking the ones that took the flow's own value
rather than the run's — the comparison table resolves the same defaults
instead of printing "unset".

Home reads brain, dashboards, health, flows: the mosaic is one full-width
scrolling strip, and the flows list and the flow-activity rollups merged
into a single left-joined table so a flow's state and its numbers sit on
one row.

Charts take a drag to narrow the x window and a double click or tap to
come back out. UplotChart holds the scale and passes resetScales:false
while a window is held, which is what the old comment said made this
impossible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LrWVRguqbk33YzfEeUx5W
2026-08-29 08:37:11 +02:00
stroblmeandClaude Opus 5 4215e057d1 Add a global search, and stop the sidebar logo squeezing
Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m14s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m53s
pre-commit / pre-commit (push) Failing after 2m13s
Test Backend / test-backend (push) Successful in 2m38s
Compose Smoke Test / test-compose (push) Successful in 38s
Playwright Tests / merge-reports (push) Successful in 1m8s
`GET /api/v1/search/` hands the client one flat index of everything worth
jumping to — flows and the nodes inside them, dashboards and the widgets on
them, panels, secrets, modules, workers and alert channels — and cmdk matches
it in the browser, so results narrow while typing without a round trip per
keystroke. A node hit is the one thing no list endpoint could answer: it opens
its flow with that node in focus.

The panel is reached from **Search** above Documentation in the sidebar, or
⌘K anywhere. The flow canvas palette moves to ⌘P, being the narrower of the two.

The panels dialog gains an address (`/dashboards?panels`) so a panel hit has
somewhere to land, and the sidebar logo gets `shrink-0`: the rail's width
animates while the logo is already back, and a flex item short of room is
squeezed rather than clipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vGH7jqcXxWKP9wZFPyVdU
2026-08-28 22:19:08 +02:00
stroblmeandClaude Opus 5 21caa354e7 Stop two docstrings describing a schema that is gone
Pages and sections left in 7ff29ca. DashboardView still promised SectionDef
stayed and that the editor wrote back into the first section; the mosaic
still said "the first page's widgets". Comments only — the code was right.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1moruzue2kTJd3uVisgNk
2026-08-28 20:41:29 +02:00
stroblme 9d11e01a48 Point the range picker at the shared segmented control
RangePicker carried a byte-for-byte copy of ui/segmented.tsx's markup; it
now renders that component instead.

The dashboard's own segmented widget stays where it is: it is not a copy
but a look-aware control, drawn per look in
Dashboard/ui/{fluksio,glass,material}/Controls.tsx over the shared
useSegmented behaviour. Note that in segmented.tsx in place of the stale
size-prop plan.
2026-08-28 18:49:29 +02:00
stroblmeandClaude Opus 5 d436597db9 Announce the pending screen once, not twice
Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m14s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m41s
pre-commit / pre-commit (push) Failing after 2m29s
Test Backend / test-backend (push) Successful in 2m33s
Compose Smoke Test / test-compose (push) Successful in 37s
Playwright Tests / merge-reports (push) Successful in 1m8s
The loader carries its own role="status" and label, so the <output> around
it was a second live region saying the same word.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1moruzue2kTJd3uVisgNk
2026-08-28 15:38:25 +02:00
stroblmeandClaude Opus 5 498c2eb221 Use the brand loading mark instead of lucide's spinner
Copies index/frontend's FluksioLoader component and its
`.fluksio-loader-line` rule into the app (the shadcn duplication model, so
the CSS travels with the component) and swaps every Loader2 pending state
over: the router's pending screen, the flow dock's Run and Publish, the
dashboard editor's Publish, the overview toolbar's Publish-all and
LoadingButton. One loading mark across both shells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1moruzue2kTJd3uVisgNk
2026-08-28 15:36:54 +02:00
stroblmeandClaude Opus 5 f855170562 A documentation link in the sidebar, and the footer points home
Documentation above Settings, opening in a new tab — the docs are read beside
the work, not instead of it. The footer's "Fluksio - <year>" links to
fluksio.com; the sidebar logo deliberately keeps going Home, since navigating a
running installation away to a marketing page is not what pressing it means.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:15:39 +02:00
stroblmeandClaude Opus 5 530dd3587c Draw the error series terracotta on Home
Failures were slot 1 of a ramp that carries identity by lightness alone, so at
a 2px stroke the line that matters read as one picture with avg and lag. It
takes --brand-secondary now — the same terracotta the brain graph above it
already spends on a flow that cannot run. A palette entry naming a token is
resolved off the chart's element, since a canvas cannot parse var().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:15:14 +02:00
stroblmeandClaude Opus 5 5da5606f79 Settings as panels, the way the portal does it
Four tabs over three cards was navigation for its own sake, and the two shells
disagreed about what a settings screen looks like. It is one grid of cards now,
matching the portal: the account card carries changing a password and deleting
the account in its footer, appearance is a card with one row, and remote access
— an operator's concern, not a personal preference — is a card of its own for a
superuser.

SettingRow, alert-dialog and UserAvatar come across from the portal, so an
account renders the same face in both shells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:04:42 +02:00
stroblmeandClaude Opus 5 6d09500a73 Fold the connection banner into the notification stack
The offline pill was drawn inside a `flex h-0` container with align-items at
its default stretch, so the pill was stretched to a zero-height box while still
carrying its padding — the "box too small" it was reported as. It is a
persistent notification now, keyed on one id so a poll that keeps confirming
"still offline" replaces the card instead of restacking it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:00:56 +02:00
stroblmeandClaude Opus 5 44c8825927 Replace sonner toasts with one top-centre notification stack
The app said things in two places: sonner's bottom-right toasts and the
custom top-centre chrome. This is the second of those, generalised — a
module-level store anyone can call, drawn as a stack of frosted cards that go
on their own unless raised as persistent, with room for buttons and for a
message longer than a line.

useCustomToast keeps its signatures, so the forty-odd call sites behind it and
handleError are untouched. The component and its store are byte-identical with
the portal's copy and checked by `make design-check`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:00:47 +02:00
stroblmeandClaude Opus 5 6bc71ddaf3 Record flags in history and draw them as steps
A bool was excluded from the history as "not a measurement", so a true/false
port had no curve in the node panel and none on an edge — only the word. It is
recorded as 0/1 now and drawn as steps, since a bezier through two states
slopes through readings that never happened. The axis is pinned to 0..1, so a
flag that was never on sits at the floor rather than mid-box.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 15:54:06 +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 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 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 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
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
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 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 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
stroblme 8224d12c8c Add a colour-wheel widget to the dashboard
A custom hue ring — a conic gradient, not a canvas — with saturation and
brightness sliders beside or under it depending on the tile's shape, sized
for a wall panel and reachable from a keyboard. It publishes [h, s, v] by
default, which is what the reference installation's DMX encoders read, and
`format` switches that to [r, g, b] or "#rrggbb".

`usePublish` moves to its own module so a widget in a file of its own can
reach it without importing `widgets.tsx` back.
2026-08-22 12:50:59 +02:00
stroblmeandClaude Opus 5 9e3093bd72 Edit the whole dashboard, and let the grid follow its canvas
The editor only ever arranged the first section, so the demo's Home
dashboard lost 16 of its 25 widgets the moment it was edited. A page's
sections are now read as one arrangement — each pushed below the one
before it — and written back as one, which is the shape a dashboard was
already heading for: one dashboard, one canvas, and the panel rail for
the several-dashboards story. The page tabs that story made dead are
gone; PageDef/SectionDef stay in the schema and a page the editor does
not show round-trips untouched.

The row height derives from the canvas as the column width already did,
so the same arrangement is the same picture on a 7" panel as on a 4K
one. The uPlot rules move beside UplotChart, where a chart on Health is
styled without a dashboard having been visited first, and the bar's
readout travels on one property instead of jumping sides at 30%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tULRZJUkZsw7rMJ3h4xvu
2026-08-22 12:22:37 +02:00
stroblme 3674099758 Centre the offline banner over the content column 2026-08-22 12:09:18 +02:00
stroblmeandClaude Opus 5 f17d51c12f Home mosaic, multi-select delete, offline banner and loading states
- Home puts the dashboards beside the flows: two equal-height columns,
  capped and scrollable, most recently worked on first. Each tile is a
  schematic footprint built from the stored widget placements.
- Flows and dashboards can be picked by long press or ctrl-click; the
  create button becomes a trash and one dialog covers the batch.
- The offline banner is drawn on the body so it centres on the viewport,
  and the live socket now releases the offline latch a stray 503 set.
- A boot spinner before React's first commit, a router pending screen for
  code-split pages, and skeletons where an empty list used to flash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tULRZJUkZsw7rMJ3h4xvu
2026-08-22 12:02:14 +02:00
stroblmeandClaude Opus 5 c69c9797d5 Stack a bar's readings, and stop widgets taking the phone sideways
A bar drew its nested reading on top of the outer one in --chart-5, which
measures 2.53:1 against --primary and lost the 3:1 guideline for non-text. The
readings now partition the fill end to end, up to three of them, in a token of
their own: --primary-nested, the primary hue a few steps deeper, 3.14:1 light
and 3.12:1 dark. It cannot also clear 3:1 against --muted — in dark those two
are 5.82:1 apart and a colour 3:1 from both would need a 9:1 gap — so a segment
is drawn inside a gutter of outer fill rather than ever bordering the track,
which is what separates neighbours too, and what caps the count at three. A
nested value larger than its outer used to spill onto the track; it is clamped.
`inner` still reads as a single binding, so no dashboard needs migrating.

On a phone, .widget-grid took its width from the widest thing any widget held —
a truncating flex item still offers its whole unwrapped line as a min-content
contribution — and a handful of widgets had no floor of their own: the uPlot
legend is a table, a fieldset carries min-inline-size: min-content from the UA
sheet, and buttons are whitespace-nowrap. Each is capped now. A widget's body
scrolls rather than clipping, so long text stops painting over the title.

Gauges and bars move between readings instead of jumping, and a segmented
control slides one thumb rather than recolouring cells. The gauge arc is drawn
whole and revealed by its dash, because `d` cannot be transitioned.

UplotChart pushed new readings only when the point count changed, so once a
rolling window was full a refetch left the old values on screen.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AD8SfVhzXBG2nAfFcVh3iD
2026-08-19 13:20:12 +02:00
Melvin Strobl 129c92e3d8 link
Signed-off-by: Melvin Strobl <lc3267@kit.edu>
2026-08-18 14:22:47 +02:00
stroblmeandClaude Opus 5 1c09b8209d dashboard: pace a querying chart by its window, and an example to evaluate it
A chart is drawn in buckets, and nothing it can show changes until the
bucket it is drawing closes — so the resolution sets the refresh rather
than a flat five-second floor. A week at quarter-hour buckets now asks
four times an hour instead of sixty, for the same picture. Leaving the
field empty follows the window; a slower rate is still honoured.

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:06:45 +02:00
stroblmeandClaude Opus 5 3b5241bb9a Overviews: create before publish-all; Home: the trend curves keep their dot
Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
The Home table drew the one sparkline without its live dot, on the grounds
that a polled rollup's right edge is the last complete slice rather than this
instant. Identical curves reading differently is the worse trade, so the dot
is back everywhere and the cell keeps room for the half that hangs past the
curve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 12:58:35 +02:00
stroblmeandClaude Opus 5 9351a86eec Overviews: icon toolbar, dashboard drafts, publish all
Both overviews carried the same toolbar twice, left-aligned, with a search
field permanently taking a row of width. One `OverviewToolbar` now serves
them: the search folds into an icon and expands again on click (Escape puts
it away and hands focus back), create is a `+`, and everything sits right of
the page. Each page keeps its own create dialog — the toolbar only renders
the trigger — so the testids the runtime spec and the capture script drive
stayed where they were.

Dashboards get the flow store's draft/publish split. The editor autosaves
`dashboard.draft.json` beside `dashboard.json`; `/view/{name}`, `bindings_for`
and `history_requirements` keep reading the published file, so a wall panel
sees an edit only once someone publishes it. `POST /dashboards/{name}/publish`
and `/discard` mirror the flow routes down to the version precondition and the
409, `GET /dashboards/{name}?draft=true` is what the editor asks for, and the
dock grows the same Publish button — which flushes a queued save first, so an
autosave in flight is not published around. Creating a dashboard still writes
the published file directly: an empty document on a panel is harmless, and it
keeps the store free of a never-published case.

"Publish all" is a checkmark in the toolbar, live only when something actually
has `has_draft`. A summary carries no version and publish needs the one it is
based on, so each document's detail is read immediately before its publish —
honest against a stale list, and no version-less backend path to maintain.
Failures are counted rather than swallowed: three of five fails says so and
names the three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
2026-08-17 11:57:11 +02:00
stroblmeandClaude Opus 5 683c25b26d Home: one time range across the health block, and failures that follow it
The health screen was fixed at 24 hours everywhere except its lists, which
were fixed at nothing: `failuresQueryOptions` read the newest 100 rows and
`HealthActivity` filtered them client-side, so on a busy engine the failures
list covered whatever few minutes 100 rows happened to span while the chart
beside it spanned a day — and pinning an older minute showed an empty card.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
2026-08-16 22:29:32 +02:00
Melvin StroblandClaude Opus 5 6b73ca3645 Put settings in the sidebar and let the sidebar frost composite
The footer was a user card with the theme picker and log out buried in
its dropdown. It is now two plain entries, Settings and Log Out, and the
theme moved into the settings page as its own tab.

The dark sidebar never matched the canvas because _canvas.tsx painted
bg-card across the whole viewport underneath it, so the floating
panel's bg-card/80 resolved to card over card and its gutter was card
too. Nothing there should paint a surface; the panel now reads the same
as the other frosted chrome.

The superuser guard sliced the first three tabs, which silently changed
meaning when a fourth arrived.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkmeRiyeYmVZqJVwuyHq9o
2026-08-15 21:19:41 +02:00
Melvin StroblandClaude Opus 5 4aa2705d34 Rename the workspace layout: fluksio -> app, website -> index
The repo moves to git.stroblme.de:Fluksio/app.git and its sibling to
Fluksio/index.git, so every cross-repo path reference moves with it. The
architecture sources the roadmap cites now live in the docs repo under
architecture/, and the brand marks in the index repo's src/assets.

Also replaces the placeholder README with one that reflects the actual layout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 16:16:08 +02:00
Melvin StroblandClaude Opus 5 5988822053 De-template the frontend and apply the design system
- FastAPI branding replaced throughout: title, favicon, logo (now the org/
  brand SVGs), footer links, route titles; workspace package renamed
- shadcn primitives restyled to the component radius/shadow map: pill
  controls, rounded-lg surfaces, shadow-e1/e2/e3
- floating frosted AppSidebar over a bg-card content region
- theme storage key aligned with the pre-paint script, MotionConfig added
- scripts/capture-screenshots.mjs backs the root 'make verify'

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 15:33:56 +02:00
Melvin StroblandClaude Opus 5 1916f7f778 Vendor backend and frontend into the monorepo
The submodule collapse was only half applied: .gitmodules was deleted but
backend/ and frontend/ were still recorded as gitlinks, so none of their
files were tracked. Replace the gitlinks with the real trees.

Also untrack .env (it carried placeholder secrets) in favour of a tracked
.env.example, drop the committed __pycache__, and narrow the blanket *.png
ignore that would have swallowed design assets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 15:14:54 +02:00