Commit Graph
272 Commits
Author SHA1 Message Date
Melvin Strobl 659799d1e5 Panel I/O: PanelDef.io ports, scoped publish and upload, I/O section in the Panels dialog 2026-10-13 21:10:54 +02:00
stroblme a93f865e91 Open in dashboard: searchable Command list 2026-10-12 22:07:17 +02:00
stroblme 530a4b602e Dashboard message picker: searchable combobox instead of a plain select 2026-10-12 22:06:31 +02:00
stroblme 4463007736 Dashboard palette: switch dashboard, publish, settings; ⌘S publishes 2026-10-12 22:05:39 +02:00
stroblme 88c80bdb60 Shared Dock, PublishButton, DiscardButton, DiscardDraftDialog for the flow and dashboard editors 2026-10-12 22:04:34 +02:00
stroblme 237b9d96c6 Button icon-touch/sm-touch sizes replace the repeated touch-target classes 2026-10-12 22:02:35 +02:00
stroblme 59272299f5 Dashboard add widget: searchable command palette with ⌘P, as on flows 2026-10-12 21:59:16 +02:00
stroblme d7c6e27a2a Embed widget: Image option scales a camera stream to the tile 2026-10-11 20:23:37 +02:00
stroblme 79e3908a76 Dashboard icons: add cctv 2026-10-11 20:20:54 +02:00
stroblme c1a6a89e13 Make alert dialog footer buttons 44px below md
The same treatment `ui/dialog.tsx` got: every footer button is a 44px
target on a phone; desktop keeps its sizes.
2026-10-07 09:32:07 +02:00
stroblme 7d6cc5b0bf Serve index.html no-cache on every route; check the theme script's CSP hash
`try_files` served the SPA fallback inside `location /`, so `/` and every
client route went out without `Cache-Control: no-cache`. The fallback is now
the last argument, an internal redirect to the exact no-cache location.

