pipekit/pipekit/web/templates/_group_run_live.html
Paul Trowbridge 7de9d83589 Add a groups JSON API and group-run cancellation
The group pages were browser-only, so external schedulers had no way to
trigger a group or read its outcome. Adds /api/groups, /api/group-runs and
their run/list endpoints under Basic auth, following the same async contract
as POST /modules/{id}/run.

Cancellation needed more than an endpoint. run_group deliberately continues
past module failures, so killing the in-flight jrunner alone would just let
the loop march on to the next member. A group-level flag in engine/cancel.py
is now checked before each member, and engine.request_group_cancel sets that
flag *and* signals the running module -- it lives in runner.py rather than
cancel.py because finding the live run needs the DB, and cancel.py is
deliberately DB-free.

Group final status gains 'cancelled', ranked above 'error' since the error is
usually the terminated jrunner process. group_run.status already permitted the
value; no migration needed.

Also adds a "cancel group" button to the group run page (web) and .pill
styling for cancelled/dry_run, neither of which had a color rule.

Note: /runs/{id}/cancel keeps its existing meaning -- cancel one member and
let the group continue -- which is now a deliberate contrast to cancel group.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:10:50 -04:00

56 lines
2.4 KiB
HTML

{# Partial: module runs table for group_run_detail.html. Polls while running. #}
<div id="group-run-live"
{% if group_run.status == 'running' %}
hx-get="/group-runs/{{ group_run.id }}/live"
hx-trigger="every 3s"
hx-swap="outerHTML"
{% endif %}>
<div class="panel">
<header>
Module runs
<span style="margin-left:auto">
{% if group_run.status == 'running' %}
<button class="btn ghost" style="margin-right:0.5rem"
hx-post="/group-runs/{{ group_run.id }}/cancel"
hx-target="#group-run-live" hx-swap="outerHTML"
hx-confirm="Cancel this group run? The module in flight is stopped and the remaining modules are skipped.">cancel group</button>
{% endif %}
<span class="pill {{ group_run.status }}">{{ group_run.status }}</span>
{% if group_run.duration_s is not none %}&nbsp;· {{ group_run.duration_s | duration }}{% endif %}
</span>
</header>
<div class="body tight">
{% if module_runs %}
<table class="grid">
<thead>
<tr>
<th>run</th>
<th>module</th>
<th>started</th>
<th>duration</th>
<th>status</th>
<th>rows</th>
</tr>
</thead>
<tbody>
{% for r in module_runs %}
<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 | localtime }}</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>
{% endfor %}
</tbody>
</table>
{% elif group_run.status == 'running' %}
<div class="empty">Waiting for module runs to start…</div>
{% else %}
<div class="empty">No module runs recorded.</div>
{% endif %}
</div>
</div>
</div>