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 import time
from collections.abc import Callable, Iterable, Iterator from collections.abc import Callable, Iterable, Iterator
from contextlib import contextmanager, nullcontext from contextlib import contextmanager, nullcontext
from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -804,7 +805,8 @@ def _status_screen(client: Client) -> Any:
) )
+ Text( + Text(
f"{str(row.get('flow', '')):<16}" 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", style="dim",
) )
) )
@@ -816,6 +818,7 @@ def _status_screen(client: Client) -> Any:
) )
parts.append( parts.append(
Text(f" {where} ", style="red") Text(f" {where} ", style="red")
+ Text(f"{_ago(event.get('ts')):>9} ", style="dim")
+ Text(str(event.get("detail", ""))[:100], style="dim") + Text(str(event.get("detail", ""))[:100], style="dim")
) )
return Group(*parts) return Group(*parts)
@@ -857,9 +860,9 @@ def cmd_status(args: argparse.Namespace) -> int:
#: value is read. #: value is read.
PARAMS_WIDTH = 80 PARAMS_WIDTH = 80
#: What the columns before the inputs take: the id, status, flow, duration and #: What the columns before the inputs take: the id, status, flow, duration,
#: stamp, with their spacing. #: age and stamp, with their spacing.
LISTING_WIDTH = 84 LISTING_WIDTH = 95
def _declared(client: Client, flows: Iterable[str]) -> dict[str, dict[str, Any]]: 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 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: def _stamp(row: dict[str, Any]) -> str:
"""What code a run ran: the commit, whether it was dirty, and the digest. """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] + "..." params = params[: room - 3] + "..."
_say( _say(
f"{row['id']} {_status(row['status'], 9)} {row['flow']:<16} " 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 return 0
+18
View File
@@ -520,6 +520,24 @@ def test_two_files_of_one_name_are_refused(tmp_path) -> None:
_import(*_module_of(second), second) _import(*_module_of(second), second)
def test_how_long_ago_reads_like_a_duration() -> None:
"""A failure with no time on it says nothing about whether it is current."""
from datetime import UTC, datetime, timedelta
from fluksio.sdk.cli import _ago
def then(**delta):
return (datetime.now(UTC) - timedelta(**delta)).isoformat()
assert _ago(then(seconds=5)) == "5s ago"
assert _ago(then(minutes=3)) == "3min ago"
assert _ago(then(hours=2)) == "2h ago"
assert _ago(then(days=3)) == "3d ago"
# What the engine stores is UTC whether or not the spelling says so.
assert _ago(datetime.now(UTC).replace(tzinfo=None).isoformat()) == "0s ago"
assert _ago(None) == ""
def test_a_serve_limit_is_refused_as_a_flag_not_as_a_traceback(capsys) -> None: def test_a_serve_limit_is_refused_as_a_flag_not_as_a_traceback(capsys) -> None:
"""These are written into the environment before the settings are built.""" """These are written into the environment before the settings are built."""
import pytest import pytest