Run a whole project: project runs in manual and auto-confirm mode (RBT-WP-0008-T10)
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Work Records / validate (push) Has been cancelled

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 295952@bnt-lap001
Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
This commit is contained in:
tegwick 2026-09-29 20:08:17 +02:00
parent 1d1087fa7f
commit eff75a303c
14 changed files with 1112 additions and 15 deletions

View file

@ -15,6 +15,8 @@ from . import ui_projects as UP
from . import ui_v2 as UI2
from . import activity as ACT, user as USER
from . import ui_attrs as UA
from . import ui_projectrun as UPR
from . import projectrun as PRUN
from .attrs import Registry
LIGHT = "--fg:#141414;--bg:#fbfbf9;--mut:#6b6b66;--line:#d9d9d2;--card:#fff;--hi:#f2e35a;--ok:#1a7f37;--warn:#9a6700;--bad:#c62828;--side:#f3f3ef"
@ -127,7 +129,7 @@ def page(title: str, body: str) -> bytes:
"const f=b.closest('form'),n=f.querySelector('[name=note]'),w=f.querySelector('[data-warn]');"
"if(n&&!n.value.trim()&&!b.dataset.warned){ev.preventDefault();b.dataset.warned='1';w.hidden=false;n.focus();}}));"
# sidebar: mark where we are; theme switch cycles system -> light -> dark
"const p=location.pathname;const alias={'/projects':['/project/','/template/'],'/runbooks':['/pack/','/run/'],'/attribute-sets':['/attribute-set/']};"
"const p=location.pathname;const alias={'/projects':['/project/','/template/','/project-run/'],'/runbooks':['/pack/','/run/'],'/attribute-sets':['/attribute-set/']};"
"document.querySelectorAll('a[data-nav]').forEach(a=>{const h=a.getAttribute('href');"
"if(h==='/'?p==='/':p===h||p.startsWith(h+'/')||(alias[h]||[]).some(x=>p.startsWith(x)))a.setAttribute('aria-current','page')});"
"const tb=document.querySelector('[data-theme-toggle]');const cur=()=>{try{return localStorage.getItem('rtut-theme')||'system'}catch(x){return 'system'}};"
@ -186,7 +188,7 @@ class App:
f"<p class=note>Running as <b>{label}</b> · <a href='/user'>change</a></p>")
def activities_html(self, limit: int = 12, level: str = "h2", link: bool = False) -> str:
events = ACT.latest(self.packs, limit)
events = ACT.latest(self.packs, limit, PRUN.list_runs(self.registry()))
if not events:
return f"{heading(level, 'Latest activities', '/activity' if link else None)}<p class=note>Nothing has happened yet. Start a run or a project.</p>"
rows = "".join(
@ -379,7 +381,8 @@ class App:
where = ""
if ctx.get("project"):
where = (f" · project <a href='/project/{e(ctx['project'])}'>{e(ctx['project'])}</a>"
f"{' @' + e(ctx['target']) if ctx.get('target') else ''} · {e(ctx.get('environment', ''))}")
f"{' @' + e(ctx['target']) if ctx.get('target') else ''} · {e(ctx.get('environment', ''))}"
f"{' · part of <a href=' + chr(39) + '/project-run/' + e(ctx['project_run']) + chr(39) + '>project run ' + e(ctx['project_run']) + '</a>' if ctx.get('project_run') else ''}")
out = [f"<h1>{e(d['title'])}</h1><p class=pill>{e(d['exercise_status'])}</p>"
f"<p class=note>run {e(rid)} · operator {e(r.data['operator'])}{where}</p>"
f"<p class=note>{UI2.times_line(r)}</p>"]
@ -520,6 +523,10 @@ def make_handler(app: App, port_holder: list):
return self._send_file(status, body, ctype, dl or ("artefact-" + parts[4] if want_dl and status == 200 else None))
if parts[0] == "run":
return self._send(app.run_page(int(parts[1]), parts[2], query=parse_qs(urlparse(self.path).query)))
if parts[0] == "project-run" and len(parts) == 2:
body = UPR.page_for(app, parts[1])
if body:
return self._send(body)
if parts[0] == "project" and len(parts) == 2:
body = UP.project_page(app, parts[1])
if body:
@ -569,6 +576,8 @@ def make_handler(app: App, port_holder: list):
except M.ReviewError as ex:
raise ValueError(str(ex))
return self._redirect(f"/pack/{i}#maturity")
if (len(parts) == 3 and parts[0] == "project" and parts[2] == "runall") or (len(parts) == 3 and parts[0] == "project-run"):
return self._redirect(UPR.handle_post(app, parts, form))
if len(parts) == 3 and parts[0] == "project" and parts[2] == "edit":
from . import project as J
doc = app.registry().projects[parts[1]]
@ -623,6 +632,8 @@ def make_handler(app: App, port_holder: list):
except (R.RunError, KeyError, ValueError, IndexError) as ex:
msg = str(ex)
try:
if parts[0] == "project-run":
return self._send(UPR.page_for(app, parts[1], msg) or page("Error", f"<p class=bad>{e(msg)}</p>"), 400)
if parts[0] == "project":
return self._send(UP.project_page(app, parts[1], msg) or page("Error", f"<p class=bad>{e(msg)}</p>"), 400)
if parts[0] == "template":