/**
 * admin-relay-panel.ts — Serves the relay admin dashboard HTML at /admin/relay.
 *
 * This page is registered directly in app.ts at /admin/relay (NOT under /api)
 * so the route is accessible at a clean URL.
 *
 * Auth gate: on first visit the user is prompted for ADMIN_TOKEN, stored in
 * localStorage, sent as Bearer Authorization header on every /api/admin call.
 *
 * Tabs:
 *   Queue    — Pending events list with content preview, Approve/Reject; 15s auto-refresh
 *   Accounts — Whitelist table with Revoke; pubkey grant form
 *
 * Stats bar: pending, approved today, total accounts, live relay connections.
 */

import { Router, type Request, type Response } from "express";

const router = Router();

// Gate: in production the panel returns 403 when ADMIN_TOKEN is not configured.
// In development (no token set), the auth gate still renders and the API uses
// localhost-only fallback, so ops can still access the panel locally.
const ADMIN_TOKEN = process.env["ADMIN_TOKEN"] ?? process.env["ADMIN_SECRET"] ?? "";
const IS_PROD = process.env["NODE_ENV"] === "production";

router.get("/admin/relay", (_req: Request, res: Response) => {
  if (IS_PROD && !ADMIN_TOKEN) {
    res.status(403).send("Relay admin panel disabled: ADMIN_TOKEN is not configured.");
    return;
  }
  res.setHeader("Content-Type", "text/html");
  res.send(ADMIN_PANEL_HTML);
});

export default router;

// ─────────────────────────────────────────────────────────────────────────────
// Inline HTML — no build step required, served directly by Express.
// ─────────────────────────────────────────────────────────────────────────────

const ADMIN_PANEL_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Relay Admin — Timmy Tower World</title>
<style>
  :root {
    --bg: #0a0a0f;
    --surface: #13131a;
    --surface2: #1a1a24;
    --border: #2a2a3a;
    --accent: #f7931a;
    --accent2: #7b61ff;
    --text: #e8e8f0;
    --muted: #6b6b80;
    --green: #00d4aa;
    --red: #ff4d6d;
    --yellow: #f7c948;
    --blue: #4da8ff;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    background: var(--bg);
    color: var(--text);
    font-family: 'SF Mono', 'Fira Code', monospace;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 32px 20px 60px;
  }

  header {
    width: 100%;
    max-width: 960px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 28px;
  }
  .header-title { display: flex; align-items: center; gap: 14px; }
  .header-title h1 {
    font-family: system-ui, sans-serif;
    font-size: 1.4rem;
    font-weight: 700;
    letter-spacing: -0.5px;
  }
  .header-title h1 span { color: var(--accent); }
  .badge {
    display: inline-block;
    background: #1a1a2e;
    border: 1px solid var(--accent2);
    color: var(--accent2);
    font-size: 0.65rem;
    padding: 2px 7px;
    border-radius: 4px;
    letter-spacing: 1px;
    vertical-align: middle;
  }
  .header-links { display: flex; gap: 14px; align-items: center; }
  .header-links a {
    color: var(--muted);
    font-size: 0.78rem;
    text-decoration: none;
    font-family: system-ui, sans-serif;
    transition: color 0.2s;
  }
  .header-links a:hover { color: var(--accent); }
  #logout-btn {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--muted);
    border-radius: 6px;
    padding: 5px 12px;
    font-size: 0.72rem;
    cursor: pointer;
    font-family: system-ui, sans-serif;
    transition: border-color 0.2s, color 0.2s;
  }
  #logout-btn:hover { border-color: var(--red); color: var(--red); }

  /* ── Auth gate ── */
  #auth-gate {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 36px;
    width: 100%;
    max-width: 440px;
    text-align: center;
    margin-top: 80px;
  }
  #auth-gate h2 {
    font-family: system-ui, sans-serif;
    font-size: 1.1rem;
    font-weight: 700;
    margin-bottom: 10px;
  }
  #auth-gate p {
    color: var(--muted);
    font-family: system-ui, sans-serif;
    font-size: 0.88rem;
    margin-bottom: 20px;
    line-height: 1.5;
  }
  #auth-gate input {
    width: 100%;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font-family: monospace;
    font-size: 0.95rem;
    padding: 12px 14px;
    outline: none;
    transition: border-color 0.2s;
    margin-bottom: 14px;
  }
  #auth-gate input:focus { border-color: var(--accent2); }
  #auth-gate .auth-btn {
    width: 100%;
    background: var(--accent);
    color: #000;
    border: none;
    border-radius: 8px;
    padding: 12px;
    font-family: system-ui, sans-serif;
    font-size: 0.95rem;
    font-weight: 700;
    cursor: pointer;
    transition: opacity 0.2s;
  }
  #auth-gate .auth-btn:hover { opacity: 0.88; }
  #auth-error {
    color: var(--red);
    font-family: system-ui, sans-serif;
    font-size: 0.82rem;
    margin-top: 10px;
    display: none;
  }

  /* ── Main content ── */
  #main { display: none; width: 100%; max-width: 960px; }

  /* ── Stats bar ── */
  .stats-bar {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 12px;
    margin-bottom: 24px;
  }
  .stat-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 18px 16px;
    text-align: center;
  }
  .stat-card .stat-value {
    font-size: 2rem;
    font-weight: 700;
    font-family: system-ui, sans-serif;
    color: var(--text);
    line-height: 1;
    margin-bottom: 6px;
  }
  .stat-card .stat-label {
    font-size: 0.7rem;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 1px;
    font-family: system-ui, sans-serif;
  }
  .stat-card.yellow .stat-value { color: var(--yellow); }
  .stat-card.green  .stat-value { color: var(--green); }
  .stat-card.purple .stat-value { color: var(--accent2); }
  .stat-card.blue   .stat-value { color: var(--blue); }

  /* ── Tabs ── */
  .tab-bar {
    display: flex;
    border-bottom: 1px solid var(--border);
    margin-bottom: 20px;
  }
  .tab-btn {
    background: transparent;
    border: none;
    border-bottom: 2px solid transparent;
    color: var(--muted);
    font-family: system-ui, sans-serif;
    font-size: 0.88rem;
    font-weight: 600;
    padding: 10px 22px;
    cursor: pointer;
    transition: color 0.2s, border-color 0.2s;
    margin-bottom: -1px;
  }
  .tab-btn:hover { color: var(--text); }
  .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }

  .tab-pane { display: none; }
  .tab-pane.active { display: block; }

  /* ── Table ── */
  .table-wrap {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
  }
  .table-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 20px;
    border-bottom: 1px solid var(--border);
  }
  .table-header h3 {
    font-family: system-ui, sans-serif;
    font-size: 0.88rem;
    font-weight: 700;
    color: var(--muted);
    text-transform: uppercase;
    letter-spacing: 1px;
  }
  .table-header .hint { font-size: 0.7rem; color: var(--muted); font-family: system-ui, sans-serif; }
  table { width: 100%; border-collapse: collapse; font-family: system-ui, sans-serif; font-size: 0.82rem; }
  th {
    text-align: left;
    padding: 10px 16px;
    background: var(--surface2);
    color: var(--muted);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    font-weight: 600;
    border-bottom: 1px solid var(--border);
  }
  td {
    padding: 12px 16px;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
    color: var(--text);
    line-height: 1.4;
  }
  tr:last-child td { border-bottom: none; }
  tr:hover td { background: rgba(255,255,255,0.02); }

  .mono { font-family: monospace; font-size: 0.78rem; color: var(--accent2); }
  .preview { color: var(--muted); max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .ts { color: var(--muted); font-size: 0.75rem; }

  /* ── Status pill ── */
  .pill {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 20px;
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.3px;
    text-transform: uppercase;
  }
  .pill-pending      { background: #2a2010; color: var(--yellow);  border: 1px solid #443010; }
  .pill-approved     { background: #0d2820; color: var(--green);   border: 1px solid #0f4030; }
  .pill-auto_approved{ background: #0d2820; color: var(--green);   border: 1px solid #0f4030; }
  .pill-rejected     { background: #2a0f18; color: var(--red);     border: 1px solid #441020; }
  .pill-read         { background: #1a1a2e; color: var(--accent2); border: 1px solid #2a1a4e; }
  .pill-write        { background: #1a2a10; color: var(--green);   border: 1px solid #1a4010; }
  .pill-elite        { background: #2a1a00; color: var(--accent);  border: 1px solid #4a2a00; }
  .pill-none         { background: #1a1a1a; color: var(--muted);   border: 1px solid #2a2a2a; }

  /* ── Buttons ── */
  .btn {
    border: none;
    border-radius: 6px;
    padding: 5px 12px;
    font-size: 0.75rem;
    font-weight: 700;
    cursor: pointer;
    font-family: system-ui, sans-serif;
    transition: opacity 0.15s, transform 0.1s;
  }
  .btn:hover { opacity: 0.85; transform: translateY(-1px); }
  .btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; }
  .btn-approve { background: var(--green); color: #000; }
  .btn-reject  { background: var(--red);   color: #fff; margin-left: 6px; }
  .btn-revoke  { background: transparent; border: 1px solid var(--red); color: var(--red); }

  /* ── Grant form ── */
  .grant-form {
    background: var(--surface2);
    border-top: 1px solid var(--border);
    padding: 18px 20px;
    display: flex;
    align-items: flex-end;
    gap: 10px;
    flex-wrap: wrap;
  }
  .grant-form label {
    font-family: system-ui, sans-serif;
    font-size: 0.75rem;
    color: var(--muted);
    display: flex;
    flex-direction: column;
    gap: 5px;
    flex: 1 1 280px;
  }
  .grant-form input, .grant-form select {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text);
    font-family: monospace;
    font-size: 0.85rem;
    padding: 8px 10px;
    outline: none;
    transition: border-color 0.2s;
  }
  .grant-form input:focus, .grant-form select:focus { border-color: var(--accent2); }
  .grant-form select { font-family: system-ui, sans-serif; }
  .btn-grant { background: var(--accent); color: #000; padding: 8px 18px; flex-shrink: 0; }

  .empty-state { padding: 36px; text-align: center; color: var(--muted); font-family: system-ui, sans-serif; font-size: 0.88rem; }

  /* ── Toast ── */
  #toast {
    position: fixed;
    bottom: 28px;
    right: 28px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 12px 18px;
    font-family: system-ui, sans-serif;
    font-size: 0.85rem;
    color: var(--text);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s;
    z-index: 99;
    max-width: 320px;
  }
  #toast.show { opacity: 1; }
  #toast.ok  { border-color: var(--green); color: var(--green); }
  #toast.err { border-color: var(--red);   color: var(--red); }
</style>
</head>
<body>

<!-- ── Auth gate ─────────────────────────────────────────────────────────── -->
<div id="auth-gate">
  <h2>&#x1F512; Admin Access</h2>
  <p>Enter the <strong>ADMIN_TOKEN</strong> to access the relay dashboard. It will be remembered in this browser.</p>
  <input type="password" id="token-input" placeholder="Admin token…" autocomplete="current-password"/>
  <button class="auth-btn" onclick="submitToken()">Unlock dashboard</button>
  <div id="auth-error">Incorrect token — please try again.</div>
</div>

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

  <header>
    <div class="header-title">
      <h1>&#x26A1; Timmy <span>Relay</span> Admin <span class="badge">OPERATOR</span></h1>
    </div>
    <div class="header-links">
      <a href="/api/ui">&#8592; Timmy UI</a>
      <a href="/tower">Workshop</a>
      <button id="logout-btn" onclick="logout()">Log out</button>
    </div>
  </header>

  <!-- Stats bar: 4 metric cards -->
  <div class="stats-bar">
    <div class="stat-card yellow">
      <div class="stat-value" id="stat-pending">—</div>
      <div class="stat-label">Pending review</div>
    </div>
    <div class="stat-card green">
      <div class="stat-value" id="stat-approved-today">—</div>
      <div class="stat-label">Approved today</div>
    </div>
    <div class="stat-card purple">
      <div class="stat-value" id="stat-accounts">—</div>
      <div class="stat-label">Accounts</div>
    </div>
    <div class="stat-card blue">
      <div class="stat-value" id="stat-connections">—</div>
      <div class="stat-label">Relay connections</div>
    </div>
  </div>

  <!-- Tabs -->
  <div class="tab-bar">
    <button class="tab-btn active" onclick="switchTab('queue',this)">Moderation Queue</button>
    <button class="tab-btn" onclick="switchTab('accounts',this)">Accounts</button>
  </div>

  <!-- Queue tab -->
  <div id="tab-queue" class="tab-pane active">
    <div class="table-wrap">
      <div class="table-header">
        <h3>Pending events</h3>
        <span class="hint">auto-refreshes every 15s</span>
      </div>
      <div id="queue-body"></div>
    </div>
  </div>

  <!-- Accounts tab -->
  <div id="tab-accounts" class="tab-pane">
    <div class="table-wrap">
      <div class="table-header">
        <h3>Relay accounts</h3>
        <span class="hint">whitelist</span>
      </div>
      <div id="accounts-body"></div>
      <div class="grant-form">
        <label>
          Pubkey (64-char hex)
          <input id="grant-pubkey" type="text" placeholder="a1b2c3…" maxlength="64" spellcheck="false"/>
        </label>
        <label style="flex:0 1 auto;">
          Access
          <select id="grant-level">
            <option value="write">write</option>
            <option value="read">read</option>
          </select>
        </label>
        <label style="flex:0 1 160px;">
          Notes
          <input id="grant-notes" type="text" placeholder="optional" style="font-family:system-ui,sans-serif;"/>
        </label>
        <button class="btn btn-grant btn" onclick="grantAccount()">Grant access</button>
      </div>
    </div>
  </div>

</div>

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

<script>
const BASE = window.location.origin;
const LS_KEY = 'relay_admin_token';
let adminToken = '';
let refreshTimer = null;

// ── Auth ─────────────────────────────────────────────────────────────────────

async function verifyToken(token) {
  const r = await fetch(BASE + '/api/admin/relay/stats', {
    headers: { Authorization: 'Bearer ' + token }
  }).catch(() => null);
  if (!r || r.status === 401) return null;
  return r;
}

async function submitToken() {
  const val = document.getElementById('token-input').value.trim();
  if (!val) return;
  const r = await verifyToken(val);
  if (!r) {
    document.getElementById('auth-error').style.display = 'block';
    return;
  }
  adminToken = val;
  localStorage.setItem(LS_KEY, val);
  showMain(await r.json());
}

document.getElementById('token-input').addEventListener('keydown', e => {
  if (e.key === 'Enter') submitToken();
});

function logout() {
  localStorage.removeItem(LS_KEY);
  adminToken = '';
  clearInterval(refreshTimer);
  document.getElementById('main').style.display = 'none';
  document.getElementById('auth-gate').style.display = 'block';
  document.getElementById('token-input').value = '';
}

async function showMain(initialStats) {
  document.getElementById('auth-gate').style.display = 'none';
  document.getElementById('main').style.display = 'block';
  if (initialStats) renderStats(initialStats);
  await Promise.all([loadStats(), loadQueue()]);
  refreshTimer = setInterval(async () => {
    await Promise.all([loadStats(), loadQueue()]);
  }, 15000);
}

// ── API helpers ──────────────────────────────────────────────────────────────

// api() wraps fetch with Bearer auth. On 401, clears stored token and forces
// back to auth gate so the user is never stuck in a degraded "logged-in" state
// with a stale/expired token.
async function api(path, opts) {
  opts = opts || {};
  const headers = Object.assign({ Authorization: 'Bearer ' + adminToken, 'Content-Type': 'application/json' }, opts.headers || {});
  const r = await fetch(BASE + '/api' + path, Object.assign({}, opts, { headers }));
  if (r.status === 401) {
    localStorage.removeItem(LS_KEY);
    adminToken = '';
    clearInterval(refreshTimer);
    document.getElementById('main').style.display = 'none';
    document.getElementById('auth-gate').style.display = 'block';
    document.getElementById('auth-error').style.display = 'block';
    document.getElementById('auth-error').textContent = 'Session expired or token changed — please re-authenticate.';
  }
  return r;
}

// ── Stats ────────────────────────────────────────────────────────────────────

async function loadStats() {
  try {
    const r = await api('/admin/relay/stats');
    if (!r.ok) return;
    renderStats(await r.json());
  } catch(e) { /* ignore */ }
}

function renderStats(d) {
  document.getElementById('stat-pending').textContent = d.pending != null ? d.pending : '—';
  document.getElementById('stat-approved-today').textContent = d.approvedToday != null ? d.approvedToday : '—';
  document.getElementById('stat-accounts').textContent = d.totalAccounts != null ? d.totalAccounts : '—';
  document.getElementById('stat-connections').textContent = d.liveConnections != null ? d.liveConnections : 'n/a';
}

// ── Queue tab ────────────────────────────────────────────────────────────────

async function loadQueue() {
  const body = document.getElementById('queue-body');
  try {
    const r = await api('/admin/relay/queue?status=pending');
    if (!r.ok) { body.innerHTML = '<div class="empty-state">Failed to load queue.</div>'; return; }
    const d = await r.json();
    if (!d.events || !d.events.length) {
      body.innerHTML = '<div class="empty-state">No pending events.</div>';
      return;
    }
    body.innerHTML = '<table><thead><tr>' +
      '<th>Event ID</th><th>Pubkey</th><th>Kind</th><th>Content preview</th><th>Status</th><th>Queued</th><th>Actions</th>' +
      '</tr></thead><tbody>' + d.events.map(renderQueueRow).join('') + '</tbody></table>';
  } catch(e) {
    body.innerHTML = '<div class="empty-state">Network error.</div>';
  }
}

// esc() — HTML-escape a value before injection into innerHTML.
// Prevents XSS from any user-controlled field (contentPreview, notes, pubkey, etc.).
function esc(v) {
  return String(v == null ? '' : v)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

function renderQueueRow(ev) {
  // eventId and pubkey are server-validated hex strings, but still escape defensively.
  var id8    = ev.eventId ? esc(ev.eventId.slice(0,8)) + '…' : '—';
  var pk12   = ev.pubkey  ? esc(ev.pubkey.slice(0,12)) + '…' : '—';
  var preview = esc(ev.contentPreview || '(empty)');
  var ts     = ev.createdAt ? esc(new Date(ev.createdAt).toLocaleString()) : '—';
  var status = esc(ev.status || 'none');
  // onclick uses hex event IDs validated server-side — still sanitise to be safe.
  var safeId = (ev.eventId || '').replace(/[^0-9a-f]/gi, '');
  var pill    = '<span class="pill pill-' + status + '">' + status + '</span>';
  var approve = '<button class="btn btn-approve" onclick="queueApprove(\'' + safeId + '\',this)">Approve</button>';
  var reject  = '<button class="btn btn-reject"  onclick="queueReject(\''  + safeId + '\',this)">Reject</button>';
  var kind    = ev.kind != null ? esc(String(ev.kind)) : '—';
  return '<tr>' +
    '<td class="mono">' + id8 + '</td>' +
    '<td class="mono">' + pk12 + '</td>' +
    '<td>' + kind + '</td>' +
    '<td class="preview" title="' + preview + '">' + preview + '</td>' +
    '<td>' + pill + '</td>' +
    '<td class="ts">' + ts + '</td>' +
    '<td>' + approve + reject + '</td>' +
    '</tr>';
}

async function queueApprove(eventId, btn) {
  btn.disabled = true;
  try {
    const r = await api('/admin/relay/queue/' + eventId + '/approve', { method: 'POST', body: JSON.stringify({ reason: 'admin approval' }) });
    if (r.ok) { toast('Event approved', 'ok'); loadQueue(); loadStats(); }
    else { const d = await r.json(); toast(d.error || 'Approve failed', 'err'); btn.disabled = false; }
  } catch(e) { toast('Network error', 'err'); btn.disabled = false; }
}

async function queueReject(eventId, btn) {
  btn.disabled = true;
  try {
    const r = await api('/admin/relay/queue/' + eventId + '/reject', { method: 'POST', body: JSON.stringify({ reason: 'admin rejection' }) });
    if (r.ok) { toast('Event rejected', 'ok'); loadQueue(); loadStats(); }
    else { const d = await r.json(); toast(d.error || 'Reject failed', 'err'); btn.disabled = false; }
  } catch(e) { toast('Network error', 'err'); btn.disabled = false; }
}

// ── Accounts tab ─────────────────────────────────────────────────────────────

async function loadAccounts() {
  const body = document.getElementById('accounts-body');
  try {
    const r = await api('/admin/relay/accounts');
    if (!r.ok) { body.innerHTML = '<div class="empty-state">Failed to load accounts.</div>'; return; }
    const d = await r.json();
    if (!d.accounts || !d.accounts.length) {
      body.innerHTML = '<div class="empty-state">No accounts whitelisted.</div>';
      return;
    }
    body.innerHTML = '<table><thead><tr>' +
      '<th>Pubkey</th><th>Access</th><th>Trust tier</th><th>Granted by</th><th>Notes</th><th>Date</th><th>Action</th>' +
      '</tr></thead><tbody>' + d.accounts.map(renderAccountRow).join('') + '</tbody></table>';
  } catch(e) {
    body.innerHTML = '<div class="empty-state">Network error.</div>';
  }
}

function renderAccountRow(ac) {
  var pk    = ac.pubkey || '';
  var pk12  = esc(pk.slice(0,12)) + (pk.length > 12 ? '…' : '');
  var ts    = ac.grantedAt ? esc(new Date(ac.grantedAt).toLocaleDateString()) : '—';
  // level and tier come from enum/trusted fields, but escape anyway.
  var level      = esc(ac.accessLevel || 'none');
  var tier       = esc(ac.trustTier || '—');
  var grantedBy  = esc(ac.grantedBy || '—');
  var rawNotes   = (ac.notes || '').slice(0,30) + ((ac.notes||'').length > 30 ? '…' : '');
  var notes      = esc(rawNotes);
  var isRevoked  = ac.revokedAt != null;
  // pubkey is server-validated hex; strip non-hex chars for onclick safety.
  var safePk = pk.replace(/[^0-9a-f]/gi, '');
  var action = isRevoked
    ? '<span style="color:var(--muted);font-size:0.75rem;">revoked</span>'
    : '<button class="btn btn-revoke" onclick="revokeAccount(\'' + safePk + '\',this)">Revoke</button>';
  return '<tr>' +
    '<td class="mono" title="' + esc(pk) + '">' + pk12 + '</td>' +
    '<td><span class="pill pill-' + level + '">' + level + '</span></td>' +
    '<td>' + tier + '</td>' +
    '<td style="color:var(--muted);">' + grantedBy + '</td>' +
    '<td style="color:var(--muted);font-size:0.78rem;">' + notes + '</td>' +
    '<td class="ts">' + ts + '</td>' +
    '<td>' + action + '</td>' +
    '</tr>';
}

async function revokeAccount(pubkey, btn) {
  if (!confirm('Revoke access for ' + pubkey.slice(0,12) + '…?')) return;
  btn.disabled = true;
  try {
    const r = await api('/admin/relay/accounts/' + pubkey + '/revoke', { method: 'POST', body: JSON.stringify({ reason: 'admin revoke' }) });
    if (r.ok) { toast('Access revoked', 'ok'); loadAccounts(); loadStats(); }
    else { const d = await r.json(); toast(d.error || 'Revoke failed', 'err'); btn.disabled = false; }
  } catch(e) { toast('Network error', 'err'); btn.disabled = false; }
}

async function grantAccount() {
  var pubkey = document.getElementById('grant-pubkey').value.trim();
  var level  = document.getElementById('grant-level').value;
  var notes  = document.getElementById('grant-notes').value.trim();
  if (!/^[0-9a-f]{64}$/.test(pubkey)) { toast('Pubkey must be 64 lowercase hex chars', 'err'); return; }
  try {
    const r = await api('/admin/relay/accounts/' + pubkey + '/grant', {
      method: 'POST',
      body: JSON.stringify({ level, notes: notes || undefined })
    });
    if (r.ok) {
      toast('Access granted: ' + pubkey.slice(0,8) + '…', 'ok');
      document.getElementById('grant-pubkey').value = '';
      document.getElementById('grant-notes').value = '';
      loadAccounts(); loadStats();
    } else { const d = await r.json(); toast(d.error || 'Grant failed', 'err'); }
  } catch(e) { toast('Network error', 'err'); }
}

// ── Tabs ─────────────────────────────────────────────────────────────────────

function switchTab(name, btn) {
  document.querySelectorAll('.tab-pane').forEach(function(p){ p.classList.remove('active'); });
  document.querySelectorAll('.tab-btn').forEach(function(b){ b.classList.remove('active'); });
  document.getElementById('tab-' + name).classList.add('active');
  btn.classList.add('active');
  if (name === 'accounts') loadAccounts();
  if (name === 'queue') loadQueue();
}

// ── Toast ─────────────────────────────────────────────────────────────────────

var toastTimer = null;
function toast(msg, type) {
  var el = document.getElementById('toast');
  el.textContent = msg;
  el.className = 'show ' + (type || 'ok');
  clearTimeout(toastTimer);
  toastTimer = setTimeout(function(){ el.className = ''; }, 3000);
}

// ── Boot ─────────────────────────────────────────────────────────────────────
// Validate any saved token before showing the main panel to avoid a degraded
// "logged-in" state if the token was rotated or never valid.

(async function boot() {
  var saved = localStorage.getItem(LS_KEY);
  if (!saved) return;
  var r = await verifyToken(saved);
  if (!r) {
    localStorage.removeItem(LS_KEY);
    return;
  }
  adminToken = saved;
  var stats = null;
  try { stats = await r.json(); } catch(e) {}
  showMain(stats);
})();
</script>
</body>
</html>`;
