UI for projects, templates, attributes and maturity (RBT-WP-0008-T08); stop a test from writing runs into the repo
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:
parent
3470552789
commit
eacbf8a21d
9 changed files with 560 additions and 12 deletions
63
rtut/ui.py
63
rtut/ui.py
|
|
@ -11,6 +11,8 @@ from pathlib import Path
|
|||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from . import pack as P, run as R
|
||||
from . import ui_projects as UP
|
||||
from .attrs import Registry
|
||||
|
||||
CSS = """body{font:15px/1.5 system-ui,sans-serif;max-width:52rem;margin:2rem auto;padding:0 1rem;color:#111;background:#fff}
|
||||
@media(prefers-color-scheme:dark){body{color:#eee;background:#111}pre,input,select{background:#1b1b1b;color:#eee}}
|
||||
|
|
@ -19,7 +21,7 @@ h1,h2{font-weight:600}a{color:inherit}hr{border:0;border-top:1px solid #8886}
|
|||
.step{border:1px solid #8886;padding:.8rem 1rem;margin:.8rem 0}.step.running{border-color:currentColor}
|
||||
.pill{font:12px ui-monospace,monospace;border:1px solid #8886;padding:0 .4rem}
|
||||
pre{border:1px solid #8886;padding:.5rem;overflow:auto;margin:.4rem 0}
|
||||
button,input,select{font:inherit;padding:.25rem .5rem}.head{display:inline-flex;width:calc(100% - 1.6rem);justify-content:space-between;align-items:baseline;gap:1rem;vertical-align:top}summary{cursor:pointer}.row{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.grow{flex:1;min-width:12rem}[hidden]{display:none}#current{scroll-margin-top:1rem}.note{opacity:.75}.bad{font-weight:600}"""
|
||||
button,input,select{font:inherit;padding:.25rem .5rem}.head{display:inline-flex;width:calc(100% - 1.6rem);justify-content:space-between;align-items:baseline;gap:1rem;vertical-align:top}summary{cursor:pointer}.row{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.grow{flex:1;min-width:12rem}[hidden]{display:none}#current{scroll-margin-top:1rem}.note{opacity:.75}.bad{font-weight:600}table{border-collapse:collapse;width:100%;margin:.5rem 0}td,th{border-bottom:1px solid #8884;padding:.3rem .5rem;text-align:left;vertical-align:top}"""
|
||||
|
||||
|
||||
def page(title: str, body: str) -> bytes:
|
||||
|
|
@ -52,9 +54,17 @@ def score_line(c: dict) -> str:
|
|||
|
||||
class App:
|
||||
def __init__(self, roots: list[Path]):
|
||||
self.roots = [Path(r) for r in roots]
|
||||
self.token = secrets.token_urlsafe(16)
|
||||
self.packs = [P.load(p) for r in roots for p in P.discover(r)]
|
||||
|
||||
def registry(self) -> Registry:
|
||||
"""Fresh view of projects, attribute sets, templates and packs (files may have changed)."""
|
||||
return Registry(self.roots)
|
||||
|
||||
def pack_index(self, pack_id: str) -> int | None:
|
||||
return next((i for i, p in enumerate(self.packs) if p.id == pack_id), None)
|
||||
|
||||
def hidden(self) -> str:
|
||||
return f"<input type=hidden name=_t value='{self.token}'>"
|
||||
|
||||
|
|
@ -71,10 +81,9 @@ class App:
|
|||
rows.append(f"<li>{e(d['title'])} <span class=pill>legacy console</span> "
|
||||
f"<span class=note>start with: python -m rtut launch {e(p.path)}</span></li>")
|
||||
else:
|
||||
rows.append(f"<li><a href='/pack/{i}'>{e(d['title'])}</a> "
|
||||
f"<span class=pill>{e(d['exercise_status'])}</span> "
|
||||
rows.append(f"<li><a href='/pack/{i}'>{e(d['title'])}</a> {UP.maturity_pill(p)} "
|
||||
f"<span class=note>{e(score_line(R.scorecard(p)))}</span></li>")
|
||||
return page("Runbooks", f"<h1>Runbooks</h1><ul>{''.join(rows) or '<li>No packs found.</li>'}</ul>")
|
||||
return page("Runbooks", UP.index_sections(self) + f"<h2>Runbooks</h2><ul>{''.join(rows) or '<li>No packs found.</li>'}</ul>")
|
||||
|
||||
def pack_page(self, i: int) -> bytes:
|
||||
p = self.packs[i]; d = p.data
|
||||
|
|
@ -88,7 +97,7 @@ class App:
|
|||
for x in d.get("parameters", []))
|
||||
return page(d["title"], f"<h1>{e(d['title'])}</h1><p class=pill>{e(d['exercise_status'])}</p>"
|
||||
f"<p>{e(d.get('outcome', ''))}</p><p class=note>{e(score_line(R.scorecard(p)))}</p>"
|
||||
f"{self.runs_list(i, p)}<h2>Prerequisites</h2><ul>{prereq}</ul>"
|
||||
f"{UP.maturity_block(self, i, p)}{self.runs_list(i, p)}<h2>Prerequisites</h2><ul>{prereq}</ul>"
|
||||
f"<form method=post action='/run/new/{i}'>{self.hidden()}{fields}"
|
||||
"<p><label>Your name (one word)<br><input name=operator required></label></p>"
|
||||
"<button>Start run</button></form>")
|
||||
|
|
@ -172,8 +181,16 @@ class App:
|
|||
|
||||
def run_page(self, i: int, rid: str, msg: str = "") -> bytes:
|
||||
p = self.packs[i]; r = R.load(p, rid); d = p.data
|
||||
if p.spec != "runbook-pack/v0.1":
|
||||
return page(d["title"], f"<h1>{e(d['title'])}</h1><p class=bad>This run uses {e(p.spec)}. The UI cannot walk "
|
||||
"v0.2 runs yet (RBT-WP-0004); the engine and CLI can.</p><p><a href='/'>← Back</a></p>")
|
||||
ctx = r.data.get("context") or {}
|
||||
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', ''))}")
|
||||
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'])}</p>"]
|
||||
f"<p class=note>run {e(rid)} · operator {e(r.data['operator'])}{where}</p>"]
|
||||
if msg:
|
||||
out.append(f"<p class=bad>{e(msg)}</p>")
|
||||
blocked = False
|
||||
|
|
@ -280,6 +297,14 @@ def make_handler(app: App, port_holder: list):
|
|||
return self._send(app.pack_page(int(parts[1])))
|
||||
if parts[0] == "run":
|
||||
return self._send(app.run_page(int(parts[1]), parts[2]))
|
||||
if parts[0] == "project" and len(parts) == 2:
|
||||
body = UP.project_page(app, parts[1])
|
||||
if body:
|
||||
return self._send(body)
|
||||
if parts[0] == "template" and len(parts) == 2:
|
||||
body = UP.template_page(app, parts[1])
|
||||
if body:
|
||||
return self._send(body)
|
||||
except (IndexError, ValueError, R.RunError):
|
||||
pass
|
||||
self._send(page("Not found", "<p>Not found.</p>"), 404)
|
||||
|
|
@ -294,6 +319,26 @@ def make_handler(app: App, port_holder: list):
|
|||
parts = [x for x in urlparse(self.path).path.split("/") if x]
|
||||
msg = ""
|
||||
try:
|
||||
if len(parts) == 3 and parts[0] == "pack" and parts[2] == "review":
|
||||
from . import maturity as M
|
||||
i = int(parts[1])
|
||||
try:
|
||||
M.add_review(app.packs[i], form.get("by", ""), form.get("note", ""))
|
||||
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] in ("attributes", "run"):
|
||||
from . import project as J
|
||||
reg = app.registry(); doc = reg.projects[parts[1]]
|
||||
if parts[2] == "attributes":
|
||||
J.set_attributes(reg, doc, {k[2:]: v for k, v in form.items() if k.startswith("a_")})
|
||||
return self._redirect(f"/project/{parts[1]}")
|
||||
r = J.start_run(reg, doc, form.get("pack", ""), form.get("operator", ""), form.get("target") or None)
|
||||
i = app.pack_index(form.get("pack", ""))
|
||||
return self._redirect(f"/run/{i}/{r.data['run_id']}#current")
|
||||
if len(parts) == 3 and parts[0] == "template" and parts[2] == "new":
|
||||
pid = UP.create_from_template(app, parts[1], form)
|
||||
return self._redirect(f"/project/{pid}")
|
||||
if parts[:2] == ["run", "new"]:
|
||||
i = int(parts[2]); p = app.packs[i]
|
||||
params = {k[2:]: v for k, v in form.items() if k.startswith("p_")}
|
||||
|
|
@ -327,6 +372,12 @@ def make_handler(app: App, port_holder: list):
|
|||
except (R.RunError, KeyError, ValueError, IndexError) as ex:
|
||||
msg = str(ex)
|
||||
try:
|
||||
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":
|
||||
return self._send(UP.template_page(app, parts[1], msg) or page("Error", f"<p class=bad>{e(msg)}</p>"), 400)
|
||||
if parts[0] == "pack":
|
||||
return self._send(app.pack_page(int(parts[1])).replace(b"<h1>", f"<p class=bad>{e(msg)}</p><h1>".encode(), 1), 400)
|
||||
if parts[:2] == ["run", "new"]:
|
||||
return self._send(app.pack_page(int(parts[2])).replace(b"<h1>", f"<p class=bad>{e(msg)}</p><h1>".encode(), 1), 400)
|
||||
return self._send(app.run_page(int(parts[1]), parts[2], msg), 400)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue