New run: start a run from the app, on the working copy

The site promises simulated inputs and mocked sensor values, and nothing in
the app was that. A run already is: the values are the caller's, the state is
the run's own namespace, and nothing it computes reaches the live flow. What
was missing was a screen to do it from, and the draft flag being honoured.

`/runs/new` is a flow, a field per declared input, a seed and Run; `/runs`
stays the log. A comma-separated list in a number field expands into the grid
`fluksio sweep --param` builds and goes to the sweep route, so launching one
no longer needs a terminal. Only numbers split: a comma in a string is
content, and one in JSON is syntax.

`RunCreate.draft` was validated at submit and dropped before the run
executed, so "try the working copy" ran the published one. `Run.draft` is a
column now, the driver reads the same copy the submit checked, and a retry
carries it. `FlowSummary.mode` came with it so the rail can say which flows
are batch before one is picked.

Also here: a Retry button on a finished run, which the route has always had
and the UI never did, and parameter cells truncated to their column with the
full value on hover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TTfoK82awm8wvxXhHz3XF
This commit is contained in:
2026-09-02 15:10:24 +02:00
co-authored by Claude Opus 5
parent d471614e6a
commit 5b122341d5
20 changed files with 926 additions and 91 deletions
+11 -2
View File
@@ -906,6 +906,7 @@ class RunService:
group_id=group_id,
cause=cause,
no_cache=no_cache,
draft=draft,
status="queued",
labels=required_labels(flow),
needs=required_resources(flow),
@@ -973,6 +974,10 @@ class RunService:
inputs, the same group, so a sweep is completed rather than repeated.
The stage cache is what makes it cheap — the nodes that finished are
restored rather than run again.
A run of the working copy retries as one, which means whatever the
draft is now rather than what it was — there is no older draft to go
back to, and the point of retrying a draft run is the code on disk.
"""
with Session(db_engine) as session:
run = session.get(Run, run_id)
@@ -989,6 +994,7 @@ class RunService:
cause="retry",
actor=actor,
no_cache=source.no_cache,
draft=source.draft,
parent_id=source.id,
)
@@ -1203,17 +1209,20 @@ class RunService:
sink = MetricSink(run_id)
try:
flow = self.controller.store.read_flow(run.flow)
flow = self.controller.store.read_flow(run.flow, draft=run.draft)
# Read again, now that it is this run's turn: a sweep queues every
# run at once, and the code on disk is free to move in the hours
# before the last of them starts. What the record must name is the
# state that ran, not the state that was submitted.
# state that ran, not the state that was submitted. A draft that
# was published meanwhile reads as the published copy, which is
# the same document under a different name.
digests = node_digests(flow)
run.code_digest = self._restamp(run, run_digest(flow))
state = self._state_factory(f"{RUN_NAMESPACE}:{run_id}")
pipeline = self.controller.build_run_pipeline(
flow,
state=state,
draft=run.draft,
observer=observe,
emission_observer=sink.handle,
run=RunContext(
+2
View File
@@ -355,6 +355,8 @@ class MessageHistory(BaseModel):
class FlowSummary(BaseModel):
name: str
title: str = ""
#: Whether running it means one finite execution or leaving it running.
mode: Literal["live", "batch"] = "live"
node_count: int = 0
error_count: int = 0
has_draft: bool = False