"""UI pages for projects, templates, attributes and maturity. Server-rendered; no script needed.""" from __future__ import annotations import html from pathlib import Path from . import maturity as M, project as J, run as R from .attrs import fold, set_layers def e(s) -> str: return html.escape(str(s)) def pill(text: str) -> str: return f"{e(text)}" def _runnable_here(pack) -> bool: return pack.ok and pack.data.get("engine") == "native" and pack.spec == "runbook-pack/v0.1" # ---- maturity ------------------------------------------------------------------------------------------------ def maturity_pill(pack) -> str: if not pack.ok or pack.data.get("engine") != "native": return "" m = M.maturity(pack) label = m["level"] + (f" (was {m['stale_from']})" if m["stale_from"] else "") return pill(label) def idempotency_text(pack) -> str: if pack.spec != "runbook-pack/v0.2": return "not declared (v0.1 packs cannot declare it)" from .flow import pack_idempotency return pack_idempotency(pack.data) def maturity_block(app, i: int, pack) -> str: m = M.maturity(pack) ev = m["evidence"] revs = "".join(f"
  • {e(r['by'])} · {e(r['at'][:10])} · {'current revision' if r['digest'] == m['digest'] else 'older revision'}" f"{' · ' + e(r['note']) if r.get('note') else ''}
  • " for r in M._reviews(pack)) stale = f" It was {e(m['stale_from'])} before the pack changed." if m["stale_from"] else "" return (f"

    Maturity {maturity_pill(pack)}

    " f"

    Earned from run history at revision {e(m['digest'])}; independent of any one environment.{stale}

    " f"

    Reviewed: {'yes' if ev['reviewed'] else 'no'} · clean runs: {ev['clean_runs']} " f"(in rehearsal: {ev['rehearsal_runs']}) · environments: {e(', '.join(ev['environments']) or '-')} · " f"operators: {e(', '.join(ev['operators']) or '-')}

    " "

    proven = reviewed + rehearsed + clean in two environments by two operators. Runs without a project " "context, and runs with deviations, do not count.

    " f"

    Idempotency: {e(idempotency_text(pack))}

    " f"{'' if revs else ''}" f"
    {app.hidden()}

    " " " "

    ") # ---- index sections --------------------------------------------------------------------------------------------- def index_sections(app) -> str: reg = app.registry() prj = [] for pid, d in reg.projects.items(): if not d.ok: prj.append(f"
  • {e(pid)}: invalid ({e(d.errors[0])})
  • ") continue errs = J.validate(reg, d) st = pill("invalid") if errs else pill(J.state(reg, d)["state"]) origin = (f" from template {e(d.data['template'])}" if d.data.get("template") else "") desc = f"
    {e(d.data['description'])}" if d.data.get("description") else "" prj.append(f"
  • {e(d.data['title'])} {pill(d.data['environment'])} {st}{origin}{desc}
  • ") tpl = [f"
  • {e(d.data['title'])} " f"{e(d.data.get('goal', ''))}
  • " if d.ok else f"
  • {e(tid)}: invalid
  • " for tid, d in reg.templates.items()] out = f"

    Projects

    " out += f"

    Project templates

    " return out # ---- attribute tables ---------------------------------------------------------------------------------------------- def attribute_table(rows: list[dict]) -> str: body = "" for r in rows: value = "" if r["value"] is None else str(r["value"]) if r["value"] is None and "default" in r: value = f"(runbook default: {r['default']})" cls = " class=bad" if r["state"] == "err" or (r["state"] == "nil" and r["requirement"] == "required" and "default" not in r) else "" source = e(r["source"]) if r["source"] else "-" body += (f"{e(r['label'])} ({e(r['id'])}){e(value)}" f"{pill(r['state'])}{' ' + pill('modified') if r['modified'] else ''}{e(r['requirement'])}" f"{source}{e(r.get('error', ''))}") return ("" f"{body}
    AttributeValueStateRequirementSourceProblem
    ") # ---- project page ---------------------------------------------------------------------------------------------------- def project_page(app, pid: str, msg: str = "") -> bytes | None: from .ui import page reg = app.registry() doc = reg.projects.get(pid) if doc is None: return None d = doc.data out = [f"

    {e(d.get('title', pid))}

    "] if msg: out.append(f"

    {e(msg)}

    ") errs = J.validate(reg, doc) if errs: out.append("

    This project has problems:

    ") return page(d.get("title", pid), "".join(out)) st = J.state(reg, doc) out.append(f"

    {pill(d['environment'])} {pill(st['state'])}" f"{' from template ' + e(d['template']) + '' if d.get('template') else ''}

    ") if d.get("description"): out.append(f"

    {e(d['description'])}

    ") if d.get("goal"): out.append(f"

    Goal: {e(d['goal'])}

    ") # what is still needed needs: dict[str, list[str]] = {} for n in st["needs"]: needs.setdefault(n["attribute"], []).append(n["pack"]) if needs: fields = "".join(f"

    needed by {e(', '.join(packs))}

    " for a, packs in needs.items()) out.append(f"

    Still needed

    Required attributes without a value. Never enter secrets.

    " f"
    {app.hidden()}{fields}
    ") # members rows = "" by_pack = {m["pack"]: m for m in st["members"]} for spec in d["runbooks"]: pack = reg.packs[spec["pack"]] ms = next(m for m in st["members"] if m["pack"] == spec["pack"] and m["target"] == spec.get("target")) i = app.pack_index(spec["pack"]) wait = [a for a in spec.get("after", []) if by_pack[a]["status"] != "applied"] needed = sorted({n["attribute"] for n in st["needs"] if n["pack"] == spec["pack"]}) if not _runnable_here(pack): action = "cannot be run in this UI yet" elif needed: action = f"needs: {e(', '.join(needed))}" elif wait: action = f"waiting for {e(', '.join(wait))}" else: action = (f"
    {app.hidden()}" f"" "
    ") link = f" latest run" if ms.get("run") and i is not None else "" dev = f" · {ms['deviations']} deviation(s)" if ms.get("deviations") else "" rows += (f"{e(pack.data.get('title', pack.id))}
    {e(spec['pack'])}" f"{' @' + e(spec['target']) if spec.get('target') else ''}" f"{pill(ms['status'])}{e(dev)}{link}{maturity_pill(pack)}{action}") out.append("

    Runbooks

    " f"

    Next: {e(', '.join(st['next']) or 'nothing to start')}

    " "" + rows + "
    RunbookStatusMaturity
    ") # attributes per member out.append("

    Attributes

    What each runbook receives from this project, and where each value comes from.

    ") for spec in d["runbooks"]: res = J.member_attributes(reg, d, spec) unused = f"

    Unused attributes: {e(', '.join(res['unused']))}

    " if res["unused"] else "" out.append(f"
    {e(reg.packs[spec['pack']].data.get('title', spec['pack']))}" f"{' @' + e(spec['target']) if spec.get('target') else ''}{attribute_table(res['rows'])}{unused}
    ") if d.get("targets"): out.append("

    Targets

    ") return page(d["title"], "".join(out)) # ---- template page ---------------------------------------------------------------------------------------------------- def template_page(app, tid: str, msg: str = "") -> bytes | None: from .ui import page reg = app.registry() doc = reg.templates.get(tid) if doc is None: return None d = doc.data out = [f"

    {e(d.get('title', tid))}

    {pill('template')}

    "] if msg: out.append(f"

    {e(msg)}

    ") if not doc.ok: out.append("

    This template is invalid:

    ") return page(d.get("title", tid), "".join(out)) if d.get("goal"): out.append(f"

    {e(d['goal'])}

    ") layers = [] try: for s in d.get("attribute_sets", []): layers += set_layers(reg, s) except ValueError as ex: out.append(f"

    {e(ex)}

    ") from .attrs import normalize layers.append(("template", normalize(d.get("attributes")))) folded = fold(layers) members = "".join(f"
  • {e(m['pack'])}{' @' + e(m['target']) if m.get('target') else ''}" f"{' (after ' + e(', '.join(m['after'])) + ')' if m.get('after') else ''}
  • " for m in d["runbooks"]) out.append(f"

    Runbooks

    ") fields = "" for name, a in folded.items(): req = a["requirement"] or "optional" val = "" if a["value"] is None else a["value"] note = f" - {e(a['note'])}" if a.get("note") else "" fields += (f"

    " f"{note}{' from ' + e(a['source']) if a['source'] else ''}

    ") envs = "".join(f"" for x in R.ENVIRONMENTS) out.append("

    Start a project from this template

    " f"
    {app.hidden()}" "

    " "

    " "

    " "

    " f"

    " "

    Attributes

    Values you leave as they are stay defaults; required ones must be given before the " f"runbooks can start. Never enter secrets.

    {fields}
    ") return page(d["title"], "".join(out)) # ---- actions ------------------------------------------------------------------------------------------------------------------ def create_from_template(app, tid: str, form: dict) -> str: """Instantiate a template; returns the new project id.""" reg = app.registry() doc = reg.templates.get(tid) if doc is None or not doc.ok: raise ValueError(f"no valid template {tid}") layers = [] for s in doc.data.get("attribute_sets", []): layers += set_layers(reg, s) from .attrs import normalize layers.append(("template", normalize(doc.data.get("attributes")))) base = fold(layers) values = {} for k, v in form.items(): if not k.startswith("a_"): continue name, v = k[2:], v.strip() if v and str(base.get(name, {}).get("value")) != v: values[name] = v from .pack import SECRET_RE for name, v in values.items(): if SECRET_RE.search(v): raise ValueError(f"{name}: value looks like a secret; attributes are never secret") pid = form.get("project_id", "").strip() if len(form.get("title", "").strip()) < 3: raise ValueError("give the project a name of at least 3 characters") J.instantiate(reg, tid, pid, form.get("environment", ""), values, title=form.get("title", ""), root=doc.path.parent.parent, description=form.get("description", ""), goal=form.get("goal", "")) reg = app.registry() created = reg.projects[pid] errs = J.validate(reg, created) if errs: created.path.unlink() created.path.parent.rmdir() raise ValueError(f"project would be invalid: {errs[0]}") return pid