Say when a run finished and when a failure happened

`fluksio status` listed recent runs and recent failures with no time on
them, so a red line said nothing about whether it was from a minute ago or
last week. Both carry an age now, spelled the way a duration is, and the
runs listing gained one too. The fields were already on the wire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Hra4ndWMCLU5F3KjUuVAc
This commit is contained in:
2026-08-29 13:55:52 +02:00
co-authored by Claude Opus 5
parent 8bd30db016
commit 743432205e
2 changed files with 49 additions and 5 deletions
+31 -5
View File
@@ -19,6 +19,7 @@ import sys
import time
from collections.abc import Callable, Iterable, Iterator
from contextlib import contextmanager, nullcontext
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
@@ -804,7 +805,8 @@ def _status_screen(client: Client) -> Any:
)
+ Text(
f"{str(row.get('flow', '')):<16}"
f"{(row.get('duration_ms') or 0) / 1000:7.1f}s",
f"{(row.get('duration_ms') or 0) / 1000:7.1f}s"
f" {_ago(row.get('finished_at') or row.get('created_at')):>9}",
style="dim",
)
)
@@ -816,6 +818,7 @@ def _status_screen(client: Client) -> Any:
)
parts.append(
Text(f" {where} ", style="red")
+ Text(f"{_ago(event.get('ts')):>9} ", style="dim")
+ Text(str(event.get("detail", ""))[:100], style="dim")
)
return Group(*parts)
@@ -857,9 +860,9 @@ def cmd_status(args: argparse.Namespace) -> int:
#: value is read.
PARAMS_WIDTH = 80
#: What the columns before the inputs take: the id, status, flow, duration and
#: stamp, with their spacing.
LISTING_WIDTH = 84
#: What the columns before the inputs take: the id, status, flow, duration,
#: age and stamp, with their spacing.
LISTING_WIDTH = 95
def _declared(client: Client, flows: Iterable[str]) -> dict[str, dict[str, Any]]:
@@ -883,6 +886,28 @@ def _declared(client: Client, flows: Iterable[str]) -> dict[str, dict[str, Any]]
return known
def _ago(stamp: Any) -> str:
"""How long ago something happened, in the notation the screens use.
A duration and an age read together, so they are spelled the same way:
seconds, then minutes, hours, days.
"""
if not stamp:
return ""
try:
then = datetime.fromisoformat(str(stamp))
except ValueError:
return ""
if then.tzinfo is None:
# Everything the engine records is UTC; only some spellings say so.
then = then.replace(tzinfo=UTC)
seconds = max((datetime.now(UTC) - then).total_seconds(), 0)
for span, unit in ((86400, "d"), (3600, "h"), (60, "min")):
if seconds >= span:
return f"{seconds / span:.0f}{unit} ago"
return f"{seconds:.0f}s ago"
def _stamp(row: dict[str, Any]) -> str:
"""What code a run ran: the commit, whether it was dirty, and the digest.
@@ -925,7 +950,8 @@ def cmd_runs(args: argparse.Namespace) -> int:
params = params[: room - 3] + "..."
_say(
f"{row['id']} {_status(row['status'], 9)} {row['flow']:<16} "
f"{row['duration_ms'] / 1000:7.1f}s {_stamp(row):<22} {params}"
f"{row['duration_ms'] / 1000:7.1f}s {_ago(row.get('created_at')):>9} "
f"{_stamp(row):<22} {params}"
)
return 0