"""
QPPG Monitoring Dashboard — FastAPI + plain HTML.

Adds /dashboard and /dashboard/api/* routes to an existing FastAPI app.
No extra dependencies: pure HTML, CSS, and vanilla JS with Chart.js from CDN.

Usage (standalone):
    llm-guard-kit dashboard --domain prod --port 8080

Usage (programmatic):
    from fastapi import FastAPI
    from qppg_service.dashboard import add_dashboard_routes
    from qppg_service.store import ChainStore

    app   = FastAPI()
    store = ChainStore()
    add_dashboard_routes(app, store)
"""

from __future__ import annotations

import time
from typing import TYPE_CHECKING, Optional

from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel

if TYPE_CHECKING:
    from .store import ChainStore


class _DashLoginRequest(BaseModel):
    """Login request body for POST /dashboard/auth/login."""
    password: str


# ── HTML template ─────────────────────────────────────────────────────────────

_HTML = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>llm-guard-kit · Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<style>
/* ── Reset & Tokens ────────────────────────────────────────────────────── */
:root {
  --bg:        #0a0c13;
  --surface:   #111827;
  --surface2:  #1a2235;
  --border:    #1e2d45;
  --border2:   #243349;
  --text:      #f1f5f9;
  --muted:     #64748b;
  --muted2:    #94a3b8;
  --accent:    #6366f1;
  --accent2:   #818cf8;
  --green:     #22c55e;
  --yellow:    #f59e0b;
  --red:       #ef4444;
  --red2:      #fca5a5;
  --purple:    #a78bfa;
  --sidebar-w: 220px;
  --header-h:  56px;
  --radius:    10px;
  --shadow:    0 1px 3px rgba(0,0,0,0.4);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: 'Inter', system-ui, sans-serif;
  font-size: 13px;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* ── Top Header ───────────────────────────────────────────────────────── */
#topbar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  height: var(--header-h);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: 16px;
  padding: 0 20px;
}
#topbar .logo {
  display: flex; align-items: center; gap: 8px;
  font-weight: 700; font-size: 15px; color: var(--text);
  text-decoration: none; white-space: nowrap;
}
#topbar .logo svg { flex-shrink: 0; }
#topbar .divider { width: 1px; height: 24px; background: var(--border); }
#topbar .domain-select-wrap { display: flex; align-items: center; gap: 8px; }
#topbar .domain-select-wrap label { color: var(--muted); font-size: 12px; }
#domain-select {
  background: var(--surface2); border: 1px solid var(--border2);
  color: var(--text); font-family: inherit; font-size: 12px;
  padding: 4px 10px; border-radius: 6px; cursor: pointer; outline: none;
}
#domain-select:focus { border-color: var(--accent); }
#topbar .spacer { flex: 1; }
#topbar .status-wrap { display: flex; align-items: center; gap: 8px; }
.live-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--green);
  box-shadow: 0 0 0 0 rgba(34,197,94,0.6);
  animation: pulse 2s infinite;
}
@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 rgba(34,197,94,0.5); }
  70%  { box-shadow: 0 0 0 6px rgba(34,197,94,0); }
  100% { box-shadow: 0 0 0 0 rgba(34,197,94,0); }
}
#refresh-ts { color: var(--muted); font-size: 12px; }
#countdown { color: var(--accent2); font-size: 12px; font-variant-numeric: tabular-nums; }
.btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; border-radius: 6px; font-size: 12px;
  font-family: inherit; cursor: pointer; border: none; font-weight: 500;
  transition: opacity .15s;
}
.btn:hover { opacity: .85; }
.btn-ghost {
  background: transparent; border: 1px solid var(--border2); color: var(--muted2);
}
.btn-ghost:hover { border-color: var(--accent); color: var(--accent2); }
.btn-primary { background: var(--accent); color: #fff; }

/* ── Left Sidebar ─────────────────────────────────────────────────────── */
#sidebar {
  position: fixed; top: var(--header-h); left: 0; bottom: 0; z-index: 90;
  width: var(--sidebar-w);
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: 16px 0;
  overflow-y: auto;
}
.nav-section {
  padding: 6px 16px 2px;
  color: var(--muted); font-size: 10px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .08em;
}
.nav-item {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 16px; color: var(--muted2); font-size: 13px;
  cursor: pointer; border-radius: 0; transition: background .1s, color .1s;
  border-left: 3px solid transparent;
}
.nav-item:hover { background: var(--surface2); color: var(--text); }
.nav-item.active {
  background: rgba(99,102,241,0.12); color: var(--accent2);
  border-left-color: var(--accent);
}
.nav-item svg { flex-shrink: 0; opacity: .7; }
.nav-item.active svg { opacity: 1; }

/* ── Main Content ─────────────────────────────────────────────────────── */
#main {
  margin-top: var(--header-h);
  margin-left: var(--sidebar-w);
  padding: 24px;
  flex: 1;
}

/* ── Deployment Status Banner ─────────────────────────────────────────── */
#deploy-banner {
  display: flex; align-items: center; gap: 12px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px 18px;
  margin-bottom: 20px;
}
.deploy-badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;
}
.deploy-COLD_START { background: rgba(100,116,139,.2); color: var(--muted2); border: 1px solid var(--border2); }
.deploy-WARMING    { background: rgba(245,158,11,.15); color: var(--yellow); border: 1px solid rgba(245,158,11,.3); }
.deploy-DEPLOYED   { background: rgba(34,197,94,.15); color: var(--green); border: 1px solid rgba(34,197,94,.3); }
#deploy-banner .meta { color: var(--muted); font-size: 12px; }
#deploy-banner .meta strong { color: var(--muted2); }
#deploy-banner .spacer { flex: 1; }
#auroc-chip {
  background: var(--surface2); border: 1px solid var(--border2);
  border-radius: 6px; padding: 6px 12px; font-size: 12px; color: var(--muted2);
}
#auroc-chip span { color: var(--accent2); font-weight: 600; }

/* ── Drift Alert ──────────────────────────────────────────────────────── */
#drift-banner {
  display: none; align-items: flex-start; gap: 12px;
  background: rgba(245,158,11,.08); border: 1px solid rgba(245,158,11,.35);
  border-radius: var(--radius); padding: 12px 16px; margin-bottom: 20px;
}
#drift-banner.visible { display: flex; }
#drift-banner .drift-icon { font-size: 16px; line-height: 1; flex-shrink: 0; }
#drift-banner .drift-body { flex: 1; }
#drift-banner .drift-title { color: var(--yellow); font-weight: 600; font-size: 13px; margin-bottom: 2px; }
#drift-banner .drift-detail { color: var(--muted2); font-size: 12px; }

/* ── KPI Cards ────────────────────────────────────────────────────────── */
.kpi-grid {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px;
  margin-bottom: 22px;
}
.kpi-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px 18px;
  display: flex; flex-direction: column; gap: 6px;
  position: relative; overflow: hidden;
  transition: border-color .15s;
}
.kpi-card:hover { border-color: var(--border2); }
.kpi-card .kpi-label {
  color: var(--muted); font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em;
}
.kpi-card .kpi-val {
  font-size: 26px; font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.kpi-card .kpi-sub {
  color: var(--muted); font-size: 11px; margin-top: 2px;
}
.kpi-card .kpi-accent {
  position: absolute; top: 0; left: 0; width: 3px; height: 100%;
  border-radius: var(--radius) 0 0 var(--radius);
}
.kpi-blue .kpi-accent { background: var(--accent); }
.kpi-red  .kpi-accent { background: var(--red); }
.kpi-yellow .kpi-accent { background: var(--yellow); }
.kpi-purple .kpi-accent { background: var(--purple); }
.kpi-green .kpi-accent { background: var(--green); }

.val-green  { color: var(--green); }
.val-yellow { color: var(--yellow); }
.val-red    { color: var(--red); }
.val-muted  { color: var(--muted2); }

/* ── Charts Row ───────────────────────────────────────────────────────── */
.charts-row {
  display: grid; grid-template-columns: 3fr 2fr; gap: 16px;
  margin-bottom: 22px;
}
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 20px;
}
.card-header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 16px;
}
.card-title {
  font-size: 12px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .06em;
}
.card-actions { display: flex; gap: 8px; }
.range-btn {
  background: transparent; border: 1px solid transparent;
  color: var(--muted); font-size: 11px; font-family: inherit;
  padding: 3px 8px; border-radius: 5px; cursor: pointer;
  transition: all .1s;
}
.range-btn:hover { color: var(--text); }
.range-btn.active {
  background: var(--surface2); border-color: var(--border2); color: var(--accent2);
}

/* chart containers */
.chart-wrap { position: relative; }
#risk-chart  { height: 200px !important; }
#fail-chart  { height: 200px !important; }

/* ── Chains Table ─────────────────────────────────────────────────────── */
.table-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden;
}
.table-toolbar {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 18px; border-bottom: 1px solid var(--border);
}
.table-toolbar .ttitle {
  font-size: 12px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .06em;
}
.filter-tabs { display: flex; gap: 2px; margin-left: auto; }
.tab-btn {
  background: transparent; border: 1px solid transparent;
  color: var(--muted); font-size: 12px; font-family: inherit;
  padding: 4px 10px; border-radius: 6px; cursor: pointer;
  transition: all .1s;
}
.tab-btn.active {
  background: var(--surface2); border-color: var(--border2); color: var(--accent2);
}
.tab-btn:hover:not(.active) { color: var(--text); }
.search-input {
  background: var(--surface2); border: 1px solid var(--border2);
  color: var(--text); font-family: inherit; font-size: 12px;
  padding: 5px 10px; border-radius: 6px; outline: none; width: 200px;
}
.search-input:focus { border-color: var(--accent); }
.search-input::placeholder { color: var(--muted); }

table { width: 100%; border-collapse: collapse; }
thead th {
  text-align: left; color: var(--muted); font-size: 11px;
  font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
  border-bottom: 1px solid var(--border); padding: 9px 16px;
  white-space: nowrap; cursor: default;
  user-select: none;
}
thead th.sortable { cursor: pointer; }
thead th.sortable:hover { color: var(--text); }
tbody td {
  padding: 11px 16px; border-bottom: 1px solid var(--border);
  vertical-align: middle; font-size: 12px;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover td { background: rgba(255,255,255,.02); }
.q-cell {
  max-width: 280px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; color: var(--text);
}
.risk-badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 52px; padding: 3px 8px; border-radius: 20px;
  font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums;
  letter-spacing: .01em;
}
.risk-high   { background: rgba(239,68,68,.15);  color: var(--red2);   border: 1px solid rgba(239,68,68,.3); }
.risk-medium { background: rgba(245,158,11,.12); color: var(--yellow); border: 1px solid rgba(245,158,11,.3); }
.risk-low    { background: rgba(34,197,94,.12);  color: var(--green);  border: 1px solid rgba(34,197,94,.3); }
.fm-tag {
  display: inline-block; padding: 2px 7px; border-radius: 4px;
  background: var(--surface2); border: 1px solid var(--border2);
  color: var(--muted2); font-size: 10px; font-weight: 500;
}
.ts-cell { color: var(--muted); white-space: nowrap; }
.model-cell { color: var(--muted2); font-size: 11px; max-width: 100px;
              overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty-state {
  text-align: center; color: var(--muted); padding: 48px;
  font-size: 13px;
}
.empty-state .empty-icon { font-size: 32px; margin-bottom: 10px; }
.table-footer {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 18px; border-top: 1px solid var(--border);
  color: var(--muted); font-size: 12px;
}

/* ── Toast ────────────────────────────────────────────────────────────── */
#toast {
  position: fixed; bottom: 24px; right: 24px; z-index: 999;
  background: var(--surface2); border: 1px solid var(--border2);
  border-radius: 8px; padding: 10px 16px; font-size: 13px;
  color: var(--text); box-shadow: 0 4px 16px rgba(0,0,0,.5);
  opacity: 0; transform: translateY(8px);
  transition: opacity .2s, transform .2s; pointer-events: none;
}
#toast.show { opacity: 1; transform: translateY(0); }

