Phase 1 child of the Web Console epic #631. Adds the operator-facing system-health dashboard on top of the read-only system-health API landed by #634, so runtime problems are visible on a surface instead of being discovered late through failed LLM sessions. - webui/system_health_views.py (new): renders the SystemHealthSnapshot as readiness, stale-runtime parity, version/uptime, dependency, MCP namespace, probe-error, and recovery cards. - webui/app.py: GET /system-health, sharing load_system_health() with the JSON API so page and API cannot disagree. ?deep=1 behaves as on the API. - webui/layout.py: nav entry and health card/badge styles. - tests/test_webui_system_health_dashboard.py (new, 26 cases). - docs/webui-local-dev.md: route, field authority, and redaction split. Readiness honesty is preserved from the API: ready and readiness_complete render separately, a probe that did not run is listed under "Not probed" rather than counted healthy, and mutation safety is never claimed when the runtime is stale or parity is indeterminate. Redaction is split by field kind. Free text (probe details, reasons, probe errors) passes through system_health.redact. Structured fields (commit SHAs, probe names, statuses, timestamps) are HTML-escaped only: redact's opaque-token rule matches any run of 32 or more characters, so routing a 40-character git SHA through it rendered "[redacted]" and blanked the parity evidence the page exists to show. Non-goals honored: no restart or reload controls (Phase 2, #642), no manual process-kill guidance (#630). Read-only throughout. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
197 lines
5.6 KiB
Python
197 lines
5.6 KiB
Python
"""Shared HTML layout for the internal web UI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
NAV_ITEMS = (
|
|
("/", "Home"),
|
|
("/system-health", "System health"),
|
|
("/queue", "Queue"),
|
|
("/projects", "Projects"),
|
|
("/prompts", "Prompts"),
|
|
("/runtime", "Runtime"),
|
|
("/audit", "Audit"),
|
|
("/worktrees", "Worktrees"),
|
|
("/leases", "Leases"),
|
|
("/actions", "Actions"),
|
|
)
|
|
|
|
MVP_NOTICE = (
|
|
"Read-only MVP — Gitea, MCP tools, and canonical workflows remain the "
|
|
"source of truth. No mutation endpoints."
|
|
)
|
|
|
|
|
|
def render_page(*, title: str, body_html: str, extra_head: str = "") -> str:
|
|
nav_links = "".join(
|
|
f'<a href="{href}">{label}</a>' for href, label in NAV_ITEMS
|
|
)
|
|
return f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{title} · MCP Control Plane</title>
|
|
<style>
|
|
:root {{
|
|
--bg: #0f1419;
|
|
--surface: #1a2332;
|
|
--text: #e7ecf3;
|
|
--muted: #8b9cb3;
|
|
--accent: #5b9fd4;
|
|
--border: #2a3648;
|
|
}}
|
|
* {{ box-sizing: border-box; }}
|
|
body {{
|
|
margin: 0;
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
line-height: 1.5;
|
|
}}
|
|
header {{
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0.75rem 1.25rem;
|
|
}}
|
|
header h1 {{
|
|
margin: 0 0 0.5rem;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
}}
|
|
nav {{
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem 1rem;
|
|
}}
|
|
nav a {{
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
font-size: 0.9rem;
|
|
}}
|
|
nav a:hover {{ text-decoration: underline; }}
|
|
main {{
|
|
max-width: 52rem;
|
|
margin: 0 auto;
|
|
padding: 1.5rem 1.25rem 2.5rem;
|
|
}}
|
|
.notice {{
|
|
color: var(--muted);
|
|
font-size: 0.85rem;
|
|
margin-bottom: 1.25rem;
|
|
padding: 0.65rem 0.85rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
background: var(--surface);
|
|
}}
|
|
h2 {{ margin-top: 0; font-size: 1.35rem; }}
|
|
p {{ color: var(--muted); }}
|
|
.stub {{
|
|
border-left: 3px solid var(--accent);
|
|
padding-left: 0.85rem;
|
|
margin: 1rem 0;
|
|
}}
|
|
.meta {{ font-size: 0.85rem; }}
|
|
table.registry, table.detail {{
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 1rem 0;
|
|
font-size: 0.9rem;
|
|
}}
|
|
table.registry th, table.registry td,
|
|
table.detail th, table.detail td {{
|
|
text-align: left;
|
|
padding: 0.45rem 0.6rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}}
|
|
table.registry th, table.detail th {{
|
|
color: var(--muted);
|
|
font-weight: 500;
|
|
}}
|
|
code {{
|
|
font-family: ui-monospace, monospace;
|
|
font-size: 0.85em;
|
|
color: var(--text);
|
|
}}
|
|
ol.checklist {{
|
|
padding-left: 1.25rem;
|
|
margin: 0.5rem 0 1.5rem;
|
|
}}
|
|
ol.checklist li {{ margin-bottom: 0.85rem; }}
|
|
ol.checklist p {{ margin: 0.25rem 0 0; font-size: 0.9rem; }}
|
|
.prompt-card {{
|
|
margin: 1.25rem 0 1.75rem;
|
|
padding: 1rem 1.1rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background: var(--surface);
|
|
}}
|
|
.prompt-card h3 {{ margin: 0 0 0.5rem; font-size: 1.05rem; }}
|
|
pre.prompt-text {{
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
margin: 0.75rem 0;
|
|
padding: 0.75rem 0.85rem;
|
|
border-radius: 6px;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
font-size: 0.9rem;
|
|
color: var(--text);
|
|
}}
|
|
.copy-btn {{
|
|
background: var(--accent);
|
|
color: #0b1219;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 0.4rem 0.85rem;
|
|
font-size: 0.85rem;
|
|
cursor: pointer;
|
|
}}
|
|
.copy-btn:hover {{ filter: brightness(1.08); }}
|
|
.muted {{ color: var(--muted); }}
|
|
.badges {{ display: inline-flex; flex-wrap: wrap; gap: 0.35rem; margin-left: 0.5rem; }}
|
|
.badge {{
|
|
font-size: 0.72rem;
|
|
padding: 0.1rem 0.45rem;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--border);
|
|
color: var(--muted);
|
|
text-transform: lowercase;
|
|
}}
|
|
.badge-claimed {{ color: #8fd19e; border-color: #3d6b4a; }}
|
|
.badge-blocked {{ color: #f0a8a8; border-color: #7a3b3b; }}
|
|
.badge-in-review {{ color: #9ec8f0; border-color: #3d5f7a; }}
|
|
.badge-duplicate {{ color: #e0c27a; border-color: #6b5730; }}
|
|
.badge-stale {{ color: #c9b8e8; border-color: #5a4a78; }}
|
|
.badge-health-ok {{ color: #8fd19e; border-color: #3d6b4a; }}
|
|
.badge-health-degraded {{ color: #e0c27a; border-color: #6b5730; }}
|
|
.badge-health-down {{ color: #f0a8a8; border-color: #7a3b3b; }}
|
|
.badge-health-skipped {{ color: var(--muted); }}
|
|
.badge-health-unproven {{ color: #c9b8e8; border-color: #5a4a78; }}
|
|
.health-card {{
|
|
margin: 1.25rem 0;
|
|
padding: 0.85rem 1rem 1rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background: var(--surface);
|
|
}}
|
|
.health-card h3 {{ margin: 0 0 0.5rem; font-size: 1.05rem; }}
|
|
.health-card h4 {{ margin: 1rem 0 0.35rem; font-size: 0.92rem; color: var(--muted); }}
|
|
.health-headline {{ color: var(--text); font-size: 1rem; margin: 0 0 0.5rem; }}
|
|
.health-degraded {{ border-left-color: #e0c27a; }}
|
|
.health-stale {{ border-left-color: #f0a8a8; }}
|
|
ul.reasons {{ margin: 0.35rem 0; padding-left: 1.15rem; color: var(--muted); font-size: 0.9rem; }}
|
|
ul.reasons li {{ margin-bottom: 0.3rem; }}
|
|
</style>
|
|
{extra_head}
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>MCP Control Plane</h1>
|
|
<nav>{nav_links}</nav>
|
|
</header>
|
|
<main>
|
|
<p class="notice">{MVP_NOTICE}</p>
|
|
{body_html}
|
|
</main>
|
|
</body>
|
|
</html>""" |