teleo-codex/ops/diagnostics/dashboard_routes.py
m3taversal 05d74d5e32 sync: import all VPS pipeline + diagnostics code as baseline
Imports 67 files from VPS (/opt/teleo-eval/) into repo as the single source
of truth. Previously only 8 of 67 files existed in repo — the rest were
deployed directly to VPS via SCP, causing massive drift.

Includes:
- pipeline/lib/: 33 Python modules (daemon core, extraction, evaluation, merge, cascade, cross-domain, costs, attribution, etc.)
- pipeline/: main daemon (teleo-pipeline.py), reweave.py, batch-extract-50.sh
- diagnostics/: 19 files (4-page dashboard, alerting, daily digest, review queue, tier1 metrics)
- agent-state/: bootstrap, lib-state, cascade inbox processor, schema
- systemd/: service unit files for reference
- deploy.sh: rsync-based deploy with --dry-run, syntax checks, dirty-tree gate
- research-session.sh: updated with Step 8.5 digest + cascade inbox processing

No new code written — all files are exact copies from VPS as of 2026-04-06.
From this point forward: edit in repo, commit, then deploy.sh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 00:00:00 +01:00

929 lines
34 KiB
Python

"""New API endpoints for the 4-page dashboard.
Endpoints:
GET /api/stage-times — median dwell time per pipeline stage
GET /api/herfindahl — domain concentration index
GET /api/agent-state — live agent-state from filesystem
GET /api/extraction-yield-by-domain — sources→claims conversion per domain
GET /api/agents-dashboard — batched agent performance payload
Owner: Argus
"""
import json
import logging
import os
import sqlite3
import statistics
import time
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from aiohttp import web
logger = logging.getLogger("argus.dashboard_routes")
# ─── Claim-index cache (60s TTL) ───────────────────────────────────────────
_claim_index_cache: dict | None = None
_claim_index_ts: float = 0
CLAIM_INDEX_TTL = 60 # seconds
CLAIM_INDEX_URL = os.environ.get("CLAIM_INDEX_URL", "http://localhost:8080/claim-index")
AGENT_STATE_DIR = Path(os.environ.get("AGENT_STATE_DIR", "/opt/teleo-eval/agent-state"))
def get_claim_index() -> dict | None:
"""Fetch claim-index with 60s cache."""
global _claim_index_cache, _claim_index_ts
now = time.monotonic()
if _claim_index_cache is not None and (now - _claim_index_ts) < CLAIM_INDEX_TTL:
return _claim_index_cache
try:
with urllib.request.urlopen(CLAIM_INDEX_URL, timeout=5) as resp:
data = json.loads(resp.read())
_claim_index_cache = data
_claim_index_ts = now
return data
except Exception as e:
logger.warning("Failed to fetch claim-index: %s", e)
# Return stale cache if available
return _claim_index_cache
# ─── GET /api/stage-times ──────────────────────────────────────────────────
async def handle_stage_times(request):
"""Median dwell time per pipeline stage from audit_log timestamps.
Stages: discover → validate → evaluate → merge
Returns median minutes between consecutive stages.
"""
conn = request.app["_get_conn"]()
hours = int(request.query.get("hours", "24"))
# Get per-PR event timestamps
rows = conn.execute(
"""SELECT json_extract(detail, '$.pr') as pr, event, timestamp
FROM audit_log
WHERE timestamp > datetime('now', ? || ' hours')
AND json_extract(detail, '$.pr') IS NOT NULL
ORDER BY json_extract(detail, '$.pr'), timestamp""",
(f"-{hours}",),
).fetchall()
# Group by PR
pr_events: dict[int, list] = {}
for r in rows:
pr = r["pr"]
if pr not in pr_events:
pr_events[pr] = []
pr_events[pr].append({"event": r["event"], "ts": r["timestamp"]})
# Compute stage dwell times
stage_pairs = [
("pr_discovered", "tier0_complete", "Ingest → Validate"),
("tier0_complete", "approved", "Validate → Approve"),
("tier0_complete", "domain_rejected", "Validate → Reject"),
("approved", "merged", "Approve → Merge"),
]
stage_times = {}
for start_event, end_event, label in stage_pairs:
durations = []
for pr, events in pr_events.items():
start_ts = None
end_ts = None
for e in events:
if e["event"] == start_event and start_ts is None:
start_ts = e["ts"]
if e["event"] == end_event and end_ts is None:
end_ts = e["ts"]
if start_ts and end_ts:
try:
s = datetime.fromisoformat(start_ts)
e = datetime.fromisoformat(end_ts)
mins = (e - s).total_seconds() / 60
if mins >= 0:
durations.append(mins)
except (ValueError, TypeError):
pass
if durations:
stage_times[label] = {
"median_minutes": round(statistics.median(durations), 1),
"p90_minutes": round(sorted(durations)[int(len(durations) * 0.9)], 1) if len(durations) >= 5 else None,
"count": len(durations),
}
return web.json_response({"hours": hours, "stages": stage_times})
# ─── GET /api/herfindahl ──────────────────────────────────────────────────
async def handle_herfindahl(request):
"""Domain concentration index (Herfindahl-Hirschman).
HHI = sum of (domain_share^2). 1.0 = single domain, lower = more diverse.
"""
conn = request.app["_get_conn"]()
days = int(request.query.get("days", "30"))
rows = conn.execute(
"""SELECT domain, COUNT(*) as cnt
FROM prs WHERE status='merged' AND domain IS NOT NULL
AND merged_at > datetime('now', ? || ' days')
GROUP BY domain""",
(f"-{days}",),
).fetchall()
if not rows:
return web.json_response({"hhi": 0, "domains": [], "days": days})
total = sum(r["cnt"] for r in rows)
domains = []
hhi = 0
for r in rows:
share = r["cnt"] / total
hhi += share ** 2
domains.append({
"domain": r["domain"],
"count": r["cnt"],
"share": round(share, 4),
})
domains.sort(key=lambda x: x["count"], reverse=True)
# Interpret: HHI < 0.15 = diverse, 0.15-0.25 = moderate, >0.25 = concentrated
status = "diverse" if hhi < 0.15 else ("moderate" if hhi < 0.25 else "concentrated")
return web.json_response({
"hhi": round(hhi, 4),
"status": status,
"domains": domains,
"total_merged": total,
"days": days,
})
# ─── GET /api/agent-state ─────────────────────────────────────────────────
async def handle_agent_state(request):
"""Read live agent-state from filesystem. 6 agents, ~1KB each."""
if not AGENT_STATE_DIR.exists():
return web.json_response({"error": "agent-state directory not found", "path": str(AGENT_STATE_DIR)}, status=404)
agents = {}
for agent_dir in sorted(AGENT_STATE_DIR.iterdir()):
if not agent_dir.is_dir():
continue
name = agent_dir.name
state = {"name": name}
# metrics.json
metrics_file = agent_dir / "metrics.json"
if metrics_file.exists():
try:
m = json.loads(metrics_file.read_text())
state["last_active"] = m.get("updated_at")
state["metrics"] = m
except (json.JSONDecodeError, OSError):
state["metrics_error"] = True
# tasks.json
tasks_file = agent_dir / "tasks.json"
if tasks_file.exists():
try:
t = json.loads(tasks_file.read_text())
state["tasks"] = t if isinstance(t, list) else []
state["task_count"] = len(state["tasks"])
except (json.JSONDecodeError, OSError):
state["tasks"] = []
# session.json
session_file = agent_dir / "session.json"
if session_file.exists():
try:
s = json.loads(session_file.read_text())
state["session"] = s
except (json.JSONDecodeError, OSError):
pass
# inbox depth
inbox_dir = agent_dir / "inbox"
if inbox_dir.exists() and inbox_dir.is_dir():
state["inbox_depth"] = len(list(inbox_dir.iterdir()))
else:
state["inbox_depth"] = 0
agents[name] = state
return web.json_response({"agents": agents, "agent_count": len(agents)})
# ─── GET /api/extraction-yield-by-domain ──────────────────────────────────
async def handle_extraction_yield_by_domain(request):
"""Sources → claims conversion rate per domain."""
conn = request.app["_get_conn"]()
days = int(request.query.get("days", "30"))
# Sources per domain (approximate from PR source_path domain)
source_counts = conn.execute(
"""SELECT domain, COUNT(DISTINCT source_url) as sources
FROM sources s
JOIN prs p ON p.source_path LIKE '%' || s.url || '%'
WHERE s.created_at > datetime('now', ? || ' days')
GROUP BY domain""",
(f"-{days}",),
).fetchall()
# Fallback: simpler query if the join doesn't work well
merged_by_domain = conn.execute(
"""SELECT domain, COUNT(*) as merged
FROM prs WHERE status='merged' AND domain IS NOT NULL
AND merged_at > datetime('now', ? || ' days')
GROUP BY domain""",
(f"-{days}",),
).fetchall()
sources_by_domain = conn.execute(
"""SELECT domain, COUNT(*) as total_prs,
SUM(CASE WHEN status='merged' THEN 1 ELSE 0 END) as merged
FROM prs WHERE domain IS NOT NULL
AND created_at > datetime('now', ? || ' days')
GROUP BY domain""",
(f"-{days}",),
).fetchall()
domains = []
for r in sources_by_domain:
total = r["total_prs"] or 0
merged = r["merged"] or 0
domains.append({
"domain": r["domain"],
"total_prs": total,
"merged": merged,
"yield": round(merged / total, 3) if total else 0,
})
domains.sort(key=lambda x: x["merged"], reverse=True)
return web.json_response({"days": days, "domains": domains})
# ─── GET /api/agents-dashboard ─────────────────────────────────────────────
async def handle_agents_dashboard(request):
"""Batched agent performance payload for Page 3.
Returns per-agent: merged count, rejection rate, yield, CI score,
top rejection reasons, contribution trend (weekly).
All in one response to avoid N client-side fetches.
"""
conn = request.app["_get_conn"]()
days = int(request.query.get("days", "30"))
# Per-agent merged + rejected counts
agent_stats = conn.execute(
"""SELECT
COALESCE(json_extract(detail, '$.agent'), json_extract(detail, '$.domain_agent')) as agent,
COUNT(*) as evaluated,
SUM(CASE WHEN event='approved' THEN 1 ELSE 0 END) as approved,
SUM(CASE WHEN event IN ('changes_requested','domain_rejected','tier05_rejected') THEN 1 ELSE 0 END) as rejected
FROM audit_log
WHERE stage='evaluate'
AND event IN ('approved','changes_requested','domain_rejected','tier05_rejected')
AND timestamp > datetime('now', ? || ' days')
AND COALESCE(json_extract(detail, '$.agent'), json_extract(detail, '$.domain_agent')) IS NOT NULL
GROUP BY agent""",
(f"-{days}",),
).fetchall()
agents = {}
for r in agent_stats:
name = r["agent"]
ev = r["evaluated"] or 0
ap = r["approved"] or 0
rj = r["rejected"] or 0
agents[name] = {
"evaluated": ev,
"approved": ap,
"rejected": rj,
"yield": round(ap / ev, 3) if ev else 0,
"rejection_rate": round(rj / ev, 3) if ev else 0,
}
# Per-agent top rejection reasons from prs.eval_issues (Epimetheus correction 2026-04-02)
tag_rows = conn.execute(
"""SELECT agent, value as tag, COUNT(*) as cnt
FROM prs, json_each(prs.eval_issues)
WHERE eval_issues IS NOT NULL AND eval_issues != '[]'
AND agent IS NOT NULL
AND created_at > datetime('now', ? || ' days')
GROUP BY agent, tag
ORDER BY agent, cnt DESC""",
(f"-{days}",),
).fetchall()
for r in tag_rows:
name = r["agent"]
if name in agents:
if "top_rejections" not in agents[name]:
agents[name]["top_rejections"] = []
if len(agents[name]["top_rejections"]) < 5:
agents[name]["top_rejections"].append({"tag": r["tag"], "count": r["cnt"]})
# Weekly contribution trend per agent
weekly = conn.execute(
"""SELECT
COALESCE(json_extract(detail, '$.agent'), json_extract(detail, '$.domain_agent')) as agent,
strftime('%Y-W%W', timestamp) as week,
SUM(CASE WHEN event='approved' THEN 1 ELSE 0 END) as merged,
COUNT(*) as evaluated
FROM audit_log
WHERE stage='evaluate'
AND event IN ('approved','changes_requested','domain_rejected','tier05_rejected')
AND timestamp > datetime('now', ? || ' days')
AND COALESCE(json_extract(detail, '$.agent'), json_extract(detail, '$.domain_agent')) IS NOT NULL
GROUP BY agent, week
ORDER BY agent, week""",
(f"-{days}",),
).fetchall()
for r in weekly:
name = r["agent"]
if name in agents:
if "weekly_trend" not in agents[name]:
agents[name]["weekly_trend"] = []
agents[name]["weekly_trend"].append({
"week": r["week"],
"merged": r["merged"] or 0,
"evaluated": r["evaluated"] or 0,
})
# CI scores from contributors table
weights = {"sourcer": 0.15, "extractor": 0.05, "challenger": 0.35, "synthesizer": 0.25, "reviewer": 0.20}
try:
contribs = conn.execute(
"SELECT handle, sourcer_count, extractor_count, challenger_count, "
"synthesizer_count, reviewer_count, claims_merged, tier FROM contributors"
).fetchall()
for c in contribs:
name = c["handle"]
if name not in agents:
agents[name] = {}
ci = sum((c[f"{role}_count"] or 0) * w for role, w in weights.items())
agents[name]["ci_score"] = round(ci, 2)
agents[name]["claims_merged"] = c["claims_merged"] or 0
agents[name]["tier"] = c["tier"]
except sqlite3.Error:
pass
return web.json_response({"days": days, "agents": agents})
# ─── GET /api/cascade-coverage ────────────────────────────────────────────
async def handle_cascade_coverage(request):
"""Cascade coverage from audit_log stage='cascade' events.
Returns: triggered count, by-agent breakdown, claims affected.
"""
conn = request.app["_get_conn"]()
days = int(request.query.get("days", "30"))
triggered = conn.execute(
"""SELECT
json_extract(detail, '$.agent') as agent,
COUNT(*) as cnt,
SUM(json_array_length(json_extract(detail, '$.source_claims'))) as claims_affected
FROM audit_log
WHERE stage='cascade' AND event='cascade_triggered'
AND timestamp > datetime('now', ? || ' days')
GROUP BY agent""",
(f"-{days}",),
).fetchall()
summaries = conn.execute(
"""SELECT
SUM(json_extract(detail, '$.notifications_sent')) as total_notifications,
COUNT(*) as total_merges_with_cascade
FROM audit_log
WHERE stage='cascade' AND event='cascade_summary'
AND timestamp > datetime('now', ? || ' days')""",
(f"-{days}",),
).fetchone()
reviewed = conn.execute(
"""SELECT COUNT(*) as cnt
FROM audit_log
WHERE stage='cascade' AND event='cascade_reviewed'
AND timestamp > datetime('now', ? || ' days')""",
(f"-{days}",),
).fetchone()
total_triggered = sum(r["cnt"] for r in triggered)
total_reviewed = reviewed["cnt"] if reviewed else 0
completion_rate = round(total_reviewed / total_triggered, 3) if total_triggered else None
by_agent = [
{"agent": r["agent"], "triggered": r["cnt"], "claims_affected": r["claims_affected"] or 0}
for r in triggered
]
return web.json_response({
"days": days,
"total_triggered": total_triggered,
"total_reviewed": total_reviewed,
"completion_rate": completion_rate,
"total_notifications": summaries["total_notifications"] if summaries else 0,
"merges_with_cascade": summaries["total_merges_with_cascade"] if summaries else 0,
"by_agent": by_agent,
})
# ─── GET /api/review-summary ─────────────────────────────────────────────
async def handle_review_summary(request):
"""Structured review data from review_records table (migration v12).
Cleaner than audit_log parsing — structured outcome, rejection_reason,
disagreement_type columns.
"""
conn = request.app["_get_conn"]()
days = int(request.query.get("days", "30"))
# Check if table exists and has data
try:
total = conn.execute(
"SELECT COUNT(*) as cnt FROM review_records WHERE reviewed_at > datetime('now', ? || ' days')",
(f"-{days}",),
).fetchone()["cnt"]
except Exception:
return web.json_response({"error": "review_records table not available", "populated": False})
if total == 0:
return web.json_response({"populated": False, "total": 0, "days": days})
# Outcome breakdown
outcomes = conn.execute(
"""SELECT outcome, COUNT(*) as cnt
FROM review_records
WHERE reviewed_at > datetime('now', ? || ' days')
GROUP BY outcome""",
(f"-{days}",),
).fetchall()
# Rejection reasons
reasons = conn.execute(
"""SELECT rejection_reason, COUNT(*) as cnt
FROM review_records
WHERE rejection_reason IS NOT NULL
AND reviewed_at > datetime('now', ? || ' days')
GROUP BY rejection_reason ORDER BY cnt DESC""",
(f"-{days}",),
).fetchall()
# Disagreement types
disagreements = conn.execute(
"""SELECT disagreement_type, COUNT(*) as cnt
FROM review_records
WHERE disagreement_type IS NOT NULL
AND reviewed_at > datetime('now', ? || ' days')
GROUP BY disagreement_type ORDER BY cnt DESC""",
(f"-{days}",),
).fetchall()
# Per-reviewer breakdown
reviewers = conn.execute(
"""SELECT reviewer,
SUM(CASE WHEN outcome='approved' THEN 1 ELSE 0 END) as approved,
SUM(CASE WHEN outcome='approved-with-changes' THEN 1 ELSE 0 END) as approved_with_changes,
SUM(CASE WHEN outcome='rejected' THEN 1 ELSE 0 END) as rejected,
COUNT(*) as total
FROM review_records
WHERE reviewed_at > datetime('now', ? || ' days')
GROUP BY reviewer ORDER BY total DESC""",
(f"-{days}",),
).fetchall()
# Per-domain breakdown
domains = conn.execute(
"""SELECT domain,
SUM(CASE WHEN outcome='rejected' THEN 1 ELSE 0 END) as rejected,
COUNT(*) as total
FROM review_records
WHERE domain IS NOT NULL
AND reviewed_at > datetime('now', ? || ' days')
GROUP BY domain ORDER BY total DESC""",
(f"-{days}",),
).fetchall()
return web.json_response({
"populated": True,
"days": days,
"total": total,
"outcomes": {r["outcome"]: r["cnt"] for r in outcomes},
"rejection_reasons": [{"reason": r["rejection_reason"], "count": r["cnt"]} for r in reasons],
"disagreement_types": [{"type": r["disagreement_type"], "count": r["cnt"]} for r in disagreements],
"reviewers": [
{"reviewer": r["reviewer"], "approved": r["approved"], "approved_with_changes": r["approved_with_changes"],
"rejected": r["rejected"], "total": r["total"]}
for r in reviewers
],
"domains": [
{"domain": r["domain"], "rejected": r["rejected"], "total": r["total"],
"rejection_rate": round(r["rejected"] / r["total"], 3) if r["total"] else 0}
for r in domains
],
})
# ─── Trace endpoint ────────────────────────────────────────────────────────
async def handle_trace(request: web.Request) -> web.Response:
"""Return the full lifecycle of a source/PR through the pipeline.
GET /api/trace/1234 → all audit_log + review_records + costs for PR 1234.
One thread, every stage, chronological.
"""
trace_id = request.match_info["trace_id"]
get_conn = request.app["_get_conn"]
conn = get_conn()
# Audit log events (the backbone)
# Try trace_id first, fall back to PR number in detail JSON
events = conn.execute(
"""SELECT timestamp, stage, event, detail
FROM audit_log
WHERE trace_id = ?
ORDER BY timestamp""",
(trace_id,),
).fetchall()
if not events:
# Fallback: match by PR number in detail JSON (for rows without trace_id)
events = conn.execute(
"""SELECT timestamp, stage, event, detail
FROM audit_log
WHERE CAST(json_extract(detail, '$.pr') AS TEXT) = ?
ORDER BY timestamp""",
(trace_id,),
).fetchall()
# Review records for this PR
reviews = conn.execute(
"""SELECT reviewed_at, reviewer, reviewer_model, outcome,
rejection_reason, disagreement_type, notes, claim_path
FROM review_records
WHERE pr_number = ?
ORDER BY reviewed_at""",
(trace_id,),
).fetchall()
# PR metadata
pr = conn.execute(
"""SELECT number, source_path, domain, agent, tier, status,
origin, created_at, merged_at
FROM prs
WHERE number = ?""",
(trace_id,),
).fetchone()
result = {
"trace_id": trace_id,
"pr": dict(pr) if pr else None,
"timeline": [
{"timestamp": r[0], "stage": r[1], "event": r[2],
"detail": json.loads(r[3]) if r[3] else None}
for r in events
],
"reviews": [
{"reviewed_at": r[0], "reviewer": r[1], "model": r[2],
"outcome": r[3], "rejection_reason": r[4],
"disagreement_type": r[5], "notes": r[6], "claim_path": r[7]}
for r in reviews
],
}
return web.json_response(result)
# ─── GET /api/growth ──────────────────────────────────────────────────────
async def handle_growth(request):
"""Cumulative growth of sources, PRs, and merged claims over time.
Returns daily data points with running totals for each series.
"""
conn = request.app["_get_conn"]()
days = int(request.query.get("days", "90"))
# Daily new sources
source_rows = conn.execute(
"""SELECT date(created_at) as day, COUNT(*) as cnt
FROM sources
WHERE created_at > datetime('now', ? || ' days')
GROUP BY day ORDER BY day""",
(f"-{days}",),
).fetchall()
# Daily new PRs
pr_rows = conn.execute(
"""SELECT date(created_at) as day, COUNT(*) as cnt
FROM prs
WHERE created_at > datetime('now', ? || ' days')
GROUP BY day ORDER BY day""",
(f"-{days}",),
).fetchall()
# Daily merged PRs
merged_rows = conn.execute(
"""SELECT date(merged_at) as day, COUNT(*) as cnt
FROM prs
WHERE status = 'merged' AND merged_at IS NOT NULL
AND merged_at > datetime('now', ? || ' days')
GROUP BY day ORDER BY day""",
(f"-{days}",),
).fetchall()
# Get totals BEFORE the window for correct cumulative baseline
source_base = conn.execute(
"SELECT COUNT(*) as cnt FROM sources WHERE created_at <= datetime('now', ? || ' days')",
(f"-{days}",),
).fetchone()["cnt"]
pr_base = conn.execute(
"SELECT COUNT(*) as cnt FROM prs WHERE created_at <= datetime('now', ? || ' days')",
(f"-{days}",),
).fetchone()["cnt"]
merged_base = conn.execute(
"""SELECT COUNT(*) as cnt FROM prs
WHERE status = 'merged' AND merged_at IS NOT NULL
AND merged_at <= datetime('now', ? || ' days')""",
(f"-{days}",),
).fetchone()["cnt"]
# Collect all unique dates
all_dates = sorted(set(
[r["day"] for r in source_rows] +
[r["day"] for r in pr_rows] +
[r["day"] for r in merged_rows]
))
# Build lookup dicts
src_by_day = {r["day"]: r["cnt"] for r in source_rows}
pr_by_day = {r["day"]: r["cnt"] for r in pr_rows}
mrg_by_day = {r["day"]: r["cnt"] for r in merged_rows}
# Build cumulative arrays
dates = []
sources_cum = []
prs_cum = []
merged_cum = []
s_total = source_base
p_total = pr_base
m_total = merged_base
for day in all_dates:
s_total += src_by_day.get(day, 0)
p_total += pr_by_day.get(day, 0)
m_total += mrg_by_day.get(day, 0)
dates.append(day)
sources_cum.append(s_total)
prs_cum.append(p_total)
merged_cum.append(m_total)
return web.json_response({
"days": days,
"dates": dates,
"sources": sources_cum,
"prs": prs_cum,
"merged": merged_cum,
"current": {
"sources": s_total,
"prs": p_total,
"merged": m_total,
},
})
import re
_DATE_PREFIX_RE = re.compile(r"^\d{4}-\d{2}-\d{2}-?")
# ─── GET /api/pr-lifecycle ────────────────────────────────────────────────
async def handle_pr_lifecycle(request):
"""All PRs with eval rounds, reviews, and time-to-merge in one payload.
Returns: summary KPIs + per-PR array for the table.
Joins prs + audit_log (eval rounds) + review_records.
"""
conn = request.app["_get_conn"]()
days = int(request.query.get("days", "30"))
day_clause = "AND p.created_at > datetime('now', ? || ' days')" if days < 9999 else ""
params = (f"-{days}",) if days < 9999 else ()
# Base PR data
pr_rows = conn.execute(
f"""SELECT p.number, p.agent, p.domain, p.tier, p.status,
p.created_at, p.merged_at, p.leo_verdict, p.description,
p.domain_agent, p.domain_model, p.branch
FROM prs p
WHERE 1=1 {day_clause}
ORDER BY p.number DESC""",
params,
).fetchall()
# Eval round counts per PR (from audit_log)
eval_rows = conn.execute(
f"""SELECT CAST(json_extract(detail, '$.pr') AS INTEGER) as pr,
COUNT(*) as rounds
FROM audit_log
WHERE stage = 'evaluate'
AND event IN ('approved', 'changes_requested', 'domain_rejected', 'tier05_rejected')
AND json_extract(detail, '$.pr') IS NOT NULL
GROUP BY pr""",
).fetchall()
eval_map = {r["pr"]: r["rounds"] for r in eval_rows}
# Review outcomes per PR (from review_records)
review_rows = conn.execute(
"""SELECT pr_number, outcome,
GROUP_CONCAT(DISTINCT reviewer) as reviewers,
COUNT(*) as review_count
FROM review_records
GROUP BY pr_number, outcome""",
).fetchall()
review_map = {}
for r in review_rows:
pr = r["pr_number"]
if pr not in review_map:
review_map[pr] = {"outcomes": [], "reviewers": set(), "count": 0}
review_map[pr]["outcomes"].append(r["outcome"])
if r["reviewers"]:
review_map[pr]["reviewers"].update(r["reviewers"].split(","))
review_map[pr]["count"] += r["review_count"]
# Review snippets for closed PRs — from review_text or issues list
snippet_rows = conn.execute(
"""SELECT CAST(json_extract(detail, '$.pr') AS INTEGER) as pr,
COALESCE(
json_extract(detail, '$.review_text'),
json_extract(detail, '$.domain_review_text'),
json_extract(detail, '$.leo_review_text')
) as review_text,
json_extract(detail, '$.issues') as issues,
json_extract(detail, '$.leo') as leo_verdict
FROM audit_log
WHERE stage = 'evaluate'
AND event IN ('domain_rejected', 'changes_requested')
AND json_extract(detail, '$.pr') IS NOT NULL
ORDER BY timestamp DESC""",
).fetchall()
snippet_map = {}
for r in snippet_rows:
pr = r["pr"]
if pr not in snippet_map:
if r["review_text"]:
text = r["review_text"].strip()
lines = [ln.strip() for ln in text.split("\n") if ln.strip() and not ln.strip().startswith("#")]
snippet_map[pr] = lines[0][:200] if lines else text[:200]
elif r["issues"]:
try:
issues = json.loads(r["issues"]) if isinstance(r["issues"], str) else r["issues"]
if isinstance(issues, list) and issues:
snippet_map[pr] = "Issues: " + ", ".join(str(i).replace("_", " ") for i in issues)
except (json.JSONDecodeError, TypeError):
pass
# Build PR list
prs = []
ttm_values = []
round_values = []
merged_count = 0
closed_count = 0
open_count = 0
for r in pr_rows:
pr_num = r["number"]
ttm = None
if r["merged_at"] and r["created_at"]:
try:
created = datetime.fromisoformat(r["created_at"])
merged = datetime.fromisoformat(r["merged_at"])
ttm = (merged - created).total_seconds() / 60
if ttm >= 0:
ttm_values.append(ttm)
else:
ttm = None
except (ValueError, TypeError):
pass
rounds = eval_map.get(pr_num, 0)
if rounds > 0:
round_values.append(rounds)
review_info = review_map.get(pr_num)
status = r["status"] or "unknown"
if status == "merged":
merged_count += 1
elif status == "closed":
closed_count += 1
elif status == "open":
open_count += 1
# Claims count from pipe-separated description titles
desc = r["description"] or ""
claims_count = desc.count("|") + 1 if desc.strip() else 1
# Summary: first claim title from description, fallback to branch name
summary = None
if desc.strip():
first_title = desc.split("|")[0].strip()
summary = first_title[:120] if first_title else None
if not summary:
branch = r["branch"] or ""
# Use prefix as category if present: "extract/...", "reweave/...", etc.
prefix = ""
if "/" in branch:
prefix = branch.split("/", 1)[0]
branch = branch.split("/", 1)[1]
# Strip date prefix like "2026-04-06-" or "2026-02-00-"
branch = _DATE_PREFIX_RE.sub("", branch)
# Strip trailing hash suffix like "-116d" or "-2cb1"
branch = re.sub(r"-[0-9a-f]{4}$", "", branch)
if branch:
summary = branch.replace("-", " ").replace("_", " ").strip()[:120]
elif prefix:
summary = prefix # "reweave", "ingestion", etc.
prs.append({
"number": pr_num,
"agent": r["agent"],
"domain": r["domain"],
"tier": r["tier"],
"status": status,
"claims_count": claims_count,
"eval_rounds": rounds,
"ttm_minutes": round(ttm, 1) if ttm is not None else None,
"created_at": r["created_at"],
"merged_at": r["merged_at"],
"leo_verdict": r["leo_verdict"],
"review_count": review_info["count"] if review_info else 0,
"summary": summary,
"description": desc if desc.strip() else None,
"review_snippet": snippet_map.get(pr_num),
})
# Summary KPIs
ttm_values.sort()
round_values.sort()
def median(vals):
if not vals:
return None
n = len(vals)
if n % 2 == 0:
return (vals[n // 2 - 1] + vals[n // 2]) / 2
return vals[n // 2]
def p90(vals):
if len(vals) < 5:
return None
return vals[int(len(vals) * 0.9)]
return web.json_response({
"days": days,
"total": len(prs),
"merged": merged_count,
"closed": closed_count,
"open": open_count,
"median_ttm": round(median(ttm_values), 1) if median(ttm_values) is not None else None,
"p90_ttm": round(p90(ttm_values), 1) if p90(ttm_values) is not None else None,
"median_rounds": round(median(round_values), 1) if median(round_values) is not None else None,
"max_rounds": max(round_values) if round_values else None,
"prs": prs,
})
# ─── Registration ──────────────────────────────────────────────────────────
def register_dashboard_routes(app: web.Application, get_conn):
"""Register new dashboard API routes."""
app["_get_conn"] = get_conn
app.router.add_get("/api/stage-times", handle_stage_times)
app.router.add_get("/api/herfindahl", handle_herfindahl)
app.router.add_get("/api/agent-state", handle_agent_state)
app.router.add_get("/api/extraction-yield-by-domain", handle_extraction_yield_by_domain)
app.router.add_get("/api/agents-dashboard", handle_agents_dashboard)
app.router.add_get("/api/cascade-coverage", handle_cascade_coverage)
app.router.add_get("/api/review-summary", handle_review_summary)
app.router.add_get("/api/trace/{trace_id}", handle_trace)
app.router.add_get("/api/growth", handle_growth)
app.router.add_get("/api/pr-lifecycle", handle_pr_lifecycle)