Close the nine open SDK tasks: one engine per directory, a tabbed dashboard, re-pairing, run recovery
Docs / docs (push) Successful in 27s
Playwright Tests / test-playwright (1, 2) (push) Failing after 17s
Playwright Tests / test-playwright (2, 2) (push) Failing after 12s
pre-commit / pre-commit (push) Failing after 1m59s
Test Backend / test-backend (push) Failing after 2m30s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m19s
Docs / docs (push) Successful in 27s
Playwright Tests / test-playwright (1, 2) (push) Failing after 17s
Playwright Tests / test-playwright (2, 2) (push) Failing after 12s
pre-commit / pre-commit (push) Failing after 1m59s
Test Backend / test-backend (push) Failing after 2m30s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m19s
serve: refuse a second engine for one data directory whatever port it was asked for, using the pidfile and a token this directory signed. The check runs before the database is touched and before the credential is written, which is what left every later CLI call pointing at a dead port. The terminal dashboard is three tabs (Overview, Runs, Logs) with the toolbar following the focused pane, the engine's output goes to serve.log rather than down a pipe, and closing the screen stops both reader threads so the prompt comes back. It adopts a running engine on every start, so stop/start and restart work on one it did not start, and a stop waits for the process to be gone before the next start. Enrolment reports itself in the modal. enroll: a new claim code replaces the pairing instead of being refused. The code is redeemed before anything is written, mappings to a portal being left are cleared, and a running engine redials when the stored enrolment changes. runs: an engine re-queues the runs left `queued` by the one before it, and `fluksio retry <id>` / `retry --group <sweep>` submits an interrupted run again with the same inputs and group, recorded through Run.parent_id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U9BoNGq6V9MdRWAte7JBuC
This commit is contained in:
+75
-17
@@ -808,36 +808,49 @@ def _status_screen(client: Client) -> Any:
|
||||
parts.append(
|
||||
table if flows else Text("No flows yet. `fluksio sync` uploads yours.", "dim")
|
||||
)
|
||||
# Tables rather than padded strings: a flow named longer than the column
|
||||
# used to push everything after it out of line.
|
||||
if runs:
|
||||
parts += ["", Text("recent runs", style="dim")]
|
||||
recent = Table(box=None, pad_edge=False, show_header=False)
|
||||
recent.add_column("")
|
||||
# Minimums rather than widths: the column keeps its shape when every
|
||||
# value is short and grows for the one that is not.
|
||||
recent.add_column("", min_width=9)
|
||||
recent.add_column("", min_width=14)
|
||||
recent.add_column("", justify="right", min_width=7)
|
||||
recent.add_column("", justify="right", min_width=8)
|
||||
for row in runs:
|
||||
state = str(row.get("status", ""))
|
||||
parts.append(
|
||||
Text(f" {str(row.get('id', ''))[-8:]} ")
|
||||
+ Text(
|
||||
f"{state:<9}",
|
||||
recent.add_row(
|
||||
f" {str(row.get('id', ''))[-8:]}",
|
||||
Text(
|
||||
state,
|
||||
style={"ok": "green", "cached": "cyan", "running": "cyan"}.get(
|
||||
state, "red" if state == "error" else "yellow"
|
||||
),
|
||||
)
|
||||
+ Text(
|
||||
f"{str(row.get('flow', '')):<16}"
|
||||
f"{_dur(row.get('duration_ms')):>8}"
|
||||
f" {_ago(row.get('finished_at') or row.get('created_at')):>9}",
|
||||
style="dim",
|
||||
)
|
||||
),
|
||||
Text(str(row.get("flow", "")), style="dim"),
|
||||
Text(_dur(row.get("duration_ms")), style="dim"),
|
||||
Text(
|
||||
_ago(row.get("finished_at") or row.get("created_at")), style="dim"
|
||||
),
|
||||
)
|
||||
parts += ["", Text("recent runs", style="dim"), recent]
|
||||
if failures:
|
||||
parts += ["", Text("recent failures", style="dim")]
|
||||
broken = Table(box=None, pad_edge=False, show_header=False)
|
||||
broken.add_column("")
|
||||
broken.add_column("", justify="right")
|
||||
broken.add_column("")
|
||||
for event in failures:
|
||||
where = " ".join(
|
||||
str(event.get(key, "")) for key in ("flow", "node") if event.get(key)
|
||||
)
|
||||
parts.append(
|
||||
Text(f" {where} ", style="red")
|
||||
+ Text(f"{_ago(event.get('ts')):>9} ", style="dim")
|
||||
+ Text(str(event.get("detail", ""))[:100], style="dim")
|
||||
broken.add_row(
|
||||
Text(f" {where}", style="red"),
|
||||
Text(_ago(event.get("ts")), style="dim"),
|
||||
Text(str(event.get("detail", ""))[:100], style="dim"),
|
||||
)
|
||||
parts += ["", Text("recent failures", style="dim"), broken]
|
||||
return Group(*parts)
|
||||
|
||||
|
||||
@@ -986,6 +999,38 @@ def cmd_runs(args: argparse.Namespace) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
#: The most runs of one group a retry looks at, which is the list route's own
|
||||
#: ceiling. ponytail: a sweep larger than this needs paging, not a bigger number.
|
||||
MAX_GROUP = 500
|
||||
|
||||
#: What a retry leaves alone. Everything else in a group — error, cancelled,
|
||||
#: abandoned — is what "the ones that did not make it" means.
|
||||
KEPT = frozenset({"ok", "cached", "queued", "running"})
|
||||
|
||||
|
||||
def cmd_retry(args: argparse.Namespace) -> int:
|
||||
"""Run the same thing again, one run or a group's unfinished ones."""
|
||||
try:
|
||||
with _client_for(args) as client:
|
||||
ids = list(args.run_id)
|
||||
if args.group:
|
||||
ids += [
|
||||
str(row["id"])
|
||||
for row in client.runs(limit=MAX_GROUP, group=args.group)
|
||||
if str(row["status"]) not in KEPT
|
||||
]
|
||||
if not ids:
|
||||
return _fail("nothing to retry; name a run or a group with runs in it")
|
||||
for run_id in ids:
|
||||
made = client.retry(run_id)
|
||||
_say(f"{made['id']} queued (retry of {run_id})")
|
||||
except (SyncError, ApiError) as exc:
|
||||
return _fail(str(exc))
|
||||
except httpx.HTTPError as exc:
|
||||
return _unreachable(exc)
|
||||
return 0
|
||||
|
||||
|
||||
def _artifacts(client: Client, args: argparse.Namespace) -> int:
|
||||
"""List a run's files, or write one of them here."""
|
||||
handle = RunHandle(client, args.run_id, client.run(args.run_id))
|
||||
@@ -1401,6 +1446,19 @@ def add_parsers(subparsers: Any) -> None:
|
||||
with_engine(parser, local=True)
|
||||
parser.set_defaults(func=cmd_runs)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
"retry", help="run something again: one run, or a group's unfinished ones"
|
||||
)
|
||||
parser.add_argument("run_id", nargs="*", help="the runs to retry")
|
||||
parser.add_argument(
|
||||
"--group",
|
||||
default="",
|
||||
metavar="ID",
|
||||
help="every run of this sweep that did not end ok",
|
||||
)
|
||||
with_engine(parser)
|
||||
parser.set_defaults(func=cmd_retry)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
"artifacts", help="the files a run produced; name one to download it"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user