/* ── Responsive ───────────────────────────────────────────────────────── */
@media (max-width: 1100px) {
  .kpi-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 900px) {
  .charts-row { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
  #sidebar { display: none; }
  #main { margin-left: 0; padding: 16px; }
  .kpi-grid { grid-template-columns: repeat(2, 1fr); }
}

/* ── Admin Panel ──────────────────────────────────────────────────── */
.admin-tabs {
  display: flex; gap: 4px; margin-bottom: 20px;
  background: var(--surface); border-radius: var(--radius);
  padding: 4px; border: 1px solid var(--border);
}
.atab {
  flex: 1; padding: 8px 12px; border: none; border-radius: 7px;
  background: transparent; color: var(--muted2); font-family: inherit;
  font-size: 12px; font-weight: 500; cursor: pointer; transition: all .15s;
}
.atab:hover { background: var(--surface2); color: var(--text); }
.atab.active { background: var(--accent); color: #fff; font-weight: 600; }
.admin-panel { display: none; }
.admin-panel.active { display: block; }
.admin-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 20px; margin-bottom: 16px;
}
.admin-card-title {
  font-size: 13px; font-weight: 600; color: var(--text);
  margin-bottom: 16px; display: flex; align-items: center; gap: 8px;
}
.admin-card-title .sub { font-weight: 400; color: var(--muted); font-size: 12px; }
.form-group { margin-bottom: 14px; }
.form-label {
  display: block; font-size: 11px; font-weight: 600; color: var(--muted2);
  text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px;
}
.form-input {
  width: 100%; padding: 8px 12px; background: var(--surface2);
  border: 1px solid var(--border2); border-radius: 7px; color: var(--text);
  font-family: inherit; font-size: 13px; outline: none; transition: border .15s;
}
.form-input:focus { border-color: var(--accent); }
.form-textarea {
  width: 100%; padding: 8px 12px; background: var(--surface2);
  border: 1px solid var(--border2); border-radius: 7px; color: var(--text);
  font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 12px;
  outline: none; resize: vertical; min-height: 120px; transition: border .15s;
}
.form-textarea:focus { border-color: var(--accent); }
.form-row { display: flex; gap: 10px; }
.form-row .form-group { flex: 1; }
.form-note { font-size: 11px; color: var(--muted); margin-top: 4px; line-height: 1.5; }
.result-box {
  margin-top: 14px; padding: 12px 16px; border-radius: 8px;
  font-size: 12px; display: none;
}
.result-box.ok  { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.3); color: var(--green); }
.result-box.err { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.3); color: var(--red2); }
.key-reveal {
  margin-top: 14px; padding: 14px 16px;
  background: rgba(34,197,94,.08); border: 1px solid rgba(34,197,94,.25);
  border-radius: 8px; display: none;
}
.key-reveal .key-label { font-size: 11px; font-weight: 600; color: var(--green); text-transform: uppercase; letter-spacing: .6px; margin-bottom: 6px; }
.key-reveal .key-value {
  font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--text);
  background: var(--surface2); padding: 8px 12px; border-radius: 6px;
  word-break: break-all; cursor: pointer; border: 1px solid var(--border);
}
.key-reveal .key-warn { font-size: 11px; color: var(--yellow); margin-top: 8px; }
.key-table { width: 100%; border-collapse: collapse; }
.key-table th {
  text-align: left; font-size: 11px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .6px; padding: 6px 10px;
  border-bottom: 1px solid var(--border);
}
.key-table td { padding: 9px 10px; font-size: 12px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.key-table tr:last-child td { border-bottom: none; }
.status-pill { display: inline-block; padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; }
.status-pill.active  { background: rgba(34,197,94,.15); color: var(--green); }
.status-pill.revoked { background: rgba(239,68,68,.12); color: var(--red); }
.danger-btn {
  background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.3);
  color: var(--red); border-radius: 6px; padding: 5px 12px;
  font-size: 12px; cursor: pointer; font-family: inherit; transition: all .15s;
}
.danger-btn:hover { background: rgba(239,68,68,.2); }
.score-result {
  display: none; margin-top: 16px; padding: 16px;
  background: var(--surface2); border: 1px solid var(--border2); border-radius: var(--radius);
}
.score-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 12px; }
.score-cell { text-align: center; }
.score-cell .sc-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 4px; }
.score-cell .sc-val { font-size: 24px; font-weight: 700; }
.domains-table { width: 100%; border-collapse: collapse; }
.domains-table th {
  text-align: left; font-size: 11px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .6px; padding: 6px 10px;
  border-bottom: 1px solid var(--border);
}
.domains-table td { padding: 9px 10px; font-size: 12px; border-bottom: 1px solid var(--border); vertical-align: middle; }
.domains-table tr:last-child td { border-bottom: none; }
.seed-tabs { display: flex; gap: 0; margin-bottom: 16px; border-radius: 7px; overflow: hidden; border: 1px solid var(--border); }
.stab { flex: 1; padding: 8px; border: none; background: var(--surface2); color: var(--muted); font-family: inherit; font-size: 12px; cursor: pointer; transition: all .15s; }
.stab.active { background: var(--accent); color: #fff; font-weight: 600; }
</style>
</head>
<body>

<!-- ── Top Header ──────────────────────────────────────────────────── -->
<div id="topbar">
  <a class="logo" href="#">
    <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
      <path d="M10 2L3 6v4c0 4.418 2.94 8.15 7 9 4.06-.85 7-4.582 7-9V6L10 2z"
            fill="var(--accent)" opacity=".9"/>
      <path d="M7 10l2 2 4-4" stroke="#fff" stroke-width="1.5"
            stroke-linecap="round" stroke-linejoin="round"/>
    </svg>
    llm-guard-kit
  </a>
  <div class="divider"></div>
  <div class="domain-select-wrap">
    <label for="domain-select">Domain</label>
    <select id="domain-select"><option>default</option></select>
  </div>
  <div class="spacer"></div>
  <div class="status-wrap">
    <div class="live-dot"></div>
    <span id="refresh-ts">—</span>
    <span id="countdown"></span>
  </div>
  <button class="btn btn-ghost" onclick="refresh()">
    <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
      <path d="M13.5 2.5a7 7 0 1 0 1.5 4.5" stroke="currentColor"
            stroke-width="1.5" stroke-linecap="round"/>
      <path d="M15 2.5v4h-4" stroke="currentColor" stroke-width="1.5"
            stroke-linecap="round" stroke-linejoin="round"/>
    </svg>
    Refresh
  </button>
  <button class="btn btn-primary" onclick="exportCSV()">
    <svg width="12" height="12" viewBox="0 0 16 16" fill="none">
      <path d="M8 2v9m0 0l-3-3m3 3l3-3M3 14h10"
            stroke="currentColor" stroke-width="1.5"
            stroke-linecap="round" stroke-linejoin="round"/>
    </svg>
    Export CSV
  </button>
</div>

<!-- ── Sidebar ─────────────────────────────────────────────────────── -->
<div id="sidebar">
  <div class="nav-section">Overview</div>
  <div class="nav-item active" onclick="showSection('overview', this)">
    <svg width="15" height="15" viewBox="0 0 16 16" fill="none">
      <rect x="1" y="1" width="6" height="6" rx="1.5" fill="currentColor" opacity=".7"/>
      <rect x="9" y="1" width="6" height="6" rx="1.5" fill="currentColor" opacity=".7"/>
      <rect x="1" y="9" width="6" height="6" rx="1.5" fill="currentColor" opacity=".7"/>
      <rect x="9" y="9" width="6" height="6" rx="1.5" fill="currentColor" opacity=".7"/>
    </svg>
    Dashboard
  </div>
  <div class="nav-section" style="margin-top:12px">Analysis</div>
  <div class="nav-item" onclick="showSection('chains', this)">
    <svg width="15" height="15" viewBox="0 0 16 16" fill="none">
      <rect x="1" y="3" width="14" height="2" rx="1" fill="currentColor" opacity=".7"/>
      <rect x="1" y="7" width="10" height="2" rx="1" fill="currentColor" opacity=".7"/>
      <rect x="1" y="11" width="12" height="2" rx="1" fill="currentColor" opacity=".7"/>
    </svg>
    Chain Log
  </div>
  <div class="nav-item" onclick="showSection('failures', this)">
    <svg width="15" height="15" viewBox="0 0 16 16" fill="none">
      <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.4" opacity=".7"/>
      <path d="M8 5v3.5l2 2" stroke="currentColor" stroke-width="1.4"
            stroke-linecap="round" opacity=".7"/>
    </svg>
    Failure Modes
  </div>
  <div class="nav-section" style="margin-top:12px">Manage</div>
  <div class="nav-item" onclick="showSection('admin', this)">
    <svg width="15" height="15" viewBox="0 0 16 16" fill="none">
      <circle cx="8" cy="8" r="3" stroke="currentColor" stroke-width="1.4" opacity=".7"/>
      <path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.5 3.5l1.4 1.4M11.1 11.1l1.4 1.4M3.5 12.5l1.4-1.4M11.1 4.9l1.4-1.4"
            stroke="currentColor" stroke-width="1.3" stroke-linecap="round" opacity=".7"/>
    </svg>
    Admin
  </div>
</div>

<!-- ── Main Content ────────────────────────────────────────────────── -->
<div id="main">

  <!-- Deployment status banner -->
  <div id="deploy-banner">
    <span id="deploy-badge" class="deploy-badge deploy-COLD_START">● COLD START</span>
    <span class="meta" id="deploy-meta">Collecting calibration chains…</span>
    <div class="spacer"></div>
    <div id="auroc-chip">Est. AUROC&nbsp;<span id="auroc-val">—</span></div>
  </div>

  <!-- Drift alert -->
  <div id="drift-banner">
    <div class="drift-icon">⚠</div>
    <div class="drift-body">
      <div class="drift-title">Risk Drift Detected</div>
      <div class="drift-detail" id="drift-msg">—</div>
    </div>
  </div>

  <!-- ══ SECTION: OVERVIEW ══════════════════════════════════════════ -->
  <div id="section-overview">

    <!-- KPI Cards -->
    <div class="kpi-grid">
      <div class="kpi-card kpi-blue">
        <div class="kpi-accent"></div>
        <div class="kpi-label">Total Queries</div>
        <div class="kpi-val" id="k-total">—</div>
        <div class="kpi-sub" id="k-total-sub">all time</div>
      </div>
      <div class="kpi-card kpi-red">
        <div class="kpi-accent"></div>
        <div class="kpi-label">High-Risk Alerts</div>
        <div class="kpi-val" id="k-alerts">—</div>
        <div class="kpi-sub" id="k-alert-rate">—% alert rate</div>
      </div>
      <div class="kpi-card kpi-yellow">
        <div class="kpi-accent"></div>
        <div class="kpi-label">Avg Risk Score</div>
        <div class="kpi-val" id="k-avg">—</div>
        <div class="kpi-sub" id="k-p95">P95: —</div>
      </div>
      <div class="kpi-card kpi-purple">
        <div class="kpi-accent"></div>
        <div class="kpi-label">Calibration Pool</div>
        <div class="kpi-val" id="k-cal">—</div>
        <div class="kpi-sub">chains in pool</div>
      </div>
      <div class="kpi-card kpi-green">
        <div class="kpi-accent"></div>
        <div class="kpi-label">Search Steps / Query</div>
        <div class="kpi-val" id="k-steps">—</div>
        <div class="kpi-sub">avg across all chains</div>
      </div>
    </div>

    <!-- Charts row -->
    <div class="charts-row">
      <!-- Risk timeline -->
      <div class="card">
        <div class="card-header">
          <span class="card-title">Risk Score Timeline</span>
          <div class="card-actions">
            <button class="range-btn active" data-h="24"  onclick="setRange(this)">24h</button>
            <button class="range-btn"        data-h="168" onclick="setRange(this)">7d</button>
            <button class="range-btn"        data-h="720" onclick="setRange(this)">30d</button>
          </div>
        </div>
        <div class="chart-wrap"><canvas id="risk-chart"></canvas></div>
      </div>

      <!-- Failure mode breakdown -->
      <div class="card">
        <div class="card-header">
          <span class="card-title">Failure Mode Breakdown</span>
        </div>
        <div class="chart-wrap"><canvas id="fail-chart"></canvas></div>
        <div id="no-failures" style="display:none;text-align:center;
             color:var(--muted);font-size:12px;padding:40px 0">
          No failure-mode data yet
        </div>
      </div>
    </div>

    <!-- Mini chain table (top alerts) -->
    <div class="table-card">
      <div class="table-toolbar">
        <span class="ttitle">Top Alerts</span>
        <span style="color:var(--muted);font-size:11px" id="top-count"></span>
        <div class="spacer"></div>
        <button class="btn btn-ghost" onclick="showSection('chains', document.querySelector('.nav-item:nth-child(5)'))">
          View all →
        </button>
      </div>
      <table>
        <thead>
          <tr>
            <th style="width:38%">Question</th>
            <th style="width:9%">Risk</th>
            <th style="width:16%">Failure Mode</th>
            <th style="width:8%">Steps</th>
            <th style="width:11%">Model</th>
            <th>Time</th>
          </tr>
        </thead>
        <tbody id="top-tbody">
          <tr><td colspan="6" class="empty-state">Loading…</td></tr>
        </tbody>
      </table>
    </div>
  </div><!-- /section-overview -->

  <!-- ══ SECTION: CHAIN LOG ════════════════════════════════════════ -->
  <div id="section-chains" style="display:none">
    <div class="table-card">
      <div class="table-toolbar">
        <span class="ttitle">Chain Log</span>
        <div class="filter-tabs">
          <button class="tab-btn active" data-filter="all"    onclick="setFilter(this)">All</button>
          <button class="tab-btn"        data-filter="alerts" onclick="setFilter(this)">Alerts only</button>
        </div>
        <input class="search-input" id="chain-search"
               placeholder="Search questions…" oninput="filterTable()">
        <div class="spacer"></div>
        <button class="btn btn-ghost" onclick="exportCSV()">Export CSV</button>
      </div>
      <table>
        <thead>
          <tr>
            <th style="width:2%">#</th>
            <th class="sortable" data-col="question" style="width:34%">Question</th>
            <th class="sortable" data-col="risk_score" style="width:9%">Risk ↓</th>
            <th style="width:16%">Failure Mode</th>
            <th class="sortable" data-col="n_steps" style="width:8%">Steps</th>
            <th style="width:10%">Model</th>
            <th class="sortable" data-col="timestamp">Time</th>
          </tr>
        </thead>
        <tbody id="log-tbody">
          <tr><td colspan="7" class="empty-state">Loading…</td></tr>
        </tbody>
      </table>
      <div class="table-footer">
        <span id="log-count">—</span>
        <div style="display:flex;gap:8px">
          <button class="btn btn-ghost" id="prev-btn" onclick="prevPage()">← Prev</button>
          <span id="page-info" style="color:var(--muted);align-self:center;font-size:12px"></span>
          <button class="btn btn-ghost" id="next-btn" onclick="nextPage()">Next →</button>
        </div>
      </div>
    </div>
  </div><!-- /section-chains -->

  <!-- ══ SECTION: FAILURE MODES ════════════════════════════════════ -->
  <div id="section-failures" style="display:none">
    <div class="card" style="margin-bottom:16px">
      <div class="card-header">
        <span class="card-title">Failure Mode Distribution (Alerted Chains)</span>
      </div>
      <canvas id="fail-chart-big" style="height:280px !important"></canvas>
      <div id="no-failures-big" style="display:none;text-align:center;
           color:var(--muted);font-size:13px;padding:48px">
        <div class="empty-icon">✅</div>
        No failure modes recorded yet — all chains are passing.
      </div>
    </div>
    <div class="table-card">
      <div class="table-toolbar">
        <span class="ttitle">Failure Mode Details</span>
      </div>
      <table>
        <thead>
          <tr>
            <th>Failure Mode</th>
            <th>Count</th>
            <th style="width:50%">Description</th>
          </tr>
        </thead>
        <tbody id="fail-tbody"></tbody>
      </table>
    </div>
  </div><!-- /section-failures -->

  <!-- ══ SECTION: ADMIN ═════════════════════════════════════════════ -->
  <div id="section-admin" style="display:none">

    <!-- Tab bar -->
    <div class="admin-tabs">
      <button class="atab active" onclick="showAdminTab('keys')">🔑 API Keys</button>
      <button class="atab" onclick="showAdminTab('calibrate')">⚡ Calibrate</button>
      <button class="atab" onclick="showAdminTab('playground')">🧪 Playground</button>
      <button class="atab" onclick="showAdminTab('domains')">🗄 Domains</button>
      <button class="atab" onclick="showAdminTab('review')">👁 Review Queue</button>
    </div>

    <!-- ─── API Keys ─── -->
    <div id="atab-keys" class="admin-panel active">
      <div class="admin-card">
        <div class="admin-card-title">Create API Key
          <span class="sub">— issue authenticated access for customers or team members</span>
        </div>
        <div class="form-row">
          <div class="form-group">
            <label class="form-label">Customer / Team ID</label>
            <input id="key-customer" class="form-input" placeholder="e.g. acme-corp" type="text">
          </div>
          <div class="form-group">
            <label class="form-label">Domain Prefix <span style="color:var(--muted)">(optional)</span></label>
            <input id="key-domain-pfx" class="form-input" placeholder="e.g. prod" type="text">
          </div>
        </div>
        <button class="btn btn-primary" onclick="createApiKey()">Generate API Key</button>
        <div class="key-reveal" id="key-result">
          <div class="key-label">✓ API Key Created — copy it now</div>
          <div class="key-value" id="key-value" onclick="copyKey()" title="Click to copy">—</div>
          <div class="key-warn">⚠ This key will NOT be shown again. Store it somewhere safe immediately.</div>
        </div>
      </div>
      <div class="admin-card">
        <div class="admin-card-title">Active Keys
          <button class="btn btn-ghost" style="margin-left:auto;padding:4px 10px;font-size:12px" onclick="loadAdminKeys()">↻ Refresh</button>
        </div>
        <table class="key-table">
          <thead>
            <tr>
              <th>Customer</th><th>Domain Prefix</th><th>Created</th>
              <th>Requests</th><th>Status</th><th>Action</th>
            </tr>
          </thead>
          <tbody id="keys-tbody">
            <tr><td colspan="6" style="text-align:center;color:var(--muted);padding:24px">Loading keys…</td></tr>
          </tbody>
        </table>
      </div>
    </div>

    <!-- ─── Calibrate ─── -->
    <div id="atab-calibrate" class="admin-panel">
      <div class="seed-tabs">
        <button class="stab active" onclick="showSeedTab('seed')">Seed Chains</button>
        <button class="stab" onclick="showSeedTab('cross')">Cross-Domain Warm-up</button>
      </div>

      <div id="stab-seed">
        <div class="admin-card">
          <div class="admin-card-title">Seed Calibration Chains
            <span class="sub">— paste labeled chains to bootstrap a domain's calibration pool</span>
          </div>
          <div class="form-group">
            <label class="form-label">Target Domain</label>
            <input id="seed-domain" class="form-input" placeholder="e.g. prod" value="default">
          </div>
          <div class="form-group">
            <label class="form-label">Chains (JSON array)</label>
            <textarea id="seed-json" class="form-textarea" rows="9"
              placeholder='[&#10;  {&#10;    "question": "What is the capital of France?",&#10;    "steps": [&#10;      {"thought": "I need to search", "action_type": "Search", "action_arg": "capital France", "observation": "Paris is the capital"},&#10;      {"thought": "", "action_type": "Finish", "action_arg": "Paris", "observation": ""}&#10;    ],&#10;    "final_answer": "Paris",&#10;    "finished": true&#10;  }&#10;]'></textarea>
            <div class="form-note">Paste a JSON array of chain objects. Each needs: question, steps[], final_answer, finished.</div>
          </div>
          <button class="btn btn-primary" onclick="seedChains()">Seed Chains</button>
          <div class="result-box" id="seed-result"></div>
        </div>
      </div>

      <div id="stab-cross" style="display:none">
        <div class="admin-card">
          <div class="admin-card-title">Cross-Domain Warm-up
            <span class="sub">— copy calibration chains from an existing domain to bootstrap a new one</span>
          </div>
          <div class="form-row">
            <div class="form-group">
              <label class="form-label">Source Domain</label>
              <select id="cross-source" class="form-input">
                <option value="">— select domain —</option>
              </select>
            </div>
            <div class="form-group">
              <label class="form-label">Target Domain</label>
              <input id="cross-target" class="form-input" placeholder="e.g. new-prod">
            </div>
          </div>
          <div class="form-group">
            <label class="form-label">Chains to Copy: <span id="cross-n-val" style="color:var(--accent2)">50</span></label>
            <input id="cross-n" class="form-input" type="range" min="10" max="200" value="50"
              oninput="document.getElementById('cross-n-val').textContent=this.value"
              style="padding:6px 0;background:transparent;border:none;cursor:pointer">
            <div class="form-note">Copies the N most recent chains from the source domain. The scorer re-calibrates automatically from the new pool.</div>
          </div>
          <button class="btn btn-primary" onclick="seedCross()">Run Warm-up</button>
          <div class="result-box" id="cross-result"></div>
        </div>
      </div>
    </div>

    <!-- ─── Playground ─── -->
    <div id="atab-playground" class="admin-panel">
      <div class="admin-card">
        <div class="admin-card-title">Score Playground
          <span class="sub">— test the scorer against any question + step sequence in real time</span>
        </div>
        <div class="form-row">
          <div class="form-group" style="flex:3">
            <label class="form-label">Question</label>
            <input id="pg-question" class="form-input" placeholder="e.g. What was the population of Tokyo in 2020?">
          </div>
          <div class="form-group" style="flex:1">
            <label class="form-label">Domain (calibration source)</label>
            <select id="pg-domain" class="form-input">
              <option value="default">default</option>
            </select>
          </div>
        </div>
        <div class="form-group">
          <label class="form-label">Steps (JSON array)</label>
          <textarea id="pg-steps" class="form-textarea" rows="9"
            placeholder='[&#10;  {"thought": "I need to find the population", "action_type": "Search", "action_arg": "Tokyo population 2020", "observation": "Tokyo population was 13.96 million in 2020"},&#10;  {"thought": "I found the answer", "action_type": "Finish", "action_arg": "13.96 million", "observation": ""}&#10;]'></textarea>
        </div>
        <button class="btn btn-primary" onclick="scorePlayground()">Score Chain</button>
        <div class="score-result" id="pg-result">
          <div class="score-grid">
            <div class="score-cell">
              <div class="sc-label">Risk Score</div>
              <div class="sc-val" id="pg-risk">—</div>
            </div>
            <div class="score-cell">
              <div class="sc-label">Behavioral</div>
              <div class="sc-val" id="pg-beh">—</div>
            </div>
            <div class="score-cell">
              <div class="sc-label">GMM Density</div>
              <div class="sc-val" id="pg-gmm">—</div>
            </div>
            <div class="score-cell">
              <div class="sc-label">Cal Pool</div>
              <div class="sc-val" id="pg-cal">—</div>
            </div>
          </div>
          <div id="pg-verdict" style="text-align:center;font-size:13px;font-weight:600;padding:6px 0"></div>
        </div>
      </div>
    </div>

    <!-- ─── Domains ─── -->
    <div id="atab-domains" class="admin-panel">
      <div class="admin-card">
        <div class="admin-card-title">Domain Management
          <button class="btn btn-ghost" style="margin-left:auto;padding:4px 10px;font-size:12px" onclick="loadAdminDomains()">↻ Refresh</button>
        </div>
        <table class="domains-table">
          <thead>
            <tr>
              <th>Domain</th><th>Chains</th><th>Avg Risk</th>
              <th>Alerts</th><th>Last Active</th><th>Action</th>
            </tr>
          </thead>
          <tbody id="domains-tbody">
            <tr><td colspan="6" style="text-align:center;color:var(--muted);padding:24px">Loading domains…</td></tr>
          </tbody>
        </table>
      </div>
    </div>

    <!-- ─── Review Queue ─── -->
    <div id="atab-review" class="admin-panel">
      <div class="admin-card">
        <div class="admin-card-title">Active Learning Review Queue
          <button class="btn btn-ghost" style="margin-left:auto;padding:4px 10px;font-size:12px" onclick="loadReviewQueue()">↻ Refresh</button>
        </div>
        <p style="color:var(--muted);margin-bottom:16px;font-size:12px">
          Label the most recent alerted chains to build a supervised training set.
          <b>✓ Correct</b> = agent answered correctly but was flagged (false positive).
          <b>✗ Wrong</b> = alert was justified — agent failed as suspected (true positive).
        </p>
        <div style="display:flex;gap:12px;margin-bottom:12px;align-items:center">
          <label style="color:var(--muted2);font-size:12px">Domain:</label>
          <select id="rq-domain" class="form-input" style="width:180px" onchange="loadReviewQueue()"></select>
          <label style="color:var(--muted2);font-size:12px">Show:</label>
          <select id="rq-n" class="form-input" style="width:80px" onchange="loadReviewQueue()">
            <option value="10" selected>10</option>
            <option value="25">25</option>
            <option value="50">50</option>
          </select>
        </div>
        <table class="domains-table" style="width:100%">
          <thead><tr>
            <th style="min-width:200px">Question</th>
            <th>Risk</th><th>Failure Mode</th><th>Time</th><th>Label</th>
          </tr></thead>
          <tbody id="rq-tbody">
            <tr><td colspan="5" style="text-align:center;color:var(--muted);padding:24px">Loading…</td></tr>
          </tbody>
        </table>
        <p id="rq-empty" style="display:none;text-align:center;color:var(--green);padding:24px;font-size:13px">
          ✓ No pending chains — review queue is empty.
        </p>
      </div>
    </div>

  </div><!-- /section-admin -->

</div><!-- /main -->

<div id="toast"></div>

<script>
// ── State ───────────────────────────────────────────────────────────────
let DOMAIN    = new URLSearchParams(location.search).get('domain') || 'default';
let allChains = [];
let failData  = {};
let riskChart = null, failChart = null, failChartBig = null;
let currentHours = 24;
let filterMode  = 'all';
let currentPage = 1;
const PAGE_SIZE = 25;
let countdownSec = 30;
let countdownTimer = null;

const FAILURE_DESCS = {
  'RETRIEVAL_FAILURE':   'Agent could not find relevant information for the question.',
  'EXCESSIVE_SEARCH':    'Agent performed too many search steps without converging.',
  'CONFLICTING_EVIDENCE':'Retrieved documents contained contradictory information.',
  'HALLUCINATION':       'Agent generated information not grounded in retrieved context.',
  'FORMAT_ERROR':        'Agent output was malformed or did not follow expected format.',
  'INCOMPLETE_ANSWER':   'Agent stopped before fully answering the question.',
  'LOW_CONFIDENCE':      'Agent expressed uncertainty or hedged excessively.',
};

// ── Utils ────────────────────────────────────────────────────────────────
function fmtTime(ts) {
  if (!ts) return '—';
  const d = new Date(ts * 1000);
  const now = new Date();
  const diffH = (now - d) / 3600000;
  if (diffH < 1)   return Math.round(diffH * 60) + 'm ago';
  if (diffH < 24)  return Math.round(diffH) + 'h ago';
  return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
}

function riskBadge(r) {
  const cls = r >= 0.65 ? 'risk-high' : r >= 0.40 ? 'risk-medium' : 'risk-low';
  return `<span class="risk-badge ${cls}">${r.toFixed(3)}</span>`;
}

function colorForRisk(r) {
  return r >= 0.65 ? 'var(--red)' : r >= 0.40 ? 'var(--yellow)' : 'var(--green)';
}

function showToast(msg, dur=2500) {
  const t = document.getElementById('toast');
  t.textContent = msg;
  t.classList.add('show');
  setTimeout(() => t.classList.remove('show'), dur);
}

// ── Navigation ────────────────────────────────────────────────────────────
function showSection(name, el) {
  document.querySelectorAll('[id^="section-"]').forEach(s => s.style.display = 'none');
  document.getElementById('section-' + name).style.display = '';
  document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
  if (el) el.classList.add('active');
  if (name === 'chains') renderLogTable();
  if (name === 'admin') { loadAdminKeys(); }
}

// ── Domain selector ───────────────────────────────────────────────────────
async function loadDomains() {
  try {
    const r = await fetch('/dashboard/api/domains');
    const domains = await r.json();
    const sel = document.getElementById('domain-select');
    sel.innerHTML = domains.map(d =>
      `<option value="${d}" ${d===DOMAIN?'selected':''}>${d}</option>`
    ).join('');
    if (!domains.includes(DOMAIN) && domains.length > 0) {
      DOMAIN = domains[0];
      sel.value = DOMAIN;
    }
    sel.addEventListener('change', e => { DOMAIN = e.target.value; refresh(); });
  } catch {}
}

// ── Data fetch ────────────────────────────────────────────────────────────
async function fetchStats() {
  const r = await fetch(`/dashboard/api/stats?domain=${DOMAIN}`);
  return r.json();
}

async function fetchTimeseries(hours) {
  const r = await fetch(`/dashboard/api/timeseries?domain=${DOMAIN}&hours=${hours}`);
  return r.json();
}

async function fetchChains() {
  const r = await fetch(`/dashboard/api/chains?domain=${DOMAIN}&limit=1000`);
  return r.json();
}

async function fetchFailures() {
  const r = await fetch(`/dashboard/api/failures?domain=${DOMAIN}`);
  return r.json();
}

async function fetchDeployStatus() {
  try {
    const r = await fetch('/status');
    return r.json();
  } catch { return null; }
}

// ── Render KPIs ───────────────────────────────────────────────────────────
function renderKPIs(d, chains) {
  document.getElementById('k-total').textContent  = d.n_tracked.toLocaleString();
  document.getElementById('k-alerts').textContent = d.n_alerts.toLocaleString();

  const ar = (d.alert_rate * 100).toFixed(1) + '%';
  const arEl = document.getElementById('k-alert-rate');
  arEl.textContent = ar + ' alert rate';
  arEl.className = 'kpi-sub ' + (d.alert_rate > .3 ? 'val-red' : d.alert_rate > .1 ? 'val-yellow' : 'val-green');

  const avgEl = document.getElementById('k-avg');
  avgEl.textContent = d.avg_risk != null ? d.avg_risk.toFixed(3) : '—';
  avgEl.className = 'kpi-val ' + (d.avg_risk > .65 ? 'val-red' : d.avg_risk > .40 ? 'val-yellow' : 'val-green');

  document.getElementById('k-p95').textContent =
    d.p95_risk != null ? 'P95: ' + d.p95_risk.toFixed(3) : 'P95: —';
  document.getElementById('k-cal').textContent = d.cal_size != null ? d.cal_size : '—';

  // avg search steps
  if (chains.length) {
    const avgSteps = (chains.reduce((s,c) => s + (c.n_steps||0), 0) / chains.length).toFixed(1);
    document.getElementById('k-steps').textContent = avgSteps;
  }
}

// ── Render deployment banner ──────────────────────────────────────────────
function renderDeployBanner(status) {
  if (!status) return;
  const badge = document.getElementById('deploy-badge');
  const meta  = document.getElementById('deploy-meta');
  const auroc = document.getElementById('auroc-val');

  const s = status.deployment_status || 'COLD_START';
  badge.className = `deploy-badge deploy-${s}`;
  const labels = {
    COLD_START: '● COLD START', WARMING: '◑ WARMING', DEPLOYED: '● DEPLOYED'
  };
  badge.textContent = labels[s] || s;

  if (s === 'COLD_START') {
    meta.textContent = `Needs ${status.chains_to_go || '?'} more chains to begin calibrating`;
  } else if (s === 'WARMING') {
    meta.textContent = `Calibrating — ${status.calibration_size} chains in pool`;
  } else {
    meta.textContent = `Deployed with ${status.calibration_size} chains · ${status.embed_model || ''}`;
  }
  auroc.textContent = status.estimated_auroc != null
    ? status.estimated_auroc.toFixed(3) : '—';

  // Update calibration KPI
  document.getElementById('k-cal').textContent = status.calibration_size ?? '—';
}

// ── Render drift banner ───────────────────────────────────────────────────
function renderDrift(d) {
  const banner = document.getElementById('drift-banner');
  if (d && d.drift) {
    banner.classList.add('visible');
    const dr = d.drift;
    document.getElementById('drift-msg').textContent =
      `Mean risk ${dr.direction === 'UP' ? 'increased' : 'decreased'} by ` +
      `${dr.delta > 0 ? '+' : ''}${dr.delta.toFixed(3)} ` +
      `(current 7d avg: ${dr.current_mean.toFixed(3)} vs prev: ${dr.previous_mean.toFixed(3)}). ` +
      `${dr.direction === 'UP' ? 'Consider re-calibrating if you upgraded your model.' : 'Query difficulty may have decreased.'}`;
  } else {
    banner.classList.remove('visible');
  }
}

// ── Risk timeline chart ───────────────────────────────────────────────────
function renderRiskChart(pts) {
  const labels = pts.map(p => {
    const d = new Date(p.t * 1000);
    return currentHours <= 24
      ? d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})
      : d.toLocaleDateString([], {month:'short',day:'numeric'});
  });
  const data = pts.map(p => p.risk);

  const colors = data.map(r => colorForRisk(r));
  const thresholdLine = {
    label: 'Alert threshold',
    data: data.map(() => 0.65),
    borderColor: 'rgba(239,68,68,0.5)',
    borderDash: [4,3],
    borderWidth: 1,
    pointRadius: 0,
    fill: false,
    tension: 0,
  };

  if (riskChart) riskChart.destroy();
  const ctx = document.getElementById('risk-chart').getContext('2d');
  const grad = ctx.createLinearGradient(0, 0, 0, 200);
  grad.addColorStop(0, 'rgba(99,102,241,0.3)');
  grad.addColorStop(1, 'rgba(99,102,241,0)');

  riskChart = new Chart(ctx, {
    type: 'line',
    data: {
      labels,
      datasets: [{
        label: 'Risk Score',
        data,
        borderColor: '#6366f1',
        backgroundColor: grad,
        tension: 0.3,
        pointRadius: pts.length < 60 ? 3 : 0,
        pointBackgroundColor: colors,
        fill: true,
        borderWidth: 2,
      }, thresholdLine]
    },
    options: {
      responsive: true,
      interaction: { mode: 'index', intersect: false },
      plugins: {
        legend: {
          display: true,
          labels: { color: '#64748b', font: { size: 11 }, boxWidth: 12 }
        },
        tooltip: {
          backgroundColor: '#1a2235',
          borderColor: '#1e2d45', borderWidth: 1,
          titleColor: '#94a3b8', bodyColor: '#f1f5f9',
          callbacks: {
            label: ctx => {
              if (ctx.datasetIndex === 1) return null;
              const r = ctx.raw;
              const status = r >= 0.65 ? '🔴 HIGH' : r >= 0.40 ? '🟡 MED' : '🟢 OK';
              return ` Risk: ${r.toFixed(4)}  ${status}`;
            }
          }
        }
      },
      scales: {
        y: {
          min: 0, max: 1,
          grid: { color: '#1e2d45' },
          ticks: { color: '#64748b', stepSize: 0.2 }
        },
        x: {
          grid: { display: false },
          ticks: { color: '#64748b', maxTicksLimit: 8,
                   font: { size: 10 } }
        }
      }
    }
  });
}

// ── Failure mode chart (mini + big) ──────────────────────────────────────
function renderFailChart(failures, canvasId, noId) {
  const canvas = document.getElementById(canvasId);
  const noEl   = document.getElementById(noId);
  const entries = Object.entries(failures).sort((a,b) => b[1] - a[1]);

  if (!entries.length) {
    canvas.style.display = 'none';
    if (noEl) noEl.style.display = '';
    return;
  }
  canvas.style.display = '';
  if (noEl) noEl.style.display = 'none';

  const labels = entries.map(([k]) => k.replace(/_/g,' '));
  const data   = entries.map(([,v]) => v);
  const COLORS  = ['#6366f1','#f59e0b','#ef4444','#22c55e','#a78bfa','#64748b','#f97316'];

  const existing = Chart.getChart(canvas);
  if (existing) existing.destroy();

  new Chart(canvas.getContext('2d'), {
    type: 'bar',
    data: {
      labels,
      datasets: [{
        data,
        backgroundColor: COLORS.slice(0, labels.length).map(c => c + 'cc'),
        borderColor:     COLORS.slice(0, labels.length),
        borderWidth: 1,
        borderRadius: 4,
      }]
    },
    options: {
      indexAxis: 'y',
      responsive: true,
      plugins: {
        legend: { display: false },
        tooltip: {
          backgroundColor: '#1a2235', borderColor: '#1e2d45', borderWidth: 1,
          titleColor: '#94a3b8', bodyColor: '#f1f5f9',
        }
      },
      scales: {
        x: { grid: { color: '#1e2d45' }, ticks: { color: '#64748b' } },
        y: { grid: { display: false }, ticks: { color: '#94a3b8', font: { size: 11 } } }
      }
    }
  });
}

// ── Failure mode table (detail section) ──────────────────────────────────
function renderFailTable(failures) {
  const tbody = document.getElementById('fail-tbody');
  const entries = Object.entries(failures).sort((a,b) => b[1] - a[1]);
  if (!entries.length) {
    tbody.innerHTML = '<tr><td colspan="3" class="empty-state">No failure modes recorded.</td></tr>';
    return;
  }
  tbody.innerHTML = entries.map(([mode, count]) => `
    <tr>
      <td><span class="fm-tag">${mode.replace(/_/g,' ')}</span></td>
      <td style="font-weight:600;color:var(--red2)">${count}</td>
      <td style="color:var(--muted2);font-size:12px">${FAILURE_DESCS[mode] || '—'}</td>
    </tr>`).join('');
}

// ── Top-alerts mini table (overview) ─────────────────────────────────────
function renderTopTable(chains) {
  const tbody = document.getElementById('top-tbody');
  const top   = chains.filter(c => c.alert).slice(0, 8);
  document.getElementById('top-count').textContent =
    top.length ? `${top.length} shown` : '';
  if (!top.length) {
    tbody.innerHTML = `<tr><td colspan="6">
      <div class="empty-state"><div class="empty-icon">✅</div>No alerts yet — all chains are healthy.</div>
    </td></tr>`;
    return;
  }
  tbody.innerHTML = top.map(c => `
    <tr>
      <td class="q-cell" title="${escHtml(c.question)}">${escHtml(c.question)}</td>
      <td>${riskBadge(c.risk_score)}</td>
      <td>${c.failure_mode ? `<span class="fm-tag">${c.failure_mode.replace(/_/g,' ')}</span>` : '<span style="color:var(--muted)">—</span>'}</td>
      <td style="color:var(--muted2)">${c.n_steps ?? '—'}</td>
      <td class="model-cell" title="${c.model_name||''}">${c.model_name || '—'}</td>
      <td class="ts-cell">${fmtTime(c.timestamp)}</td>
    </tr>`).join('');
}

// ── Chain log table (full, paginated) ────────────────────────────────────
let sortCol = 'timestamp', sortDir = -1;

function escHtml(s) {
  return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

function renderLogTable() {
  const search = (document.getElementById('chain-search').value || '').toLowerCase();
  let rows = allChains;
  if (filterMode === 'alerts') rows = rows.filter(c => c.alert);
  if (search) rows = rows.filter(c => (c.question||'').toLowerCase().includes(search));

  rows = [...rows].sort((a,b) => {
    const av = a[sortCol] ?? '', bv = b[sortCol] ?? '';
    return av < bv ? -sortDir : av > bv ? sortDir : 0;
  });

  const total   = rows.length;
  const pages   = Math.max(1, Math.ceil(total / PAGE_SIZE));
  currentPage   = Math.min(currentPage, pages);
  const start   = (currentPage - 1) * PAGE_SIZE;
  const visible = rows.slice(start, start + PAGE_SIZE);

  document.getElementById('log-count').textContent =
    `${total.toLocaleString()} chains${search ? ' (filtered)' : ''}`;
  document.getElementById('page-info').textContent = `${currentPage} / ${pages}`;
  document.getElementById('prev-btn').disabled = currentPage <= 1;
  document.getElementById('next-btn').disabled = currentPage >= pages;

  const tbody = document.getElementById('log-tbody');
  if (!visible.length) {
    tbody.innerHTML = `<tr><td colspan="7">
      <div class="empty-state"><div class="empty-icon">🔍</div>No chains match your filter.</div>
    </td></tr>`;
    return;
  }
  tbody.innerHTML = visible.map((c, i) => `
    <tr>
      <td style="color:var(--muted);font-size:11px">${start+i+1}</td>
      <td class="q-cell" title="${escHtml(c.question)}">${escHtml(c.question)}</td>
      <td>${riskBadge(c.risk_score)}</td>
      <td>${c.failure_mode ? `<span class="fm-tag">${c.failure_mode.replace(/_/g,' ')}</span>` : '<span style="color:var(--muted)">—</span>'}</td>
      <td style="color:var(--muted2)">${c.n_steps ?? '—'}</td>
      <td class="model-cell">${c.model_name || '—'}</td>
      <td class="ts-cell">${fmtTime(c.timestamp)}</td>
    </tr>`).join('');
}

function prevPage() { if (currentPage > 1) { currentPage--; renderLogTable(); } }
function nextPage() {
  const pages = Math.ceil(allChains.length / PAGE_SIZE);
  if (currentPage < pages) { currentPage++; renderLogTable(); }
}

function filterTable() { currentPage = 1; renderLogTable(); }

function setFilter(el) {
  document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
  el.classList.add('active');
  filterMode = el.dataset.filter;
  currentPage = 1;
  renderLogTable();
}

// ── Sortable table headers ─────────────────────────────────────────────────
document.querySelectorAll('th.sortable').forEach(th => {
  th.addEventListener('click', () => {
    const col = th.dataset.col;
    if (sortCol === col) sortDir *= -1;
    else { sortCol = col; sortDir = -1; }
    document.querySelectorAll('th.sortable').forEach(t => {
      t.textContent = t.textContent.replace(/ [↑↓]$/, '');
    });
    th.textContent += sortDir === 1 ? ' ↑' : ' ↓';
    renderLogTable();
  });
});

// ── Range buttons ─────────────────────────────────────────────────────────
function setRange(el) {
  document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active'));
  el.classList.add('active');
  currentHours = parseInt(el.dataset.h);
  fetchTimeseries(currentHours).then(renderRiskChart);
}

// ── Export CSV ────────────────────────────────────────────────────────────
async function exportCSV() {
  const url = `/api/${DOMAIN}/audit-log?fmt=csv`;
  // Fallback to dashboard export
  const rows = allChains;
  if (!rows.length) { showToast('No data to export'); return; }
  const cols = ['timestamp','question','risk_score','alert','failure_mode','n_steps','model_name'];
  const header = cols.join(',');
  const lines  = rows.map(r =>
    cols.map(c => JSON.stringify(r[c] ?? '')).join(',')
  );
  const blob = new Blob([header + '\n' + lines.join('\n')], {type: 'text/csv'});
  const a = Object.assign(document.createElement('a'), {
    href: URL.createObjectURL(blob),
    download: `llm-guard-kit-${DOMAIN}-${Date.now()}.csv`,
  });
  a.click();
  showToast('CSV downloaded');
}

// ── Countdown & auto-refresh ──────────────────────────────────────────────
function startCountdown() {
  countdownSec = 30;
  clearInterval(countdownTimer);
  countdownTimer = setInterval(() => {
    countdownSec--;
    document.getElementById('countdown').textContent =
      countdownSec > 0 ? `· refresh in ${countdownSec}s` : '';
    if (countdownSec <= 0) {
      clearInterval(countdownTimer);
      refresh();
    }
  }, 1000);
}

// ── Main refresh ──────────────────────────────────────────────────────────
async function refresh() {
  try {
    const [stats, pts, chains, failures, status] = await Promise.all([
      fetchStats(),
      fetchTimeseries(currentHours),
      fetchChains(),
      fetchFailures(),
      fetchDeployStatus(),
    ]);

    allChains = chains;
    failData  = failures;

    renderKPIs(stats, chains);
    renderDrift(stats);
    renderDeployBanner(status);
    renderRiskChart(pts);
    renderFailChart(failures, 'fail-chart', 'no-failures');
    renderFailChart(failures, 'fail-chart-big', 'no-failures-big');
    renderFailTable(failures);
    renderTopTable(chains);
    renderLogTable();

    document.getElementById('refresh-ts').textContent =
      'Updated ' + new Date().toLocaleTimeString();
    startCountdown();
  } catch(e) {
    console.error(e);
    showToast('Error loading data — retrying…', 3000);
  }
}

// ── Admin: helpers ────────────────────────────────────────────────────────
function esc(s) {
  return String(s)
    .replace(/&/g,'&amp;').replace(/</g,'&lt;')
    .replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

// ── Admin: tab navigation ─────────────────────────────────────────────────
function showAdminTab(tab) {
  document.querySelectorAll('.atab').forEach(b => b.classList.remove('active'));
  document.querySelectorAll('.admin-panel').forEach(p => p.classList.remove('active'));
  const btn = document.querySelector(`.atab[onclick="showAdminTab('${tab}')"]`);
  if (btn) btn.classList.add('active');
  const panel = document.getElementById('atab-' + tab);
  if (panel) panel.classList.add('active');
  if (tab === 'keys')       loadAdminKeys();
  if (tab === 'domains')    loadAdminDomains();
  if (tab === 'calibrate')  populateCrossSource();
  if (tab === 'playground') populatePgDomain();
  if (tab === 'review')     initReviewQueue();
}

// ── Admin: Review Queue ────────────────────────────────────────────────────
async function initReviewQueue() {
  // Populate domain dropdown from known domains
  const sel = document.getElementById('rq-domain');
  if (sel && sel.options.length === 0) {
    try {
      const doms = await fetch('/dashboard/api/domains').then(r => r.json());
      sel.innerHTML = doms.map(d => `<option value="${esc(d)}">${esc(d)}</option>`).join('');
    } catch(_) {}
  }
  loadReviewQueue();
}

async function loadReviewQueue() {
  const domain = document.getElementById('rq-domain')?.value || DOMAIN;
  const n      = document.getElementById('rq-n')?.value     || 10;
  const tbody  = document.getElementById('rq-tbody');
  const empty  = document.getElementById('rq-empty');
  if (!tbody) return;
  tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--muted);padding:16px">Loading…</td></tr>';
  if (empty) empty.style.display = 'none';
  try {
    const data = await fetch(
      `/dashboard/api/admin/review?domain=${encodeURIComponent(domain)}&n=${n}`
    ).then(r => r.json());
    if (!data.length) {
      tbody.innerHTML = '';
      if (empty) empty.style.display = '';
      return;
    }
    tbody.innerHTML = data.map(c => {
      const riskClass = c.risk_score > 0.65 ? 'var(--red)' : 'var(--yellow)';
      const ts = c.timestamp ? new Date(c.timestamp * 1000).toLocaleString() : '—';
      return `<tr>
        <td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px" title="${esc(c.question)}">${esc(c.question || '—')}</td>
        <td><span style="color:${riskClass};font-weight:600">${c.risk_score.toFixed(3)}</span></td>
        <td style="font-size:11px;color:var(--muted2)">${esc(c.failure_mode || '—')}</td>
        <td style="font-size:11px;color:var(--muted)">${ts}</td>
        <td style="white-space:nowrap">
          <button onclick="reviewChain(${c.chain_id},'accept','${encodeURIComponent(domain)}')"
            style="background:var(--green);color:#fff;border:none;border-radius:5px;padding:4px 10px;cursor:pointer;font-size:11px;margin-right:4px">✓ Correct</button>
          <button onclick="reviewChain(${c.chain_id},'reject','${encodeURIComponent(domain)}')"
            style="background:var(--red);color:#fff;border:none;border-radius:5px;padding:4px 10px;cursor:pointer;font-size:11px">✗ Wrong</button>
        </td>
      </tr>`;
    }).join('');
  } catch(e) {
    tbody.innerHTML = `<tr><td colspan="5" style="color:var(--red);padding:16px">${esc(String(e))}</td></tr>`;
  }
}

async function reviewChain(chainId, action, domainEnc) {
  try {
    const resp = await fetch(`/dashboard/api/admin/review/${chainId}/${action}`, {
      method: 'POST',
    }).then(r => r.json());
    if (resp.labeled) {
      // Remove the row immediately without full reload
      const row = document.querySelector(`[data-chain="${chainId}"]`);
      if (row) row.remove();
      loadReviewQueue();  // refresh to show updated count
    } else {
      alert('Error: ' + (resp.detail || JSON.stringify(resp)));
    }
  } catch(e) {
    alert('Network error: ' + e);
  }
}

function showSeedTab(tab) {
  document.querySelectorAll('.stab').forEach(b => b.classList.remove('active'));
  document.getElementById('stab-seed').style.display  = tab === 'seed'  ? '' : 'none';
  document.getElementById('stab-cross').style.display = tab === 'cross' ? '' : 'none';
  const btn = document.querySelector(`.stab[onclick="showSeedTab('${tab}')"]`);
  if (btn) btn.classList.add('active');
}

// ── Admin: API keys ───────────────────────────────────────────────────────
async function loadAdminKeys() {
  const tbody = document.getElementById('keys-tbody');
  tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--muted);padding:16px">Loading…</td></tr>';
  try {
    const r = await fetch('/dashboard/api/admin/keys');
    const keys = await r.json();
    if (!keys.length) {
      tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--muted);padding:24px">No API keys yet — create one above.</td></tr>';
      return;
    }
    tbody.innerHTML = keys.map(k => `
      <tr>
        <td style="font-weight:600">${esc(k.customer_id)}</td>
        <td><code style="font-size:11px;color:var(--muted2)">${esc(k.domain_prefix) || '—'}</code></td>
        <td style="color:var(--muted)">${fmtTime(k.created_at)}</td>
        <td>${k.request_count.toLocaleString()}</td>
        <td><span class="status-pill ${k.is_active ? 'active' : 'revoked'}">${k.is_active ? 'Active' : 'Revoked'}</span></td>
        <td>${k.is_active
          ? `<button class="danger-btn" onclick="revokeKey('${esc(k.key_id)}')">Revoke</button>`
          : '—'}</td>
      </tr>`).join('');
  } catch {
    tbody.innerHTML = '<tr><td colspan="6" style="color:var(--red);padding:16px">Error loading keys</td></tr>';
  }
}

async function createApiKey() {
  const cid = document.getElementById('key-customer').value.trim();
  const pfx = document.getElementById('key-domain-pfx').value.trim();
  if (!cid) { showToast('Customer ID is required'); return; }
  try {
    const r = await fetch('/dashboard/api/admin/keys', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({customer_id: cid, domain_prefix: pfx}),
    });
    const data = await r.json();
    if (!r.ok) { showToast('Error: ' + (data.detail || 'Failed')); return; }
    const box = document.getElementById('key-result');
    document.getElementById('key-value').textContent = data.api_key;
    box.style.display = 'block';
    loadAdminKeys();
    showToast('API key created — copy it now!', 4000);
  } catch { showToast('Error creating key'); }
}

function copyKey() {
  const val = document.getElementById('key-value').textContent;
  navigator.clipboard.writeText(val).then(() => showToast('Copied to clipboard!'));
}

async function revokeKey(keyId) {
  if (!confirm(`Revoke key ${keyId}...? Active connections using this key will stop working.`)) return;
  try {
    const r = await fetch(`/dashboard/api/admin/keys/${keyId}/revoke`, {method: 'POST'});
    if (r.ok) { showToast('Key revoked'); loadAdminKeys(); }
    else showToast('Error revoking key');
  } catch { showToast('Error'); }
}

// ── Admin: Domains ────────────────────────────────────────────────────────
async function loadAdminDomains() {
  const tbody = document.getElementById('domains-tbody');
  tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--muted);padding:16px">Loading…</td></tr>';
  try {
    const r = await fetch('/dashboard/api/admin/domains');
    const data = await r.json();
    if (!data.length) {
      tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--muted);padding:24px">No domains found.</td></tr>';
      return;
    }
    tbody.innerHTML = data.map(d => `
      <tr>
        <td style="font-weight:600">${esc(d.domain)}</td>
        <td>${(d.n_chains||0).toLocaleString()}</td>
        <td>${riskBadge(d.avg_risk||0)}</td>
        <td>${(d.n_alerts||0).toLocaleString()}</td>
        <td style="color:var(--muted)">${fmtTime(d.last_active)}</td>
        <td><button class="danger-btn" onclick="clearDomain('${esc(d.domain)}')">Clear All</button></td>
      </tr>`).join('');
  } catch {
    tbody.innerHTML = '<tr><td colspan="6" style="color:var(--red);padding:16px">Error loading domains</td></tr>';
  }
}

async function clearDomain(domain) {
  if (!confirm(`Delete ALL chains for domain "${domain}"?\n\nThis will reset the calibration pool and cannot be undone.`)) return;
  try {
    const r = await fetch(`/dashboard/api/admin/domains/${encodeURIComponent(domain)}`, {method: 'DELETE'});
    const d = await r.json();
    showToast(`Cleared ${d.deleted} chains from "${domain}"`);
    loadAdminDomains();
    if (domain === DOMAIN) refresh();
  } catch { showToast('Error clearing domain'); }
}

// ── Admin: Calibrate helpers ───────────────────────────────────────────────
async function populateCrossSource() {
  const sel = document.getElementById('cross-source');
  try {
    const r = await fetch('/dashboard/api/domains');
    const domains = await r.json();
    sel.innerHTML = '<option value="">— select domain —</option>' +
      domains.map(d => `<option value="${esc(d)}">${esc(d)}</option>`).join('');
  } catch {}
}

async function populatePgDomain() {
  const sel = document.getElementById('pg-domain');
  try {
    const r = await fetch('/dashboard/api/domains');
    const domains = await r.json();
    const cur = sel.value;
    sel.innerHTML = domains.map(d =>
      `<option value="${esc(d)}" ${d===cur?'selected':''}>${esc(d)}</option>`
    ).join('');
  } catch {}
}

async function seedChains() {
  const domain = document.getElementById('seed-domain').value.trim() || 'default';
  const raw    = document.getElementById('seed-json').value.trim();
  const res    = document.getElementById('seed-result');
  res.style.display = 'none';
  let chains;
  try { chains = JSON.parse(raw); } catch {
    res.className = 'result-box err';
    res.textContent = 'Invalid JSON — check your input.';
    res.style.display = 'block'; return;
  }
  if (!Array.isArray(chains)) {
    res.className = 'result-box err';
    res.textContent = 'Expected a JSON array of chain objects.';
    res.style.display = 'block'; return;
  }
  try {
    const r = await fetch('/dashboard/api/admin/seed', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({domain, chains}),
    });
    const d = await r.json();
    res.className = 'result-box ok';
    res.textContent = `✓ Seeded ${d.seeded} chains into domain "${d.domain}"`;
    res.style.display = 'block';
    showToast(`Seeded ${d.seeded} chains`);
  } catch {
    res.className = 'result-box err';
    res.textContent = 'Error seeding chains.';
    res.style.display = 'block';
  }
}

async function seedCross() {
  const source = document.getElementById('cross-source').value;
  const target = document.getElementById('cross-target').value.trim();
  const n      = parseInt(document.getElementById('cross-n').value);
  const res    = document.getElementById('cross-result');
  res.style.display = 'none';
  if (!source) { showToast('Select a source domain'); return; }
  if (!target) { showToast('Enter a target domain name'); return; }
  try {
    const r = await fetch('/dashboard/api/admin/seed/cross', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({source_domain: source, target_domain: target, n}),
    });
    const d = await r.json();
    res.className = 'result-box ok';
    res.textContent = `✓ Copied ${d.copied} chains from "${d.source}" → "${d.target}"`;
    res.style.display = 'block';
    showToast(`Warm-up complete: ${d.copied} chains copied`);
  } catch {
    res.className = 'result-box err';
    res.textContent = 'Error during cross-domain warm-up.';
    res.style.display = 'block';
  }
}

// ── Admin: Score Playground ───────────────────────────────────────────────
async function scorePlayground() {
  const question = document.getElementById('pg-question').value.trim();
  const domain   = document.getElementById('pg-domain').value;
  const raw      = document.getElementById('pg-steps').value.trim();
  const res      = document.getElementById('pg-result');
  res.style.display = 'none';
  let steps;
  try { steps = JSON.parse(raw); } catch {
    showToast('Invalid JSON in steps field'); return;
  }
  try {
    const r = await fetch('/dashboard/api/admin/score', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({question, steps, domain}),
    });
    const d = await r.json();
    if (!r.ok) { showToast('Error: ' + (d.detail || 'Scoring failed')); return; }
    res.style.display = 'block';
    const rEl = document.getElementById('pg-risk');
    rEl.textContent = d.risk_score != null ? d.risk_score.toFixed(3) : '—';
    rEl.style.color = colorForRisk(d.risk_score || 0);
    document.getElementById('pg-beh').textContent = d.behavioral_score != null ? d.behavioral_score.toFixed(3) : '—';
    document.getElementById('pg-gmm').textContent = d.gmm_score       != null ? d.gmm_score.toFixed(3)       : '—';
    document.getElementById('pg-cal').textContent = d.calibration_size != null ? d.calibration_size           : '—';
    const verdict = document.getElementById('pg-verdict');
    if (d.needs_review) {
      verdict.textContent = '⚠ High Risk — this chain needs review';
      verdict.style.color = 'var(--red)';
    } else {
      verdict.textContent = '✓ Low Risk — chain looks reliable';
      verdict.style.color = 'var(--green)';
    }
  } catch(e) { showToast('Error: ' + e.message); }
}

// ── JWT Auth ──────────────────────────────────────────────────────────────
const AUTH_REQUIRED = __AUTH_REQUIRED__;

function getToken() { return localStorage.getItem('qppg_jwt'); }
function setToken(t) { localStorage.setItem('qppg_jwt', t); }
function clearToken() { localStorage.removeItem('qppg_jwt'); }

function authHeaders() {
  const t = getToken();
  return t ? { 'Authorization': 'Bearer ' + t } : {};
}

async function verifyToken() {
  if (!AUTH_REQUIRED) return true;
  const t = getToken();
  if (!t) return false;
  try {
    const r = await fetch('/dashboard/auth/verify', { headers: { 'Authorization': 'Bearer ' + t } });
    return r.ok;
  } catch { return false; }
}

async function doLogin() {
  const pw = document.getElementById('login-pw').value;
  const err = document.getElementById('login-err');
  err.textContent = '';
  try {
    const r = await fetch('/dashboard/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ password: pw })
    });
    if (r.ok) {
      const d = await r.json();
      setToken(d.access_token);
      document.getElementById('login-overlay').style.display = 'none';
      loadDomains().then(() => refresh());
    } else {
      err.textContent = 'Invalid password.';
    }
  } catch { err.textContent = 'Server error.'; }
}

function doLogout() {
  clearToken();
  if (AUTH_REQUIRED) {
    document.getElementById('login-overlay').style.display = 'flex';
  }
}

document.getElementById('login-pw').addEventListener('keydown', e => {
  if (e.key === 'Enter') doLogin();
});

// Override fetch for admin routes to inject token
const _origFetch = fetch;
window.fetch = function(url, opts) {
  if (typeof url === 'string' && url.startsWith('/dashboard/api/admin') && AUTH_REQUIRED) {
    opts = opts || {};
    opts.headers = Object.assign({}, opts.headers, authHeaders());
  }
  return _origFetch(url, opts);
};

// ── Init ──────────────────────────────────────────────────────────────────
async function init() {
  if (AUTH_REQUIRED) {
    const valid = await verifyToken();
    if (!valid) {
      document.getElementById('login-overlay').style.display = 'flex';
      return;  // don't load data until logged in
    }
  }
  loadDomains().then(() => refresh());
}
init();
</script>

<!-- Login overlay (shown when AUTH_REQUIRED and no valid token) -->
<div id="login-overlay" style="display:none;position:fixed;inset:0;background:rgba(10,12,19,0.95);
  z-index:9999;align-items:center;justify-content:center;flex-direction:column;">
  <div style="background:#111827;border:1px solid #1e2d45;border-radius:12px;
    padding:2rem 2.5rem;min-width:320px;max-width:400px;width:90%;">
    <div style="font-size:1.2rem;font-weight:700;color:#f1f5f9;margin-bottom:0.5rem;">
      llm-guard-kit
    </div>
    <div style="color:#64748b;margin-bottom:1.5rem;font-size:0.85rem;">
      Dashboard login required
    </div>
    <label style="display:block;color:#94a3b8;font-size:0.8rem;margin-bottom:0.4rem;">
      Password
    </label>
    <input id="login-pw" type="password" placeholder="Dashboard key"
      style="width:100%;padding:0.6rem 0.8rem;background:#1a2235;border:1px solid #1e2d45;
        border-radius:6px;color:#f1f5f9;font-size:0.9rem;outline:none;margin-bottom:0.8rem;"
      autofocus>
    <div id="login-err" style="color:#ef4444;font-size:0.8rem;min-height:1rem;margin-bottom:0.6rem;"></div>
    <button onclick="doLogin()"
      style="width:100%;padding:0.65rem;background:#6366f1;border:none;border-radius:6px;
        color:#fff;font-size:0.9rem;font-weight:600;cursor:pointer;">
      Sign in
    </button>
  </div>
</div>

</body>
</html>"""


# ── Route registration ─────────────────────────────────────────────────────────

def add_dashboard_routes(
    app: FastAPI,
    store: "ChainStore",
    *,
    dashboard_key: Optional[str] = None,
) -> None:
    """
    Register dashboard routes on an existing FastAPI app.

    Parameters
    ----------
    app           : FastAPI application instance
    store         : ChainStore for reading/writing chain data
    dashboard_key : If set, all /dashboard/api/admin/* routes require an
                    ``X-Dashboard-Key: <value>`` header.  Pass via
                    ``llm-guard-kit serve --dashboard-key <secret>``.

    Routes added:
        GET /dashboard                    — main dashboard HTML page
        GET /dashboard/api/stats          — summary stats JSON
        GET /dashboard/api/timeseries     — risk score time series JSON
        GET /dashboard/api/chains         — all chains JSON (sorted by risk desc)
        GET /dashboard/api/failures       — failure mode counts JSON
        GET /dashboard/api/domains        — list of all domain names JSON
        (admin routes — protected by X-Dashboard-Key when dashboard_key is set)
        GET /dashboard/api/admin/keys
        POST /dashboard/api/admin/keys
        POST /dashboard/api/admin/keys/{key_prefix}/revoke
        GET /dashboard/api/admin/domains
        DELETE /dashboard/api/admin/domains/{domain}
        POST /dashboard/api/admin/score
        POST /dashboard/api/admin/seed
        POST /dashboard/api/admin/seed/cross
        GET /dashboard/api/admin/review
        POST /dashboard/api/admin/review/{chain_id}/{action}
    """
    from .drift import DriftDetector
    detector = DriftDetector()

    # ── JWT helpers ─────────────────────────────────────────────────────────

    import hashlib as _hashlib

    def _jwt_secret() -> bytes:
        """Derive a 32-byte signing secret from the dashboard_key."""
        return _hashlib.sha256(
            (dashboard_key or "no-auth").encode()
        ).digest()

    def _issue_token() -> str:
        """Issue a JWT with 24-hour expiry."""
        import jwt as _jwt
        import datetime as _dt
        payload = {
            "sub": "admin",
            "exp": _dt.datetime.utcnow() + _dt.timedelta(hours=24),
        }
        return _jwt.encode(payload, _jwt_secret(), algorithm="HS256")

    def _verify_jwt(token: str) -> bool:
        """Return True if token is valid and not expired."""
        try:
            import jwt as _jwt
            _jwt.decode(token, _jwt_secret(), algorithms=["HS256"])
            return True
        except Exception:
            return False

    # ── Admin auth dependency ────────────────────────────────────────────────

    def _require_admin(request: Request) -> None:
        """FastAPI dependency: enforces JWT Bearer token OR X-Dashboard-Key."""
        if not dashboard_key:
            return  # auth disabled
        # Accept JWT Bearer token (preferred)
        auth_header = request.headers.get("authorization", "")
        if auth_header.startswith("Bearer "):
            token = auth_header.removeprefix("Bearer ").strip()
            if _verify_jwt(token):
                return
        # Fallback: legacy X-Dashboard-Key header
        if request.headers.get("x-dashboard-key") == dashboard_key:
            return
        raise HTTPException(401, "Unauthorized: valid Bearer token or X-Dashboard-Key required.")

    # ── Auth endpoints ───────────────────────────────────────────────────────

    @app.post("/dashboard/auth/login", include_in_schema=False)
    async def dashboard_login(req: _DashLoginRequest):
        """Exchange dashboard_key password for a 24-hour JWT access token."""
        if not dashboard_key:
            # Auth disabled — return a dummy token so frontend is happy
            return {"access_token": _issue_token(), "token_type": "bearer"}
        if req.password != dashboard_key:
            raise HTTPException(401, "Invalid password.")
        return {"access_token": _issue_token(), "token_type": "bearer"}

    @app.get("/dashboard/auth/verify", include_in_schema=False)
    async def dashboard_verify(request: Request):
        """Verify a JWT Bearer token (used by frontend on page load)."""
        if not dashboard_key:
            return {"valid": True}
        auth = request.headers.get("authorization", "")
        if auth.startswith("Bearer ") and _verify_jwt(auth.removeprefix("Bearer ").strip()):
            return {"valid": True}
        raise HTTPException(401, "Token invalid or expired.")

    @app.get("/dashboard", response_class=HTMLResponse, include_in_schema=False)
    async def dashboard_page(domain: str = "default"):
        auth_flag = "true" if dashboard_key else "false"
        return HTMLResponse(_HTML.replace("__AUTH_REQUIRED__", auth_flag))

    @app.get("/dashboard/api/stats", include_in_schema=False)
    async def dashboard_stats(domain: str = "default"):
        import numpy as np

        rows = store.get_chains(domain, limit=10_000)
        if not rows:
            return {
                "n_tracked": 0, "n_alerts": 0, "alert_rate": 0.0,
                "avg_risk": 0.0, "p95_risk": 0.0, "cal_size": 0, "drift": None,
            }

        risks    = [r["risk_score"]      for r in rows]
        n_alerts = sum(1 for r in rows if r.get("alert_triggered"))
        cal_size = len(store.get_calibration_pool(domain, n=2000))

        drift_alert = detector.check(domain, store)
        drift_data  = None
        if drift_alert:
            drift_data = {
                "delta":         drift_alert.delta,
                "current_mean":  drift_alert.current_mean,
                "previous_mean": drift_alert.previous_mean,
                "direction":     drift_alert.direction,
                "recommendation": drift_alert.recommendation,
            }

        return {
            "n_tracked":  len(rows),
            "n_alerts":   n_alerts,
            "alert_rate": round(n_alerts / len(rows), 4),
            "avg_risk":   round(float(np.mean(risks)), 4),
            "p95_risk":   round(float(np.percentile(risks, 95)), 4),
            "cal_size":   cal_size,
            "drift":      drift_data,
        }

    @app.get("/dashboard/api/timeseries", include_in_schema=False)
    async def dashboard_timeseries(domain: str = "default", hours: int = 24):
        since = time.time() - hours * 3600
        pts   = store.get_recent_risk(domain, days=max(1, hours // 24 + 1))
        pts   = [(t, r) for t, r in pts if t >= since]
        # Downsample to ≤500 points
        step  = max(1, len(pts) // 500)
        pts   = pts[::step]
        return [{"t": t, "risk": round(r, 4)} for t, r in pts]

    @app.get("/dashboard/api/chains", include_in_schema=False)
    async def dashboard_chains(domain: str = "default", limit: int = 1000):
        rows = store.get_chains(domain, limit=limit)
        # Sort by risk desc (highest risk first)
        rows.sort(key=lambda r: r.get("risk_score", 0), reverse=True)
        return [
            {
                "question":     (r.get("question") or "")[:200],
                "risk_score":   round(r.get("risk_score", 0.0), 4),
                "failure_mode": r.get("failure_mode") or "",
                "n_steps":      r.get("n_steps", 0),
                "model_name":   r.get("model_name") or "",
                "timestamp":    r.get("timestamp"),
                "alert":        bool(r.get("alert_triggered", 0)),
            }
            for r in rows
        ]

    @app.get("/dashboard/api/failures", include_in_schema=False)
    async def dashboard_failures(domain: str = "default"):
        """Return {failure_mode: count} for alerted chains."""
        return store.get_failure_mode_counts(domain)

    @app.get("/dashboard/api/domains", include_in_schema=False)
    async def dashboard_domains():
        """Return list of all domain names in the store."""
        domains = store.get_domains()
        return domains if domains else ["default"]

    # ── Admin: API key management ──────────────────────────────────────────

    @app.get("/dashboard/api/admin/keys", include_in_schema=False,
             dependencies=[Depends(_require_admin)])
    async def admin_list_keys():
        """List all API keys (hashes omitted — safe to show in UI)."""
        with store._conn() as conn:
            rows = conn.execute(
                "SELECT key_hash, customer_id, domain_prefix, created_at, "
                "is_active, request_count FROM api_keys ORDER BY created_at DESC"
            ).fetchall()
        return [
            {
                "key_id":        dict(r)["key_hash"][:12],
                "customer_id":   dict(r)["customer_id"],
                "domain_prefix": dict(r)["domain_prefix"] or "",
                "created_at":    dict(r)["created_at"],
                "is_active":     bool(dict(r)["is_active"]),
                "request_count": dict(r)["request_count"],
            }
            for r in rows
        ]

    @app.post("/dashboard/api/admin/keys", include_in_schema=False,
              dependencies=[Depends(_require_admin)])
    async def admin_create_key(request: Request):
        """Create a new API key and return it (plaintext, only time)."""
        body          = await request.json()
        customer_id   = (body.get("customer_id") or "").strip()
        domain_prefix = (body.get("domain_prefix") or "").strip()
        if not customer_id:
            return JSONResponse({"detail": "customer_id is required"}, status_code=400)
        raw_key = store.create_api_key(customer_id, domain_prefix)
        return {"api_key": raw_key, "customer_id": customer_id, "domain_prefix": domain_prefix}

    @app.post("/dashboard/api/admin/keys/{key_prefix}/revoke", include_in_schema=False,
              dependencies=[Depends(_require_admin)])
    async def admin_revoke_key(key_prefix: str):
        """Deactivate an API key identified by its 12-char prefix."""
        from fastapi import HTTPException
        try:
            revoked = store.revoke_key_by_prefix(key_prefix)
        except ValueError as e:
            raise HTTPException(409, str(e))
        if not revoked:
            raise HTTPException(404, "Key not found or already revoked")
        return {"revoked": True, "key_prefix": key_prefix}

    # ── Admin: Domain management ───────────────────────────────────────────

    @app.get("/dashboard/api/admin/domains", include_in_schema=False,
             dependencies=[Depends(_require_admin)])
    async def admin_list_domains():
        """Return per-domain stats for the management table."""
        domains = store.get_domains()
        return [store.get_domain_stats(d) for d in domains]

    @app.delete("/dashboard/api/admin/domains/{domain}", include_in_schema=False,
                dependencies=[Depends(_require_admin)])
    async def admin_clear_domain(domain: str):
        """Delete all chains for a domain (irreversible)."""
        n = store.clear_domain(domain)
        return {"deleted": n, "domain": domain}

    # ── Admin: Score playground ────────────────────────────────────────────

    @app.post("/dashboard/api/admin/score", include_in_schema=False,
              dependencies=[Depends(_require_admin)])
    async def admin_score(request: Request):
        """Score an arbitrary chain — for playground / smoke testing."""
        body     = await request.json()
        question = body.get("question", "")
        steps    = body.get("steps", [])
        domain   = body.get("domain", "default")
        from .label_free_scorer import LabelFreeScorer
        scorer = LabelFreeScorer()
        pool   = store.get_calibration_pool(domain, n=200)
        if pool:
            scorer.calibrate(pool)
        result = scorer.score(question, steps, "", True)
        return {
            "risk_score":       result.risk_score,
            "needs_review":     result.needs_review,
            "behavioral_score": result.behavioral_score,
            "gmm_score":        result.gmm_score,
            "calibration_size": result.calibration_size,
        }

    # ── Admin: Calibration seeding ─────────────────────────────────────────

    @app.post("/dashboard/api/admin/seed", include_in_schema=False,
              dependencies=[Depends(_require_admin)])
    async def admin_seed(request: Request):
        """Bulk-seed a domain's calibration pool with chains from the UI."""
        body   = await request.json()
        domain = body.get("domain", "default")
        chains = body.get("chains", [])
        count  = 0
        for chain in chains:
            if isinstance(chain, dict):
                store.add_chain(domain, chain, risk_score=0.0)
                count += 1
        return {"seeded": count, "domain": domain}

    @app.post("/dashboard/api/admin/seed/cross", include_in_schema=False,
              dependencies=[Depends(_require_admin)])
    async def admin_seed_cross(request: Request):
        """Copy N recent chains from source domain into target domain."""
        body   = await request.json()
        source = (body.get("source_domain") or "").strip()
        target = (body.get("target_domain") or "").strip()
        n      = int(body.get("n", 50))
        if not source:
            return JSONResponse({"detail": "source_domain is required"}, status_code=400)
        chains = store.get_calibration_pool(source, n=n)
        count  = 0
        for chain in chains:
            store.add_chain(target, chain, risk_score=0.0)
            count += 1
        return {"copied": count, "source": source, "target": target}

    # ── Admin: Active learning review queue ────────────────────────────────

    @app.get("/dashboard/api/admin/review", include_in_schema=False,
             dependencies=[Depends(_require_admin)])
    async def admin_review_queue(domain: str = "default", n: int = 10):
        """Return the N most recent alerted, unreviewed chains for labeling."""
        return store.get_review_queue(domain, n=n)

    @app.post("/dashboard/api/admin/review/{chain_id}/{action}", include_in_schema=False,
              dependencies=[Depends(_require_admin)])
    async def admin_label_chain(chain_id: int, action: str):
        """
        Label a chain from the review queue.

        action: 'accept' (agent was correct despite alert) or
                'reject' (alert was justified — agent failed).
        """
        if action not in ("accept", "reject"):
            raise HTTPException(400, "action must be 'accept' or 'reject'")
        label   = "correct" if action == "accept" else "incorrect"
        updated = store.label_chain(chain_id, label)
        if not updated:
            raise HTTPException(404, f"Chain {chain_id} not found")
        return {"labeled": True, "chain_id": chain_id, "label": label}