`src/csp.check.ts` recomputes the inline theme script's sha256 and fails
`bun run check` with the new value when the CSP no longer names it.
2026-10-07 09:32:06 +02:00
stroblme 8f3e229c8f Sandbox website tiles by default; opt into same-origin per widget
A website tile now runs in an opaque origin (allow-scripts, popups, forms).
Its new "Trusted site" setting adds allow-same-origin for sites that need
their own cookies and storage, and a trusted tile pointing at the origin the
app runs on (its own, or the hub's) shows a notice instead of the frame.
2026-10-07 09:31:49 +02:00
stroblme ed8aee2387 Authenticate sockets by header or one-time ticket instead of ?token= 2026-10-07 08:58:22 +02:00
stroblme 953a983919 Bind a worker's name and labels into its token; list and revoke tokens by jti 2026-10-07 08:42:14 +02:00
stroblme 243d39b591 Give connectors a store that survives restarts, and a way to clear it
`ConnectorNode.store` keeps short strings per connector package in the
engine's database (`connector_state`, migration f8c3e5a91d27), so the nodes of
one package share them and `fluksio serve` needs nothing new: get, set and
delete, at most 64 kB a value and 256 keys a package. A superuser lists the
keys (never the values) and clears them through `/connectors/{node_type}/state`,
or under Stored state in a connector node's panel. Documented in the connector
contract.
2026-10-06 17:19:48 +02:00
stroblme 8a1685741b Show Secrets to superusers only, and guard its route as Admin is
The page reads GET /secrets/, which is a superuser's, so any other account
opened it onto a permission error.
2026-10-06 12:33:00 +02:00
stroblme 4dfb1e1206 Send a CSP and security headers from the frontend's nginx
script-src admits the bundle and the pre-paint theme script by hash, with no
unsafe-inline or unsafe-eval; style-src keeps unsafe-inline for the <style>
elements Monaco, Radix and the toasts insert. frame-ancestors 'self',
nosniff and strict-origin-when-cross-origin go with it, and the Dockerfile
adds the API origin to connect-src when the bundle calls one elsewhere. Zod
runs jitless so its eval probe is not reported on every page. The embed
widget keeps allow-same-origin: the demo's weather map does not render
without it; the comment now says so and what it costs.
2026-10-06 12:30:47 +02:00
stroblme 8e928c3ffb Read secret names in the node panel only as a superuser
GET /secrets/ is a superuser's, so every node a non-superuser opened
showed "You do not have permission to do that". The query now runs only
for a superuser; anyone else gets no secret suggestions, silently.
2026-10-06 12:01:53 +02:00
stroblme 6f6970d9d6 Upgrade dependencies flagged by pip-audit and bun audit
Backend: starlette 1.7.0, pyjwt 2.15.1, python-multipart 0.0.32,
aiohttp 3.14.4, anyio, idna, urllib3, click, lxml, mako, multidict,
pydantic-settings, pygments, python-dotenv, requests. pip-audit goes
from 121 findings in 16 packages to pytest alone (dev, needs 9.x).

Frontend: axios 1.20.0, form-data 4.0.6, dompurify 3.4.16 through an
override (monaco-editor pins 3.4.8), vite 7.3.7, the router packages
and the vite and router build-time dependencies within their ranges.
bun audit goes from 97 to 21, all through @hey-api/openapi-ts 0.73.0,
whose fix is a breaking 0.97 upgrade. The new router plugin reorders
routeTree.gen.ts.
2026-10-06 11:50:35 +02:00
stroblme 6802cd2203 Make changing engine-run code superuser-only
Writing node source, installing modules and any flow save that adds,
retypes or repoints a code node (Function, Command), edits a Command
node's settings or changes a node's resources.env now answer 403 for
everyone but a superuser. MCP tools and the assistant reach the same
routes, so they inherit the gate. Node types report runs_code.

The editor shows code read-only with a note, disables a Command node's
settings, leaves code nodes and reusable nodes out of the palette and
paste, and the Modules page offers no install.
2026-10-06 11:36:30 +02:00
stroblme 2914d860b1 Make self-service signup opt-in via SIGNUP_ENABLED
/users/signup answers 403 unless SIGNUP_ENABLED is set. A public
GET /utils/config/ tells the login page, which hides the sign-up link,
and the signup page explains that an administrator creates accounts.
2026-10-06 11:24:27 +02:00
stroblme 70421c7454 Scroll the current dashboard to the middle of the phone rail
On mount and on every switch the horizontal rail scrolls itself (not the
page) so the active entry is centred, instantly under reduced motion.
2026-10-06 07:24:53 +02:00
stroblme eca2ccde2f Make dialog close and footer buttons 44px below md
The close button grows to a 44px target centred on where its icon already
sits, and every footer button is 44px tall, on a phone only; desktop keeps
its sizes.
2026-10-06 07:24:53 +02:00
stroblme b5513f8615 Edit json, record and list values in the JSON dialog
An inject's payload and a flow input's starting value of those types use
JsonField instead of a one-line text field. The field can now hold no value
(shown as the placeholder; an emptied editor saves null) and checks the shape:
a record must be an object, a list an array.
2026-10-06 07:24:43 +02:00
stroblme 409a90db28 Draw charts from one categorical palette, --chart-1…8
The one-hue ramp told lines apart by lightness alone. --chart-1 is now
--primary and slots 2-8 are distinct hues (orange, violet, green, orchid,
ochre, teal, magenta) that clear 3:1 on --card and stay apart under
protanopia and deuteranopia; red and sage are left out as the status
colours. A chart takes the slots in order instead of spreading a ramp,
and a dashboard's own palette continues after its primary and listed
colours with slots 2-8 rather than tints of its scheme. Line charts stay
at five series. Tokens mirror index's, as design-check requires.
2026-10-06 07:05:09 +02:00
stroblme 2c605b2c13 Float the dashboard rail along the bottom on a phone
Below md a dashboard stacks to one column and dropped its rail. The rail is
now a horizontal row in the dashboard's own look, floated over the bottom
of the screen on /panel/{id} and above the dock in the app's view, with
44px items, sideways scrolling when it overflows, and padding so the last
tile clears it; a panel's read-only notice moves up out of its way. The
three looks share the geometry from ui/core.
2026-10-06 00:22:28 +02:00
stroblme 5a145b8f59 Edit JSON node settings in a dialog from a one-line preview
A free setting typed json and a declared object or array setting (a Switch
node's rules, HTTP headers) show one line of their value; clicking opens a
Monaco JSON editor in a dialog that saves only once the text parses.
Declared object settings were not shown at all before. Escape in a dialog
opened from a side panel no longer closes the panel too.
2026-10-05 23:49:42 +02:00
stroblme 6fe59347e0 Name a port apart from its message in the node panel, two lines per port
The port name follows the message until it is typed by hand, and clearing
it follows again. The second line holds the message, so a list port's item
type no longer squeezes the name.
2026-10-05 23:28:04 +02:00
stroblme afe0c94a24 Talk to wren nodes from the chat's Control tab, opened from Home 2026-10-05 20:59:11 +02:00
stroblme 11c7fdcb8b Centre a node arrived on from another screen
The viewport effect marked the selection as centred before the layout
had placed anything, so an arrival at ?node= fitted the whole flow.
It is recorded once the fit has run.
2026-10-05 18:19:38 +02:00
stroblme a677409079 Find a flow's declared inputs and outputs in the global search
/search lists each input and batch output under its qualified message,
and picking one opens the flow on the label's id, which the editor now
treats like a click on the label: it opens the flow's settings.
2026-10-05 18:15:35 +02:00
stroblme 43e777b7c7 Read a key a run lacks as its flow's declared value in "What differs"
Runs from before the declared values were folded into params lack them, so
every such key read as varied next to a newer run with the same value.
2026-10-05 16:34:48 +02:00
stroblme a0b90afe5c Show one seed column when the flow declares a seed input
The run-level seed is that parameter then, so the comparison table drew it
twice and counted it twice in "N of M varied".
2026-10-05 15:11:01 +02:00
stroblme 929933d7c3 Record which inputs a run's caller passed, and mark defaults from it
`Run.passed` (migration d5a3c8e1f246) keeps the names the caller chose, so
the run detail and the comparison table can mark a value as the flow's own
again; rows from before it fall back to the names their params lack. Retries
hand on their parent's list, imports record their entry's.

Also: a cancel of a queued run no longer leaves its id in `_cancelled`, a
linked comparison no longer crashes before the list loads, and the pre-commit
failures are fixed (stale client, `text-[10px]`, worker formatting).
2026-10-05 15:01:33 +02:00
stroblme 7240b7b823 Add the assistant: a chat that drives the MCP tools in-process
A provider set under Settings (OpenAI-compatible; key encrypted in
flow-data/ai.json) turns on an Assistant panel in the sidebar. Each turn runs
the FastMCP tools in-process as the person chatting, streams the rounds over
SSE and keeps one transcript per user. The panel floats on any screen and
shares the editor's lane: a settings panel keeps the top half, the chat the
bottom. The MCP client is now built once, for the endpoint and the chat.
2026-09-16 23:28:23 +02:00
stroblme af18d134a4 Add POST /runs/import for runs executed elsewhere
A history recorded before the move (MLflow experiments) had nowhere to
go, since runs were only made by running a flow. Each entry is stored as
it ended: parameters completed and checked like a submit, cause
"import", empty commit and code digest, metrics and artifacts written
as given, nothing queued or announced. Run.external_id (new migration)
makes a second import a no-op and is an export column so a row joins
back. The request is all or nothing and capped at 200 entries;
Client.import_runs pages a longer history.
2026-09-16 17:01:07 +02:00
stroblme 831bda9c46 Sync only the declaring module before a run; skip git-ignored paths
`fluksio run` walked the working directory and imported every module in
it, which ran a research repository's analysis scripts before the run was
submitted. A flow's origin now records the file and module name that
declared it, and run/sweep import just that, under the same name so the
generated node bodies do not change. A flow with no record still walks,
now skipping what .gitignore covers.

Every code-defined flow reports `flow` changed once on its next sync,
since the origin gained two fields.
2026-09-16 16:55:02 +02:00
stroblme a1141fbd6a Surface a node's health when the engine's floor expires
`node_health` fires once at the transition, while `HEALTH_FLOOR` still
hides the node, and nothing is published when the floor expires 60 s
later. Schedule a per-node re-read for that moment, cancelled when the
node recovers and on socket teardown. Health now also invalidates the
brain graph, whose key is a sibling of the flow's rather than a child,
so Home's fault rings never moved on a health flip at all.
2026-09-14 15:50:33 +02:00
stroblme 57fd3d1b2c Collapse the button-variant fork and the canvas's duplicate work
LoadingButton kept its own copy of buttonVariants, which had drifted past
`brand` and the icon sizes; it imports Button's now, so the Connect /
Re-pair button can show the loader instead of swapping its own label.

DashboardView mounted a second LookProvider inside the one CanvasSurface
already gives it and dropped `touch` on the way, so every widget on a
touch panel read `useLook().touch` as false. The provider is now mounted
once, by whatever owns the canvas root.

useCanvasRoot resolved the palette twice per render — once for the theme
class, once for the tokens — so useDashboardTheme takes the roles the
caller already has.

The bar assertions in widgets.spec.ts are scoped to the Load tile rather
than the page, which held only because the unbound spare bar draws no
rows.
2026-09-14 15:29:40 +02:00
stroblme 94c9260452 Spin the Add remote user button; stop the search prefilter dropping typos
"Add remote user" was the one call site swapping its label instead of
using LoadingButton, so it alone had no spinner.

Global search's couldMatch was a strict subsequence test while cmdk's
scorer steps over one query character for a transposition, so a typo it
would have scored never reached it. The prefilter now carries that one
skip, which recovers every single-transposition query in a sweep of the
index shapes and admits nothing cmdk scores zero.
2026-09-14 14:40:17 +02:00
stroblme 46f9575870 Stop a bound dashboard setting rebuilding every tile
A setting driven by a flow re-renders whatever reads it on every message.
`LookProvider` and `PaletteProvider` built a fresh context value each time,
and `CanvasSurface` called its render prop again, so a value on `theme`,
`palette`, `look`, `background`, `touch` or `locked` reconciled the whole
widget tree and re-ran every chart's `setData`.

The palette is now read as roles once per set of colours, the tokens and
both context values are memoised on it, and the canvas holds the tiles it
built while the callback and the scale stay put. `WidgetBody` is memoised
too: its two props are stable while the document is, so a grid rendered for
its own reasons no longer rebuilds every chart with it.
2026-09-14 14:38:59 +02:00
stroblme 0f040b6e1d Merge branch 'main' of git.stroblme.de:Fluksio/app
# Conflicts:
#	frontend/src/client/sdk.gen.ts
2026-09-07 08:41:45 +02:00
stroblme 23a80db6bb Let the native clipboard through where the canvas has nothing to copy
A bound chord used to be `preventDefault`ed before its handler ran, so the
canvas took every ⌘C and ⌘V whether or not the node clipboard would act —
which is what blocked copying selected text out of the chrome floating over
the canvas. A binding may now return `false` to decline the key; ⌘C does so
when there is a text selection or no node selected, ⌘V when nothing of ours
is stored.
2026-09-06 18:16:17 +02:00
stroblme 6fdc30a4d1 Make lint-backend green, and regenerate the stale client
`wanted_names` never read its `previous` argument — the caller reassigns
`wanted` from the return value — so the parameter goes rather than gaining
an underscore; its one call site and four test calls follow. Sorts the
imports in test_artifacts.py. Both were failing `make lint-backend` at HEAD.

Regenerates the client, which 8c097d9 left behind when it corrected the
revoke_client docstring.
2026-09-06 15:29:35 +02:00
stroblme fa5ce4f694 Give both summaries a modified time, and the dashboard list a footprint
Home's "recently modified" order was a proxy — drafts first, then the
version counter, then the name. Both summaries now carry `updated_at`,
read as the mtime of the working copy: every write in the store commits
immediately, so a file's mtime is its commit time, and `git log -1 --
<path>` costs ~990ms across this instance's 15 documents (it walks the
history back to the last commit touching each one, so it is slowest for
the stalest) against ~1.8ms for the stats. No cache needed.

`DashboardSummary` also carries `footprint`: each widget as its type plus
the placement a panel resolves, which is the whole of what the mosaic
draws. That drops the document fetch per tile and with it the cap of
eight, past which tiles showed a name and nothing else. Verified against
this instance's 8 dashboards: the blocks are identical to what the old
client-side derivation produced.
2026-09-06 15:26:48 +02:00
stroblme 8c097d9222 Make revoking an agent and locking a dashboard actually revoke and lock
An MCP access token is a stateless JWT good until it expires, so deleting
the client row revoked nothing already handed out — on the MCP endpoint or
on the REST API, which takes the same token directly. Both doors now look
the client up by the `client_id` the token has always carried, so tokens
already in circulation are held to it too.

A dashboard's `locked` setting stopped the client drawing a control and
nothing else; the server took a publish from a panel showing it anyway. It
now bounds the panel's write scope, resolved live where a flow drives the
flag, exactly as the client resolves it. Reads are untouched — read-only is
not blind — and so is a querying chart's request, which is how that tile
reads rather than something anyone touched.
2026-09-06 15:24:53 +02:00
stroblme c9172adfaa Keep a flow's runs when the flow goes
A run record is a record of what ran, so deleting a flow no longer sweeps its
history: `_forget_runs` is gone from the flow-delete path, and `DELETE /runs/{id}`
is the only thing that removes a run one at a time. The in-flight guard stays —
that is about work, not history.

`DELETE /runs?flow=` is the counterpart to the list's flow filter and what a
reseed needs, sharing the four statements with the single-run delete and
refusing the same way while a run of that flow is still going.

A run whose flow is gone reads as one: `useFlowInputs` reports the 404 rather
than an empty declaration set, so the run page says "flow deleted", explains it,
and disables Retry, which the route would refuse anyway.
2026-09-06 14:53:23 +02:00
stroblme 6ad249bfb3 Let the search reach past the twentieth of a kind, and unbreak two specs
The global search capped each category before cmdk had matched anything, so
nothing past the twentieth node or widget could be found at all — this
instance has 28 nodes and 97 widgets. The cap now trims the candidates the
query could reach rather than the raw index.

The admin teardown asked /users/ for limit=1000, which the route stopped
accepting when its bounds went in; a 422 body has no `data` to iterate, so the
hook threw and took the tests it was attributed to with it. It asks for the
500 the route allows.

And `submit` folds every declared initial into a run's params, so an input the
run never passed can no longer read as the flow's own. That assertion is gone;
what the panel does show — the value the run actually started from, passed or
not — is what the test checks.
2026-09-06 14:46:59 +02:00
stroblme fb98b04c46 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.
2026-09-06 14:24:28 +02:00
stroblme 10fe2fd760 A buttons widget: a grid of presses on one message
Several stateless instructions where a single button is one — six presets are
otherwise six tiles to place, six titles to read, and the message they share
repeated six times. Nothing is read back, as for a single button: what these
send is an instruction, and the last one sent is not a state to draw.

Auto-fit columns rather than a configured count: a tile is resized in the
editor and scaled again to whatever panel it hangs on, so how many fit is not
something the document can know.

The editor reads `cfg.buttons` raw rather than through `buttonsOf`, which
drops the blanks — a row being typed into is blank until the first keystroke.

Also carried along by the hooks: the generated SDK was stale (it had no
`RunsReadMetricNames`) and one pre-existing block in `test_panels.py` was
unformatted.
2026-09-04 08:44:16 +02:00