"""HTML view renderer for the Sentry/GlitchTip observability console (#649, Phase 4). Renders connection status widgets, error correlation links, and gated issue creation affordances over the read-only observability snapshot. """ from __future__ import annotations import html from typing import Any from webui.layout import render_page from webui.observability_loader import ObservabilitySnapshot, snapshot_to_dict def _badge(status: str) -> str: st = (status or "").lower() if st == "healthy": return 'healthy' if st == "disabled": return 'disabled (dry-run)' if st in {"missing_token", "not_configured"}: return f'{html.escape(st)}' return f'{html.escape(st)}' def _provider_card(p: dict[str, Any]) -> str: name = html.escape(str(p.get("provider", "provider")).upper()) base_url = html.escape(str(p.get("base_url", ""))) org = html.escape(str(p.get("org", ""))) proj = html.escape(str(p.get("project", ""))) status_badge = _badge(str(p.get("status", ""))) min_events = p.get("min_events_for_issue", 2) lookback = html.escape(str(p.get("lookback", "24h"))) bridge_enabled = "yes" if p.get("bridge_enabled") else "no" return f"""

{name} Connection

{status_badge}
Base URL:{base_url}
Scope:{org} / {proj}
Bridge Enabled:{bridge_enabled}
Min Events for Issue:{min_events}
Lookback Window:{lookback}
""" def render_observability_page(snapshot: ObservabilitySnapshot | dict[str, Any]) -> str: """Render the observability dashboard HTML page.""" data = snapshot.to_dict() if isinstance(snapshot, ObservabilitySnapshot) else dict(snapshot) providers_raw = data.get("providers", []) provider_cards = "".join(_provider_card(p) for p in providers_raw) if providers_raw else "

No providers configured.

" links = data.get("links", []) link_rows = [] for l in links: prov = html.escape(str(l.get("provider", ""))) p_issue_id = html.escape(str(l.get("provider_issue_id", ""))) fingerprint = html.escape(str(l.get("fingerprint") or "—")) g_issue_num = int(l.get("gitea_issue_number", 0)) g_org = html.escape(str(l.get("gitea_org", ""))) g_repo = html.escape(str(l.get("gitea_repo", ""))) g_issue_link = f'#{g_issue_num} ({g_org}/{g_repo})' event_cnt = int(l.get("event_count", 1)) last_seen = html.escape(str(l.get("last_seen") or "—")) short_id = html.escape(str(l.get("provider_short_id") or p_issue_id)) link_rows.append(f""" {prov} {short_id}
id: {p_issue_id} {fingerprint} {g_issue_link} {event_cnt} {last_seen} """) table_body = "".join(link_rows) if link_rows else 'No correlated incident links stored. Bridge operates under dry-run default.' metrics = data.get("metrics", {}) total_links = metrics.get("total_links", 0) sentry_cnt = metrics.get("sentry_links_count", 0) glitchtip_cnt = metrics.get("glitchtip_links_count", 0) body_html = f"""

Observability & Incident Bridge (#649)

Read-only console surface for Sentry/GlitchTip provider connections, error correlation, and durable Gitea issue linkage.

ADR Authority Model: Gitea records durable issue history. Control-plane DB coordinates incident links. Sentry/GlitchTip observe errors. Raw monitoring incidents are never assignable control-plane work items. Durable issue creation is gated and dry-runable via the #612 bridge APIs.

Provider Connections

{provider_cards}

Correlated Incidents ({total_links})

Sentry: {sentry_cnt} GlitchTip: {glitchtip_cnt}
{table_body}
Provider Incident ID Fingerprint Gitea Issue Link Events Last Seen

Reconcile & Link Controls (Gated)

Create or reconcile durable Gitea issues from provider observations using the #612 incident bridge:

mcp call gitea_observability_reconcile_incident --provider sentry --apply false
""" return render_page(title="Observability", body_html=body_html)