Fix missing run start time and add duration to run history

create_run now sets started_at on INSERT. list_runs computes duration_s
via julianday arithmetic. Both the module detail and runs page show
duration formatted as Xs or Xm Ys. A Jinja2 filter handles formatting.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-05-21 00:23:31 -04:00
parent 595024eb52
commit f39b1df75e
4 changed files with 25 additions and 7 deletions

View File

@ -404,7 +404,8 @@ def clear_stale_locks(max_age_hours: int = 24, live_pids: set[int] | None = None
def create_run(module_id: int, *, group_run_id: int | None = None) -> int:
with db.connect() as c:
cur = c.execute(
"INSERT INTO run_log (module_id, group_run_id) VALUES (?, ?)",
"INSERT INTO run_log (module_id, group_run_id, started_at) "
"VALUES (?, ?, datetime('now'))",
(module_id, group_run_id),
)
return int(cur.lastrowid)
@ -494,6 +495,10 @@ def list_runs(*, module_id: int | None = None, status: str | None = None,
params.append(limit)
with db.connect() as c:
return [dict(r) for r in c.execute(
f"SELECT r.*, m.name AS module_name FROM run_log r "
f"SELECT r.*, m.name AS module_name, "
f"CASE WHEN r.started_at IS NOT NULL AND r.finished_at IS NOT NULL "
f"THEN CAST(ROUND((julianday(r.finished_at) - julianday(r.started_at)) * 86400) AS INTEGER) "
f"ELSE NULL END AS duration_s "
f"FROM run_log r "
f"LEFT JOIN module m ON r.module_id=m.id "
f"{clause} ORDER BY r.id DESC LIMIT ?", params)]

View File

@ -29,6 +29,18 @@ _WEB_DIR = Path(__file__).parent
_templates = Jinja2Templates(directory=_WEB_DIR / "templates")
def _fmt_duration(seconds) -> str:
if seconds is None:
return ""
s = int(seconds)
if s < 60:
return f"{s}s"
return f"{s // 60}m {s % 60:02d}s"
_templates.env.filters["duration"] = _fmt_duration
def mount_web(app: FastAPI) -> None:
"""Attach HTML pages + /static onto a FastAPI app."""
app.mount("/static", StaticFiles(directory=_WEB_DIR / "static"), name="static")

View File

@ -187,12 +187,13 @@
<div class="body tight">
{% if recent_runs %}
<table class="grid">
<thead><tr><th>id</th><th>started</th><th>status</th><th>rows</th></tr></thead>
<thead><tr><th>id</th><th>started</th><th>duration</th><th>status</th><th>rows</th></tr></thead>
<tbody>
{% for r in recent_runs %}
<tr>
<td><a href="/runs/{{ r.id }}">#{{ r.id }}</a></td>
<td class="mono">{{ r.started_at }}</td>
<td class="mono">{{ r.started_at or '—' }}</td>
<td class="mono">{{ r.duration_s | duration }}</td>
<td><span class="pill {{ r.status }}">{{ r.status }}</span></td>
<td class="mono">{{ r.row_count if r.row_count is not none else "—" }}</td>
</tr>

View File

@ -22,7 +22,7 @@
<th style="width:5em">id</th>
<th>module</th>
<th>started</th>
<th>finished</th>
<th>duration</th>
<th style="width:8em">status</th>
<th style="width:7em">rows</th>
<th>error</th>
@ -33,8 +33,8 @@
<tr>
<td><a href="/runs/{{ r.id }}">#{{ r.id }}</a></td>
<td><a href="/modules/{{ r.module_id }}">{{ r.module_name }}</a></td>
<td class="mono">{{ r.started_at }}</td>
<td class="mono">{{ r.finished_at or '—' }}</td>
<td class="mono">{{ r.started_at or '—' }}</td>
<td class="mono">{{ r.duration_s | duration }}</td>
<td><span class="pill {{ r.status }}">{{ r.status }}</span></td>
<td class="mono">{{ r.row_count if r.row_count is not none else "—" }}</td>
<td class="mono" style="max-width:22rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ r.error or '' }}</td>