Commit Graph
29 Commits
Author SHA1 Message Date
stroblme c8f14b6913 Back to development
Docs / docs (push) Successful in 23s
Playwright Tests / test-playwright (1, 2) (push) Failing after 23s
Playwright Tests / test-playwright (2, 2) (push) Failing after 11s
pre-commit / pre-commit (push) Failing after 2m29s
Test Backend / test-backend (push) Failing after 2m40s
Compose Smoke Test / test-compose (push) Failing after 17s
Playwright Tests / merge-reports (push) Failing after 2m38s
2026-09-02 22:48:40 +02:00
stroblme 77dd87f1b9 Release v0.1.5
Docs / docs (push) Canceled after 0s
Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Test Backend / test-backend (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Publish / publish (push) Successful in 54s
2026-09-02 22:47:33 +02:00
stroblmeandClaude Opus 5 f370601aec Draw curves in the terminal, and stop waiting five seconds to hear
Docs / docs (push) Successful in 27s
Playwright Tests / test-playwright (1, 2) (push) Failing after 18m28s
Playwright Tests / test-playwright (2, 2) (push) Successful in 5m11s
pre-commit / pre-commit (push) Failing after 2m2s
Test Backend / test-backend (push) Failing after 2m32s
Compose Smoke Test / test-compose (push) Successful in 32s
Playwright Tests / merge-reports (push) Successful in 5m50s
`space` ticks runs on the serve dashboard's table and `enter` compares them:
one metric in braille, five distinct hues, and the table of what actually
differs under it. Both halves are routes that already existed — the browser's
own comparison endpoint for the curves, the runs export for the table, whose
input columns are filtered to the ones that vary.

The palette is hue rather than the web's lightness ramp on purpose: five steps
of one brand hue collapse to a single colour on a 16-colour tty.

The screen also subscribes to the engine's event bus over the same websocket a
browser uses, so a run that starts and finishes inside a tick is seen rather
than only recorded. `a` lists what a run left behind and fetches it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SmyLMSqcJQ8tUL2qLj21s
2026-08-30 12:51:08 +02:00
stroblmeandClaude Opus 5 45cc7504e1 Notify a phone that has this installation installed
A `webpush` alert channel, and the PWA it needs to arrive. The payload is
encrypted to the subscription (RFC 8291) and the request signed with this
installation's own keypair (RFC 8292), both over `http-ece` — `pywebpush`
does the same in one call but brings `requests` and `aiohttp` with it, two
HTTP stacks beside httpx on a machine that may be a Raspberry Pi.

The manifest and the worker are hand-written rather than `vite-plugin-pwa`:
there is nothing worth precaching when the page carrying the credential is
`no-store`, so the worker handles `push` and `notificationclick` and nothing
else. `registration.scope` is the app's root in both places it runs, which is
why the payload carries no URL.

A run finishing in error is the first event worth waking someone for; `ok`
and `cancelled` describe to nothing, so a nightly batch that works stays
quiet. The events were already on the bus — only the filter changed.

`WEBPUSH_FILE` is a derived path, so the keypair lands on the data volume
with the alerts beside it. Off it, a rebuild would silently stop every phone
being notified: the key they subscribed against would be gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014EbeFPm6WNC3YD9vrqqT3a
2026-08-30 12:12:37 +02:00
stroblmeandClaude Opus 5 a4ea1dd0d6 Open a dashboard when serve is run at a terminal
`fluksio serve` printed a log stream and nothing else, so watching an engine
meant a second terminal running `status --watch`, and stopping or pairing it
meant a third. At a terminal it now opens a dashboard: the health and flow
overview `status` draws, the recent runs as a table, and the engine's own
output in a pane below — which is what the earlier decision against this
was protecting, and it is still all there.

The engine is a child process running `serve --plain`, not a thread, so it
outlives the dashboard: q leaves it running and says so, s and r stop and
restart it, c cancels the selected run and e pairs with a portal. An engine
already serving this directory is adopted rather than duplicated, and it can
be stopped from here only because the pidfile and the token together prove
it is this installation's.

`--plain` and no terminal both keep the old behaviour, which is what the
container and CI run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Hra4ndWMCLU5F3KjUuVAc
2026-08-29 14:21:13 +02:00
stroblmeandClaude Opus 5 22c682e505 Put the deployment-only dependencies behind a server extra
A data-science environment installing fluksio waited for lxml, aiohttp and
the rest of a connector stack it has nothing to talk to. Outbound mail,
error reporting and the MQTT and InfluxDB clients moved to
`fluksio[server]`, which the image installs; each import is guarded and
names the extra. `tenacity` had no import site at all and is gone.

23 fewer packages and the compiled ones among them — a bare `pip install
fluksio` still serves, runs every python node, and registers the mqtt and
influxdb node types, which only need the library when one is actually
built. sentry-sdk arrives anyway underneath `fastapi[standard]`; what
changed there is that nothing of ours requires it.

The dev environment keeps every extra: the suite exercises the connectors
and strict mypy checks their call sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Hra4ndWMCLU5F3KjUuVAc
2026-08-29 14:06:50 +02:00
stroblmeandClaude Opus 5 dfa0d44589 Back to development: 0.1.4+dev
A tag is the only thing that is a version. Between two of them the tree is a
build nobody released, and until now it went on claiming to *be* the release
it came after — which is how 0.1.4 came to name both the build before the
per-node digest and the build after, with the mismatch messages unable to tell
them apart.

`+dev` is a local version: it says "0.1.4, plus changes", it does not have to
guess what the next release will be numbered, and PyPI refuses one, so a dev
build cannot be published by accident. `scripts/release.sh` in the workspace
root writes it after each tag; this is that commit, by hand, for the release
that predates it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9Hdrmf2cwNABCnE5x9UJa
2026-08-28 11:01:25 +02:00
stroblme 9dc1fe0a84 Release v0.1.4
Docs / docs (push) Successful in 33s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m8s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m29s
pre-commit / pre-commit (push) Failing after 2m19s
Test Backend / test-backend (push) Successful in 3m9s
Compose Smoke Test / test-compose (push) Successful in 43s
Publish / publish (push) Successful in 54s
Playwright Tests / merge-reports (push) Successful in 1m12s
2026-08-27 23:16:19 +02:00
stroblmeandClaude Opus 5 f8a7dfb101 A sync that changed nothing says so again
Docs / docs (push) Successful in 32s
Playwright Tests / test-playwright (1, 2) (push) Successful in 2m58s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m9s
pre-commit / pre-commit (push) Failing after 2m23s
Test Backend / test-backend (push) Successful in 3m17s
Playwright Tests / merge-reports (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 28s
Comparing the per-node digest against an engine that does not record one is
comparing against nothing, and reporting every node as changed on every sync
for ever — which is what a client newer than its engine did, since `NodeDef`
drops fields it has never heard of. A node is named now only when both sides
carry a digest, so a no-op sync is `unchanged` again and the signal one syncs
for is back.

That silence had also been the only sign of the mismatch, so sync now names
it: one line saying the engine stored no record of what a node's code reaches,
with both versions in it and what to run. Bumped to 0.1.6 — the digest changed
the stored document's shape, and a version that does not move makes two
different engines indistinguishable, which is the thing it was made
load-bearing for a day ago.

`— draft` was printed whenever there was simply nothing to publish, which
reads as work left unfinished. It is said only when a draft is genuinely
there, and `— published` when one was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9Hdrmf2cwNABCnE5x9UJa
2026-08-27 22:58:31 +02:00
stroblmeandClaude Opus 5 4479eeb726 Follow a record into its fields, name the metrics, name the version
Docs / docs (push) Successful in 38s
Playwright Tests / test-playwright (1, 2) (push) Successful in 2m56s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m7s
pre-commit / pre-commit (push) Failing after 2m17s
Test Backend / test-backend (push) Successful in 2m54s
Compose Smoke Test / test-compose (push) Successful in 44s
Playwright Tests / merge-reports (push) Successful in 1m17s
Three things the first export pass got wrong for a real study.

**Dotted paths.** A node returns a record, not a scalar — the numbers arrive
inside `final_metrics` — so `--metrics final_metrics.train_loss` yielded an
empty column and `--metrics final_metrics` yielded the whole record in one
cell. Both sides of the wide table now take dotted paths, and the defaults
reach the same depth: every number a result carries is a column named by its
path, and inputs are compared leaf by leaf, so two configurations differing in
one field give that field as the axis rather than two blobs that are merely
not equal. Lists stay whole — a curve belongs in the long table.

**`--list`.** Metric names are flow-qualified, so `--name train_loss` matched
nothing and said only that. `fluksio export metrics --list` prints the names
the selection carries, and an empty export made with `--name` points at it.

**A version to compare.** The CLI ships ahead of the engine and a stale one
answered a flat 404 with nothing anywhere in the API to tell how old it was.
The engine reports `version` on `/observability/summary`, `fluksio status`
prints it, and a 404 from export now names both versions — or says "older"
when the field itself predates the engine. Bumped to 0.1.5, which is what
makes the number worth reading.

Also formats `flow/metrics.py`, which had been committed unformatted and was
the last `ruff format --check` failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9Hdrmf2cwNABCnE5x9UJa
2026-08-27 20:42:19 +02:00
stroblmeandClaude Opus 5 51464941ac Export runs and their curves as tables an analysis reads
Docs / docs (push) Successful in 35s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m11s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m17s
pre-commit / pre-commit (push) Failing after 2m44s
Test Backend / test-backend (push) Successful in 3m0s
Compose Smoke Test / test-compose (push) Successful in 41s
Playwright Tests / merge-reports (push) Successful in 8m14s
`fluksio export metrics` is the long table — a row per run, metric and step —
and `fluksio export runs` the wide one, a row per run with the inputs that
*vary* across the selection as columns beside its final numbers, status,
duration and the commit and digest of the code it ran. Both carry the run id
on every row, which is the join back to the run page and what makes an
exported file auditable. `Client.export_metrics`/`export_runs` answer the same
rows to a notebook.

The engine streams csv or jsonl from two routes declared above `/{run_id}`;
parquet is a client-side conversion behind the new `fluksio[parquet]` extra,
so nobody pays for pyarrow who does not want dtypes kept. The long export
reads each run through `_series`, so a cached node's curve comes with it, and
`--stride` thins each series rather than the concatenation of all of them.

Two things they needed on the way: `GET /runs` takes `?since=` and `?before=`,
so a long history pages by the last row's own timestamp instead of an offset
that shifts under it; and a read that reaches no engine now says so in half a
second rather than seven, because `runs`, `flavors`, `export` and an unwatched
`status` pass `retries=0`. Everything that submits keeps them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9Hdrmf2cwNABCnE5x9UJa
2026-08-27 17:43:30 +02:00
stroblmeandClaude Opus 5 1a9753fa9d Let a worker say what machine it is
A worker reported its labels and nothing about the machine behind them, so the
engine could route a node to a GPU box but not tell whether that box had a GPU
free. Inventory — cores, GPUs, memory — now arrives with the hello frame, and
the run frame carries back what the engine allocated for that call.

Which is protocol 2 on both ends. GPUs are never probed: asking a vendor tool
would make the one dependency two, so a GPU is what the batch job says it was
given or what --gpus says. A worker that reports nothing still attaches and is
scheduled by its label alone.

Two things a job scheduler needs: --max-idle stops a worker started for one job
rather than letting it hold its allocation to the walltime, and a refusal is now
fatal instead of a reconnect loop that reads as a hang in a job's log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6HeySA27EkGANZN95QySW
2026-08-27 08:29:35 +02:00
stroblmeandClaude Opus 5 608d30d884 Let a node say how much of the machine it takes
Five concurrent training nodes, each sizing its thread pool to every core,
left the engine's own event loop unscheduled: the API stopped answering
within 10 s and every client died. The same shape on a GPU deadlocked a run
for 21 minutes at 0% utilisation with nothing failing and nothing to read --
it just sat in `running`.

@node(resources={"cpus": 2}) is the declaration. The engine holds that much
for the length of the execution, so more of them than the machine has room
for wait their turn rather than oversubscribing it, and a `gpus` node holds
its card exclusively. FLOW_CPUS defaults to every core but two, and those two
are what keeps the engine answering.

Because a thread cap is read when the process imports the library, a warm
worker cannot be told a different one -- so an environment gets a pool of its
own and nodes deriving the same one share it, rather than paying a cold start
per call on exactly the nodes whose imports are slowest. XLA_FLAGS is never
derived: it is a composed, version-dependent string, so it travels in
resources.env where it is visible.

A node that declares nothing is not accounted for and behaves as it always
did -- it just gets FLOW_CPUS/FLOW_MAX_WORKERS as a thread cap, which is the
half of this that fixes the reported incident without anybody declaring
anything. An operator who set OMP_NUM_THREADS themselves still wins.

Resources are claimed strictly before a worker slot, so the two blocking
waits cannot deadlock. A node queued for them publishes node_queued and shows
on GET /workers/resources, because waiting and hanging looked identical.

Accounted, not enforced: no cgroups, no rlimits. Scheduling across machines,
flavours and enforcement are the next steps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 21:36:59 +02:00
stroblme 2cf45e4f39 Release v0.1.3
Docs / docs (push) Successful in 20s
Playwright Tests / test-playwright (1, 2) (push) Successful in 2m38s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m40s
pre-commit / pre-commit (push) Failing after 2m44s
Test Backend / test-backend (push) Successful in 2m15s
Compose Smoke Test / test-compose (push) Successful in 31s
Publish / publish (push) Successful in 50s
Playwright Tests / merge-reports (push) Successful in 1m31s
2026-08-26 16:05:04 +02:00
stroblmeandClaude Opus 5 180da3d640 Stop paying five Redis round trips and a global lock per message
The engine was I/O-bound on its own state backend. `RedisState.lock()` is one
key — `pipeline:_lock` — for the whole process, taken five times a message at
two round trips each, and every cascade and every node read queued behind it.
Inside it, reading a node's inputs was three round trips per input (an EXISTS
for `in`, then EXISTS and GET for the value), writing was two updates that a
single transaction already gives, and the version counters went one INCR at a
time.

Replaced with the atomic command that was always available: `get_present` is
one MGET and tells a missing key from one holding null, so the lock it used to
be read under bought nothing; value and timestamp land in one `update`, which
is a MULTI/EXEC; `increment_multi` pipelines the counters. `values()` — what
every websocket snapshot calls — is two reads whatever the message count
instead of two per message.

Beside that: every webhook did its blocking XADD on the asyncio event loop
(MQTT already used `to_thread`); the per-execution `NodeOutcome` was built and
validated even with no run watching; `_minute` built a tz-aware datetime per
event on the loop thread to key a dict, and now keys on an int; `move_due`
promoted delayed items one round trip each, every second; `FLOW_MAX_CASCADES`
makes the in-flight ceiling a setting rather than a constant.

`orjson` replaces stdlib json where a message pays for it — state, the
journal, the engine side of the worker pipe. `fluksio-worker` stays
dependency-free, and the run-cache digest stays on stdlib so no stored key is
invalidated. A non-finite number now stores as `null` rather than the bare
`NaN` that was never JSON.

Measured with `scripts/bench_engine.py` against a real Redis, 200 messages:
a five-node chain went from 43.9 to 103.1 msg/s with p50 latency 2110ms →
782ms and p95 3913ms → 1439ms; one source into twenty consumers went from 5.4
to 33.7 msg/s. In memory, twenty consumers went from 187 to 448 msg/s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BpfSinyCBfjuieikyfMPbf
2026-08-26 10:12:25 +02:00
stroblme aa2bd1e665 Release v0.1.2
Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Failing after 2m11s
Playwright Tests / test-playwright (2, 2) (push) Failing after 1m46s
pre-commit / pre-commit (push) Failing after 2m52s
Test Backend / test-backend (push) Successful in 2m21s
Compose Smoke Test / test-compose (push) Successful in 33s
Publish / publish (push) Successful in 49s
Playwright Tests / merge-reports (push) Failing after 1m11s
2026-08-25 12:54:41 +02:00
stroblmeandClaude Opus 5 4941c2787c Add fluksio status, and ask for a run's parameters at a terminal
Two halves of the same gap: the CLI could start work but not show you any.

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019V5bsYGNxcgPs4xXmTPx69
2026-08-25 07:42:09 +02:00
stroblme bf6cad45ad Release v0.1.1
Docs / docs (push) Successful in 20s
Playwright Tests / test-playwright (1, 2) (push) Failing after 33s
Playwright Tests / test-playwright (2, 2) (push) Failing after 19s
pre-commit / pre-commit (push) Successful in 2m47s
Test Backend / test-backend (push) Successful in 2m7s
Compose Smoke Test / test-compose (push) Failing after 20s
Publish / publish (push) Successful in 48s
Playwright Tests / merge-reports (push) Failing after 1m2s
2026-08-24 21:33:18 +02:00
stroblmeandClaude Opus 5 d4a9406c51 Fix the CI gates: Python 3.13, concurrency groups, hook violations
Docs / docs (push) Successful in 49s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m11s
Playwright Tests / test-playwright (2, 2) (push) Failing after 23s
pre-commit / pre-commit (push) Successful in 3m2s
Test Backend / test-backend (push) Successful in 2m22s
Compose Smoke Test / test-compose (push) Failing after 22s
Playwright Tests / merge-reports (push) Canceled after 1s
The gates have never gone green on the new runners. Three separate reasons:

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 14:55:59 +02:00
stroblmeandClaude Opus 5 10b0ba9e49 Add the fluksio CLI: serve, enroll, worker
`pip install fluksio && fluksio serve` on a machine with no Docker, no
database and no configuration — which is the case this is for: a node on
a cluster where ports cannot be opened. It makes its data directory, its
key and an admin account, prints the password once, and serves. Pairing
is `fluksio enroll <code> --portal …`, doing what the Settings screen
does through the same function, before the engine starts and without one
running — a machine nobody can route to has no browser pointed at it
either. The portal serves the dashboard, so nothing is served here.

Two things had to give way. `fastapi[standard]` pulls a cloud CLI that
wants sentry-sdk 2.x while we pinned below it — no pip resolution
existed, so the pin is lifted, which the comment beside it had been
waiting for and which also lets the Python cap go. And `uv` is now a
dependency rather than something to find on PATH: the Modules screen is
how a data scientist installs torch, and it was quietly falling back to
the engine's own interpreter.

The CLI imports nothing from the engine before it has set DATA_DIR — the
settings are built on the first import of core.config, and reaching it
early put the database in the working directory. There is a test for
that now, because the failure is silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 22:51:13 +02:00
stroblmeandClaude Opus 5 2c369ac75f Split the worker into a distribution of its own
A cluster or GPU host installs `pip install fluksio-worker` and gets the
agent and the runner, not psycopg, numpy and the MCP SDK. The engine
depends on it as a workspace member, so the file it launches node code
with is the same file a remote worker runs — which is what keeps a node
unable to tell the difference. Copying the two files by hand still works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 21:54:10 +02:00
stroblmeandClaude Opus 5 60d7ec81c0 Rename the import package app to fluksio
A wheel whose top-level module is `app` collides with anything else in a
user's venv, so the package that is about to be published takes the name
it is published under. Only the Python package moves; the repo, the
Docker WORKDIR and the compose project keep theirs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 21:48:05 +02:00
Melvin StroblandClaude Fable 5 8d82d6c4ec Let agents drive the flow API over MCP
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
Test Backend / test-backend (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
The engine now speaks MCP at /mcp, with a built-in OAuth 2.1 authorization
server in front of it: an agent registers itself, sends a human to the browser
to approve it, and exchanges the resulting code for a token. PKCE is required,
codes are single-use and stored only as hashes, the browser is redirected to
the URI that was registered rather than the one asked for, and refresh tokens
rotate so that replaying a spent one revokes the whole line.

Twenty tools cover reading, building, publishing and running flows, and each
one calls the same REST endpoint the dashboard calls, in-process, carrying the
caller's own token. That keeps one description of what a flow is and what may
be done to it — validation, the draft/publish split, the version check — and
means an agent can do nothing a person could not do in the browser.

Agent tokens are RS256 with a keypair of their own rather than the secret that
signs browser sessions, so deleting the key withdraws every agent without
logging anyone out, and deps.decode_token grew the branch that trusting a
second issuer will need when the hosted login arrives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 00:22:41 +02:00
Melvin StroblandClaude Fable 5 06a4506767 Add the flow API: typed messages, git-backed store, REST and live events
Makes the flow engine reachable from the API, which is what M3 needs before
any of it can reach the browser.

- app/flow is a package now; the prototype's watch-dir scripts and the
  matplotlib/networkx visualiser are gone with their dependencies.
- Messages carry a serializable dtype instead of a live Python type, and a
  port name, so the graph can speak qualified names while node functions keep
  local arguments. Redis state is JSON, not pickle.
- Message names are namespaced per flow ("heating.temp"); a bare name resolves
  to its own flow, a dotted one crosses flows.
- Several nodes may provide the same message: producers are a list, so fan-in
  is a real edge instead of a silently dropped one.
- Flows are stored as flow.json plus node sources in a git repository, one
  commit per save, with identical saves skipped so autosave stays quiet.
- Node failures are isolated and reported per node; validate() returns cycles
  and unconnected inputs instead of raising deep in a run.
- Credentials live in an encrypted store and are referenced as {"$secret": …}.
- Engine events reach websocket clients through a bus, so values, node status
  and execution show up live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WzrvW7rjQbynnhF6pxh6i
2026-08-15 17:23:36 +02:00
Melvin Strobl 3cb16958ba croniter
Signed-off-by: Melvin Strobl <lc3267@kit.edu>
2026-02-15 10:43:02 +01:00
Melvin Strobl 77ad40059b influx
Signed-off-by: Melvin Strobl <lc3267@kit.edu>
2026-02-08 10:53:50 +01:00
Melvin Strobl 75b8c0ecf6 gc
Signed-off-by: Melvin Strobl <lc3267@kit.edu>
2026-02-06 19:44:48 +01:00
Melvin Strobl 57d2304d3c up 2026-02-06 10:43:19 +01:00
Melvin Strobl 2cd8e8f634 initial 2026-02-03 21:43:09 +01:00