v0.2 walk-through UI: guided flow, data view, overview, modes, overrides, typed phrase; retry confirmation; visual pass
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
f8ce4717a8
commit
f070999aa2
12 changed files with 1285 additions and 53 deletions
93
rtut/ui.py
93
rtut/ui.py
|
|
@ -12,16 +12,66 @@ from urllib.parse import parse_qs, urlparse
|
|||
|
||||
from . import pack as P, run as R
|
||||
from . import ui_projects as UP
|
||||
from . import ui_v2 as UI2
|
||||
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}}
|
||||
h1,h2{font-weight:600}a{color:inherit}hr{border:0;border-top:1px solid #8886}
|
||||
.eyebrow{font:12px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.06em}
|
||||
.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}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}"""
|
||||
CSS = """
|
||||
:root{--fg:#141414;--bg:#fbfbf9;--mut:#6b6b66;--line:#d9d9d2;--card:#fff;--hi:#f2e35a;--ok:#1a7f37;--warn:#9a6700;--bad:#c62828;--radius:6px}
|
||||
@media(prefers-color-scheme:dark){:root{--fg:#ececea;--bg:#121212;--mut:#9a9a94;--line:#2e2e2b;--card:#191918;--hi:#f2e35a;--ok:#56d364;--warn:#e3b341;--bad:#ff7b72}}
|
||||
*{box-sizing:border-box}
|
||||
body{font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;max-width:56rem;margin:2rem auto;padding:0 1.2rem;color:var(--fg);background:var(--bg)}
|
||||
h1{font-size:1.6rem;font-weight:650;letter-spacing:-.01em;margin:.4rem 0 .6rem}
|
||||
h2{font-size:1.1rem;font-weight:650;margin:1.8rem 0 .5rem;padding-bottom:.25rem;border-bottom:1px solid var(--line)}
|
||||
h3{font-size:.95rem;margin:1.2rem 0 .3rem}
|
||||
a{color:inherit;text-underline-offset:.15em}a:hover{background:var(--hi);color:#141414}
|
||||
hr{border:0;border-top:1px solid var(--line)}
|
||||
code,pre{font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}
|
||||
pre{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:.6rem .8rem;overflow:auto;margin:.5rem 0}
|
||||
.eyebrow{font:11px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.08em;color:var(--mut);margin:.2rem 0}
|
||||
.note{color:var(--mut)}.bad{color:var(--bad);font-weight:600}
|
||||
progress{height:.45rem;border:0;border-radius:99px;overflow:hidden;background:var(--line);accent-color:var(--fg)}
|
||||
progress::-webkit-progress-bar{background:var(--line)}progress::-webkit-progress-value{background:var(--fg)}
|
||||
button,input,select,textarea{font:inherit;color:inherit;background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:.3rem .6rem}
|
||||
button{cursor:pointer;background:var(--fg);color:var(--bg);border-color:var(--fg)}
|
||||
button:hover{background:var(--hi);color:#141414;border-color:var(--hi)}
|
||||
button[name=ok][value="0"],button[data-fail],button[type=button]{background:var(--card);color:var(--fg);border-color:var(--line)}
|
||||
button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,summary:focus-visible{outline:2px solid var(--hi);outline-offset:2px}
|
||||
label{display:inline-block}fieldset{border:1px solid var(--line);border-radius:var(--radius);margin:.8rem 0;padding:.4rem .9rem}
|
||||
table{border-collapse:collapse;width:100%;margin:.5rem 0;background:var(--card);border:1px solid var(--line);border-radius:var(--radius)}
|
||||
td,th{border-bottom:1px solid var(--line);padding:.4rem .6rem;text-align:left;vertical-align:top}th{font:11px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.06em;color:var(--mut)}
|
||||
tr:last-child td{border-bottom:0}
|
||||
table.data td:first-child{width:42%}
|
||||
td label{display:block}
|
||||
td input:not([type=checkbox]):not([type=hidden]):not([type=radio]){width:100%}
|
||||
.row{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.grow{flex:1;min-width:12rem}[hidden]{display:none}
|
||||
summary{cursor:pointer}.head{display:inline-flex;width:calc(100% - 1.6rem);justify-content:space-between;align-items:baseline;gap:1rem;vertical-align:top}
|
||||
.step,.stage,.panel,.banner,.questions,details.framing,details.override{border:1px solid var(--line);border-radius:var(--radius);background:var(--card);padding:.7rem 1rem;margin:.7rem 0}
|
||||
.stage{padding:.8rem 1.1rem;background:transparent}.stage>summary{font-size:1.02rem}
|
||||
.stage .step,.stage .questions,.stage .stage{background:var(--card)}
|
||||
.step.running{border-color:var(--fg);box-shadow:0 0 0 1px var(--fg)}.step.failed{border-color:var(--bad)}.step.done,.step.pushed{opacity:.9}
|
||||
.step.gate{border-style:dashed}.step.gate.passed{border-style:solid}
|
||||
details.framing{background:transparent;border-left:3px solid var(--warn)}details.override{background:transparent;border-style:dashed;margin:.5rem 0}
|
||||
.panel{background:transparent}.panel[data-s=auto]{border-color:var(--fg)}
|
||||
.banner{background:var(--hi);color:#141414;border-color:var(--hi)}.banner a{text-decoration-thickness:2px}.banner .note{color:#3d3a10}
|
||||
.banner .chip{background:rgba(255,255,255,.55);color:#141414;border-color:rgba(0,0,0,.2)}
|
||||
.questions{background:transparent;border-left:3px solid var(--fg)}
|
||||
.tabs{display:flex;gap:.2rem;margin:1rem 0;border-bottom:1px solid var(--line)}
|
||||
.tab{padding:.4rem .9rem;text-decoration:none;border:1px solid transparent;border-bottom:0;border-radius:var(--radius) var(--radius) 0 0;color:var(--mut)}
|
||||
.tab.on{color:var(--fg);border-color:var(--line);background:var(--card);font-weight:650;margin-bottom:-1px}
|
||||
.chip{display:inline-block;font:12px ui-monospace,monospace;border:1px solid var(--line);border-radius:99px;padding:0 .55rem;margin:.1rem .1rem;text-decoration:none;color:var(--mut)}
|
||||
a.chip:hover{background:var(--hi);color:#141414;border-color:var(--hi)}.chip.on{background:var(--fg);color:var(--bg);border-color:var(--fg)}
|
||||
.chip[data-s=irreversible]{color:var(--bad);border-color:var(--bad)}
|
||||
.pill{font:11.5px ui-monospace,monospace;border:1px solid var(--line);border-radius:99px;padding:.05rem .55rem;white-space:nowrap;color:var(--mut);background:var(--card)}
|
||||
.pill[data-s=done],.pill[data-s=passed],.pill[data-s=ok],.pill[data-s=complete],.pill[data-s=succeeded],.pill[data-s=applied]{color:var(--ok);border-color:var(--ok)}
|
||||
.pill[data-s=running],.pill[data-s="in progress"],.pill[data-s=applying],.pill[data-s=auto],.pill[data-s=auto-confirm]{background:var(--hi);color:#141414;border-color:var(--hi)}
|
||||
.pill[data-s=failed],.pill[data-s=err],.pill[data-s=invalid]{color:var(--bad);border-color:var(--bad)}
|
||||
.pill[data-s=pushed],.pill[data-s=waived],.pill[data-s=stale],.pill[data-s=override],.pill[data-s=push-through],.pill[data-s=waiver],.pill[data-s=modified],.pill[data-s=abandoned]{color:var(--warn);border-color:var(--warn)}
|
||||
.pill[data-s=nil],.pill[data-s=todo],.pill[data-s=open],.pill[data-s=planned],.pill[data-s="not checked"],.pill[data-s=not-applied]{color:var(--mut);border-style:dashed}
|
||||
.checks{list-style:none;padding:0;margin:.4rem 0}.checks li{padding:.35rem 0;border-bottom:1px dashed var(--line)}.checks li:last-child{border:0}
|
||||
#current{scroll-margin-top:1rem}details>summary{list-style-position:outside}
|
||||
@media(max-width:40rem){body{margin:1rem auto}.head{display:block;width:auto}.tabs{overflow:auto}}
|
||||
@media print{button,.tabs,form{display:none}.step,.stage{break-inside:avoid}}
|
||||
"""
|
||||
|
||||
|
||||
def page(title: str, body: str) -> bytes:
|
||||
|
|
@ -74,9 +124,6 @@ class App:
|
|||
d = p.data
|
||||
if not p.ok:
|
||||
rows.append(f"<li class=bad>{e(p.id)}: invalid ({e(p.errors[0])})</li>")
|
||||
elif d.get("spec") != "runbook-pack/v0.1" and d["engine"] == "native":
|
||||
rows.append(f"<li>{e(d['title'])} <span class=pill>{e(d.get('spec'))}</span> "
|
||||
"<span class=note>cannot be run yet (RBT-WP-0007)</span></li>")
|
||||
elif d["engine"] != "native":
|
||||
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>")
|
||||
|
|
@ -87,6 +134,12 @@ class App:
|
|||
|
||||
def pack_page(self, i: int) -> bytes:
|
||||
p = self.packs[i]; d = p.data
|
||||
if p.spec == "runbook-pack/v0.2":
|
||||
framing = "".join(f"<li>{e(x)}</li>" for x in d.get("framing", []))
|
||||
return page(d["title"], f"<h1>{e(d['title'])}</h1><p>{UP.pill('v0.2')} {UP.maturity_pill(p)}</p><p>{e(d.get('outcome', ''))}</p>"
|
||||
f"<p class=note>{e(score_line(R.scorecard(p)))}</p>"
|
||||
f"<details class=framing open><summary><b>Keep in mind</b></summary><ul>{framing}</ul></details>"
|
||||
f"{UP.maturity_block(self, i, p)}{self.runs_list(i, p)}<h2>Start a run</h2>{UI2.start_form(self, i, p)}")
|
||||
prereq = "".join(f"<li>[{e(x['owner'])}] {e(x['text'])}</li>" for x in d.get("prerequisites", []))
|
||||
fields = "".join(
|
||||
f"<p><label>{e(x['label'])}<br>"
|
||||
|
|
@ -179,11 +232,10 @@ class App:
|
|||
f"<details><summary class=note>Change my feedback</summary>{form}</details></div>")
|
||||
return f"<div id=feedback class=step><h2>How was this runbook?</h2>{form}</div>"
|
||||
|
||||
def run_page(self, i: int, rid: str, msg: str = "") -> bytes:
|
||||
def run_page(self, i: int, rid: str, msg: str = "", query: dict | None = None) -> 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>")
|
||||
if p.spec == "runbook-pack/v0.2":
|
||||
return UI2.run_page(self, i, rid, msg, query)
|
||||
ctx = r.data.get("context") or {}
|
||||
where = ""
|
||||
if ctx.get("project"):
|
||||
|
|
@ -259,7 +311,7 @@ class App:
|
|||
out.append(f"<details{anchor} class='step {state}'{fold}><summary><span class=head>"
|
||||
f"<span><b>{n}. {e(s['title'])}</b> "
|
||||
f"<span class=note>[{e(s['owner'])}] {e(s.get('risk', 'read-only'))}</span></span>"
|
||||
f"<span class=pill>{state}</span></span></summary>{block}"
|
||||
f"<span class=pill data-s='{state}'>{state}</span></span></summary>{block}"
|
||||
f"{self.capture_html(url, s, st, state, finished)}"
|
||||
f"<p class=note>Done when: {e(s['verify']['done_when'])}</p>{rb}{act}</details>")
|
||||
if r.complete:
|
||||
|
|
@ -296,7 +348,7 @@ def make_handler(app: App, port_holder: list):
|
|||
if parts[0] == "pack":
|
||||
return self._send(app.pack_page(int(parts[1])))
|
||||
if parts[0] == "run":
|
||||
return self._send(app.run_page(int(parts[1]), parts[2]))
|
||||
return self._send(app.run_page(int(parts[1]), parts[2], query=parse_qs(urlparse(self.path).query)))
|
||||
if parts[0] == "project" and len(parts) == 2:
|
||||
body = UP.project_page(app, parts[1])
|
||||
if body:
|
||||
|
|
@ -319,6 +371,8 @@ def make_handler(app: App, port_holder: list):
|
|||
parts = [x for x in urlparse(self.path).path.split("/") if x]
|
||||
msg = ""
|
||||
try:
|
||||
if parts[0] == "run" and len(parts) >= 4 and parts[1].isdigit() and app.packs[int(parts[1])].spec == "runbook-pack/v0.2":
|
||||
return self._redirect(UI2.handle_post(app, int(parts[1]), parts[2], parts[3:], form))
|
||||
if len(parts) == 3 and parts[0] == "pack" and parts[2] == "review":
|
||||
from . import maturity as M
|
||||
i = int(parts[1])
|
||||
|
|
@ -333,7 +387,7 @@ def make_handler(app: App, port_holder: list):
|
|||
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)
|
||||
r = J.start_run(reg, doc, form.get("pack", ""), form.get("operator", ""), form.get("target") or None, form.get("mode", "manual"))
|
||||
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":
|
||||
|
|
@ -341,6 +395,9 @@ def make_handler(app: App, port_holder: list):
|
|||
return self._redirect(f"/project/{pid}")
|
||||
if parts[:2] == ["run", "new"]:
|
||||
i = int(parts[2]); p = app.packs[i]
|
||||
if p.spec == "runbook-pack/v0.2":
|
||||
r = UI2.create_run(app, i, form)
|
||||
return self._redirect(f"/run/{i}/{r.data['run_id']}#current")
|
||||
params = {k[2:]: v for k, v in form.items() if k.startswith("p_")}
|
||||
r = R.create(p, form.get("operator", ""), params)
|
||||
return self._redirect(f"/run/{i}/{r.data['run_id']}#current")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue