Add Phase 4 advisory surfaces for declared AI-provider connection status (no secrets, no live probe claims) and operational insights derived only from durable traffic, health, provider, and analytics evidence. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
197 lines
7.1 KiB
Python
197 lines
7.1 KiB
Python
"""HTML views for AI-provider connections and operational insights (#650)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from html import escape
|
|
|
|
from webui.insights_loader import (
|
|
CONNECTION_DECLARED_AVAILABLE,
|
|
CONNECTION_DECLARED_UNAVAILABLE,
|
|
InsightsSnapshot,
|
|
ProviderSnapshot,
|
|
SEVERITY_CRITICAL,
|
|
SEVERITY_INFO,
|
|
SEVERITY_UNPROVEN,
|
|
SEVERITY_WARN,
|
|
)
|
|
from webui.layout import render_page
|
|
|
|
_SEVERITY_CSS = {
|
|
SEVERITY_CRITICAL: "badge-blocked",
|
|
SEVERITY_WARN: "badge-health-degraded",
|
|
SEVERITY_INFO: "badge-health-ok",
|
|
SEVERITY_UNPROVEN: "badge-health-unproven",
|
|
}
|
|
|
|
_CONN_CSS = {
|
|
CONNECTION_DECLARED_AVAILABLE: "badge-health-ok",
|
|
CONNECTION_DECLARED_UNAVAILABLE: "badge-health-degraded",
|
|
"registry_unavailable": "badge-blocked",
|
|
}
|
|
|
|
|
|
def _badge(text: str, css: str) -> str:
|
|
return f'<span class="badge {css}">{escape(text)}</span>'
|
|
|
|
|
|
def _limits_card(lines: list[str], *, title: str) -> str:
|
|
items = "".join(f"<li>{escape(line)}</li>" for line in lines)
|
|
return f"""<div class="prompt-card">
|
|
<h3>{escape(title)}</h3>
|
|
<ul class="reasons">{items}</ul>
|
|
<p class="muted">Advisory surface only — no review, merge, close, or provider
|
|
mutation is available here.</p>
|
|
</div>"""
|
|
|
|
|
|
def render_providers_page(snapshot: ProviderSnapshot) -> str:
|
|
"""Render the AI-provider connections page."""
|
|
if not snapshot.ok:
|
|
body = f"""<h2>AI provider connections</h2>
|
|
<p class="meta">Phase 4 read-only provider status (#650).</p>
|
|
<div class="health-card health-stale">
|
|
<strong>Provider registry unavailable:</strong>
|
|
{escape(snapshot.fetch_error or "registry could not be loaded")}.
|
|
No connection table is rendered — an empty table would claim that no
|
|
providers are configured.
|
|
</div>
|
|
{_limits_card([
|
|
"connection_status reflects the worker registry declaration only",
|
|
"no API keys or credential material are loaded or rendered",
|
|
"live executable health is not probed on this surface",
|
|
], title="Interpretation limits")}
|
|
"""
|
|
return render_page(title="Providers", body_html=body)
|
|
|
|
rows = []
|
|
for provider in snapshot.providers:
|
|
models = (
|
|
", ".join(f"<code>{escape(m)}</code>" for m in provider.models)
|
|
if provider.models
|
|
else '<span class="muted">none declared</span>'
|
|
)
|
|
rows.append(
|
|
"<tr>"
|
|
f"<td><code>{escape(provider.provider_id)}</code></td>"
|
|
f"<td>{escape(provider.display_name)}</td>"
|
|
f"<td>{escape(provider.vendor)}</td>"
|
|
f"<td><code>{escape(provider.executable)}</code></td>"
|
|
f"<td>{_badge(provider.connection_status, _CONN_CSS.get(provider.connection_status, 'badge-health-skipped'))}</td>"
|
|
f"<td>{provider.worker_count} "
|
|
f"({provider.enabled_worker_count} enabled)</td>"
|
|
f"<td>{models}</td>"
|
|
"</tr>"
|
|
)
|
|
table = (
|
|
"".join(rows)
|
|
if rows
|
|
else '<tr><td colspan="7" class="muted">No providers declared in the registry.</td></tr>'
|
|
)
|
|
|
|
body = f"""<h2>AI provider connections</h2>
|
|
<p class="meta">Phase 4 read-only provider status (#650). Registry revision
|
|
<code>{escape(str(snapshot.registry_revision))}</code>.
|
|
Declared status only — secrets never load.</p>
|
|
|
|
<div class="health-card">
|
|
<h3>Declared connections</h3>
|
|
<table class="registry">
|
|
<thead>
|
|
<tr>
|
|
<th>Provider</th><th>Name</th><th>Vendor</th><th>Executable</th>
|
|
<th>Connection</th><th>Workers</th><th>Models (declared)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>{table}</tbody>
|
|
</table>
|
|
<p class="muted">{escape(snapshot.providers[0].probe_limit if snapshot.providers else "")}</p>
|
|
</div>
|
|
|
|
{_limits_card([
|
|
"connection_status reflects the worker registry declaration only",
|
|
"no API keys or credential material are loaded or rendered",
|
|
"live executable health is not probed on this surface (#800 owns that)",
|
|
], title="Interpretation limits")}
|
|
<p class="muted">Related: <a href="/insights">Operational insights</a> ·
|
|
<a href="/analytics">Analytics</a></p>
|
|
"""
|
|
return render_page(title="Providers", body_html=body)
|
|
|
|
|
|
def _evidence_list(insight) -> str:
|
|
items = "".join(
|
|
f"<li><code>{escape(ref.kind)}:{escape(ref.ref)}</code> — "
|
|
f"{escape(ref.detail)}</li>"
|
|
for ref in insight.evidence
|
|
)
|
|
return f'<ul class="reasons">{items}</ul>'
|
|
|
|
|
|
def render_insights_page(snapshot: InsightsSnapshot) -> str:
|
|
"""Render the operational insights page."""
|
|
if not snapshot.ok and not snapshot.insights:
|
|
body = f"""<h2>Operational insights</h2>
|
|
<p class="meta">Phase 4 evidence-backed insights (#650).</p>
|
|
<div class="health-card health-stale">
|
|
<strong>Insights unavailable:</strong>
|
|
{escape(snapshot.fetch_error or "no sources ran")}.
|
|
</div>
|
|
{_limits_card([
|
|
"insights are advisory only and never authorize merge, review, or close",
|
|
"an insight without evidence refs is refused rather than emitted",
|
|
], title="Interpretation limits")}
|
|
"""
|
|
return render_page(title="Insights", body_html=body)
|
|
|
|
source_bits = []
|
|
if snapshot.sources_used:
|
|
source_bits.append(
|
|
"sources used: " + ", ".join(f"<code>{escape(s)}</code>" for s in snapshot.sources_used)
|
|
)
|
|
if snapshot.sources_unavailable:
|
|
missing = "; ".join(
|
|
f"{escape(item.get('source', '?'))}: {escape(item.get('reason', ''))}"
|
|
for item in snapshot.sources_unavailable
|
|
)
|
|
source_bits.append(f"sources unavailable: {missing}")
|
|
|
|
cards = []
|
|
for insight in snapshot.insights:
|
|
cards.append(
|
|
f"""<div class="prompt-card">
|
|
<h3>{_badge(insight.severity, _SEVERITY_CSS.get(insight.severity, "badge-health-skipped"))}
|
|
{escape(insight.title)}</h3>
|
|
<p class="meta"><code>{escape(insight.kind)}</code> · confidence
|
|
<code>{escape(insight.confidence)}</code> ·
|
|
{_badge("advisory only", "badge-health-skipped")} ·
|
|
{_badge("no action claimed", "badge-health-ok")}</p>
|
|
<p>{escape(insight.summary)}</p>
|
|
<h4>Evidence</h4>
|
|
{_evidence_list(insight)}
|
|
</div>"""
|
|
)
|
|
if not cards:
|
|
cards.append(
|
|
'<div class="prompt-card"><p class="muted">No insights met the '
|
|
"evidence threshold in the loaded sources. That is not a claim "
|
|
"that the fleet is healthy — only that no qualifying pattern was "
|
|
"found.</p></div>"
|
|
)
|
|
|
|
body = f"""<h2>Operational insights</h2>
|
|
<p class="meta">Phase 4 evidence-backed insights (#650). Derived only from
|
|
durable console evidence; never invents policy or completes workflow actions.</p>
|
|
<p class="muted">{" · ".join(source_bits) if source_bits else ""}</p>
|
|
{"".join(cards)}
|
|
{_limits_card([
|
|
"insights are advisory only and never authorize merge, review, or close",
|
|
"an insight without evidence refs is refused rather than emitted",
|
|
"a missing source is listed as unavailable, not as an empty success",
|
|
"insights never claim a workflow action completed",
|
|
], title="Interpretation limits")}
|
|
<p class="muted">Related: <a href="/providers">Provider connections</a> ·
|
|
<a href="/traffic">Traffic</a> · <a href="/system-health">System health</a> ·
|
|
<a href="/analytics">Analytics</a></p>
|
|
"""
|
|
return render_page(title="Insights", body_html=body)
|