// Minimal, dependency-free UI for the observability dashboard.
// Uses SSE for live log streaming; no external dependencies or build step required.
export function renderIndexHtml() {
    return `<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>VisionClaw</title>
  <link rel="icon" type="image/png" href="https://files.catbox.moe/au9xqh.png" />
  <style>
    :root {
      --bg: #0b1020;
      --panel: #0f1731;
      --border: rgba(255,255,255,0.10);
      --text: rgba(255,255,255,0.92);
      --muted: rgba(255,255,255,0.70);
      --dim: rgba(255,255,255,0.55);
      --good: #31d0aa;
      --warn: #ffcc66;
      --bad: #ff5c7a;
      --accent: #7aa2ff;
      --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
      --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
    }
    body { margin: 0; background: radial-gradient(1200px 800px at 15% 10%, rgba(122,162,255,0.18), transparent 60%), var(--bg); color: var(--text); font-family: var(--sans); }
    header { position: sticky; top: 0; backdrop-filter: blur(10px); background: rgba(22,32,55,0.55); border-bottom: 1px solid var(--border); }
    .wrap { max-width: 1200px; margin: 0 auto; padding: 6px 16px; }
    .row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
    h1 { margin: 0; font-size: 14px; letter-spacing: 0.2px; font-weight: 650; display: flex; align-items: center; gap: 6px; }
    .logo { width: 44px; height: 44px; border-radius: 8px; background: transparent; }
    .pill { border: 1px solid var(--border); background: rgba(255,255,255,0.06); color: var(--muted); padding: 6px 10px; border-radius: 999px; font-size: 12px; }
    .pill b { color: var(--text); }
    .btn { cursor: pointer; border: 1px solid var(--border); background: rgba(255,255,255,0.06); color: var(--text); padding: 8px 10px; border-radius: 10px; font-size: 12px; }
    .btn:active { transform: translateY(1px); }
    .input { border: 1px solid var(--border); background: rgba(0,0,0,0.25); color: var(--text); padding: 8px 10px; border-radius: 10px; font-size: 12px; font-family: var(--mono); min-width: 220px; }
    main .wrap { padding-top: 16px; }
    .grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
    .panel { border: 1px solid var(--border); background: rgba(15,23,49,0.65); border-radius: 14px; overflow: hidden; }
    .panel-head { padding: 10px 12px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
    .panel-head .title { font-size: 12px; color: var(--muted); }
    .panel-body { padding: 0; }
    .logs { height: calc(100vh - 140px); overflow: auto; font-family: var(--mono); font-size: 12px; line-height: 1.35; }
    .logs-raw { height: calc(100vh - 140px); overflow: auto; font-family: var(--mono); font-size: 12px; line-height: 1.45; padding: 10px 12px; white-space: pre-wrap; word-break: break-word; color: var(--text); user-select: text; display: none; }
    .line { padding: 6px 10px; border-bottom: 1px solid rgba(255,255,255,0.06); display: grid; grid-template-columns: 130px 50px 100px 1fr auto; gap: 12px; align-items: baseline; }
    .line:hover { background: rgba(255,255,255,0.04); }
    .ts { color: var(--dim); font-size: 11px; white-space: nowrap; }
    .lvl { font-weight: 650; }
    .lvl.debug { color: var(--dim); }
    .lvl.info { color: #9bb6ff; }
    .lvl.warn { color: var(--warn); }
    .lvl.error { color: var(--bad); }
    .cat { color: rgba(255,255,255,0.80); padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; background: rgba(255,255,255,0.06); }
    .cat-wake { color: #31d0aa; background: rgba(49,208,170,0.12); }
    .cat-send { color: #7aa2ff; background: rgba(122,162,255,0.12); }
    .cat-assistant { color: #d09cf0; background: rgba(208,156,240,0.12); }
    .cat-tool_call { color: #5cc9f5; background: rgba(92,201,245,0.12); }
    .cat-tool_result { color: rgba(255,255,255,0.55); background: rgba(255,255,255,0.05); }
    .cat-result { color: #f0f0f0; background: rgba(255,255,255,0.10); }
    .cat-channel { color: #ffcc66; background: rgba(255,204,102,0.12); }
    .cat-system { color: rgba(255,255,255,0.55); background: rgba(255,255,255,0.05); }
    .cat-error { color: #ff5c7a; background: rgba(255,92,122,0.12); }
    .cat-incoming { color: #66e0b8; background: rgba(102,224,184,0.12); }
    .cat-fast_response { color: #7ac8ff; background: rgba(122,200,255,0.12); }
    .badge { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--border); background: rgba(255,255,255,0.05); padding: 2px 8px; border-radius: 999px; font-size: 11px; color: var(--muted); white-space: nowrap; flex-shrink: 0; }
    .badge b { color: var(--text); font-weight: 650; }
    .badge .dim { color: var(--dim); }
    .msg-col { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
    .msg-line { display: flex; gap: 8px; align-items: baseline; min-width: 0; }
    .msg { color: var(--text); white-space: pre-wrap; word-break: break-word; min-width: 0; }
    .msg.collapsed { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .msg.tagged { background: rgba(122,162,255,0.14); padding: 4px 8px; border-radius: 6px; border-left: 3px solid var(--accent); }
    .actions { display: flex; gap: 6px; align-items: flex-start; }
    .toggle { cursor: pointer; user-select: none; border: 1px solid var(--border); background: rgba(255,255,255,0.06); color: var(--muted); padding: 2px 6px; border-radius: 8px; font-size: 11px; line-height: 1.4; flex: 0 0 auto; white-space: nowrap; }
    .toggle:hover { background: rgba(255,255,255,0.1); color: var(--text); }
    .toggle.active { background: rgba(255,255,255,0.15); color: var(--text); }
    .toggle.hidden { display: none; }
    .details { border: 1px solid var(--border); background: rgba(0,0,0,0.18); border-radius: 12px; overflow: hidden; margin-top: 4px; }
    .details-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 7px 10px; border-bottom: 1px solid rgba(255,255,255,0.08); color: var(--muted); font-size: 11px; }
    .details-head code { color: var(--text); font-family: var(--mono); font-size: 11px; }
    .details pre { margin: 0; padding: 10px 12px; white-space: pre-wrap; word-break: break-word; font-family: var(--mono); font-size: 11.5px; line-height: 1.45; color: rgba(255,255,255,0.86); }
    .meta { color: var(--dim); font-size: 11px; }
    .badged { display: inline-flex; align-items: center; gap: 8px; }
    .dot { width: 8px; height: 8px; border-radius: 999px; background: var(--bad); box-shadow: 0 0 0 3px rgba(255,92,122,0.15); }
    .dot.ok { background: var(--good); box-shadow: 0 0 0 3px rgba(49,208,170,0.18); }
    .header-bar { justify-content: space-between; }
    .pill a { color: var(--good); text-decoration: none; font-weight: 600; }
    .pill a:hover { text-decoration: underline; }
    /* Tabs */
    .tabs { display: flex; gap: 2px; }
    .tab { cursor: pointer; background: transparent; border: 1px solid transparent; color: var(--muted); padding: 5px 12px; border-radius: 8px; font-size: 12px; font-family: var(--sans); }
    .tab:hover { background: rgba(255,255,255,0.06); color: var(--text); }
    .tab.active { background: rgba(255,255,255,0.10); border-color: var(--border); color: var(--text); font-weight: 600; }
    .scroll-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-left: 5px; vertical-align: middle; transition: background 0.2s; }
    .scroll-dot.on { background: var(--good); box-shadow: 0 0 4px rgba(49,208,170,0.5); }
    .scroll-dot.off { background: var(--warn); box-shadow: 0 0 4px rgba(255,204,102,0.4); }
    .tab-content { display: none; }
    .tab-content.visible { display: block; }
    /* Resource view (memory / skills) */
    .resource-view { height: calc(100vh - 140px); overflow: auto; }
    .resource-toolbar { padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,0.06); display: flex; justify-content: space-between; align-items: center; gap: 10px; }
    .resource-toolbar-meta { color: var(--dim); font-size: 11px; }
    .resource-empty { padding: 48px 24px; text-align: center; color: var(--dim); font-size: 13px; }
    .rcard { border-bottom: 1px solid rgba(255,255,255,0.06); }
    .rcard-head { padding: 10px 12px; display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; cursor: pointer; user-select: none; }
    .rcard-head:hover { background: rgba(255,255,255,0.04); }
    .rcard-left { min-width: 0; }
    .rcard-title { font-family: var(--mono); font-size: 12px; color: var(--accent); font-weight: 600; word-break: break-all; }
    .rcard-desc { color: var(--muted); font-size: 11px; margin-top: 3px; }
    .rcard-meta { color: var(--dim); font-size: 10px; white-space: nowrap; flex-shrink: 0; text-align: right; }
    .rcard-chevron { color: var(--dim); font-size: 10px; flex-shrink: 0; transition: transform 0.15s; }
    .rcard-chevron.open { transform: rotate(90deg); }
    .rcard-body { display: none; }
    .rcard-body.open { display: block; }
    .rcard-body pre { margin: 0; padding: 10px 14px 14px; white-space: pre-wrap; word-break: break-word; font-family: var(--mono); font-size: 11.5px; line-height: 1.45; color: rgba(255,255,255,0.86); border-top: 1px solid rgba(255,255,255,0.06); }
    .todo-item { padding: 10px 14px; border-bottom: 1px solid rgba(255,255,255,0.06); display: flex; align-items: center; gap: 10px; font-size: 13px; }
    .todo-status { font-size: 11px; font-weight: 650; padding: 3px 10px; border-radius: 999px; white-space: nowrap; flex-shrink: 0; }
    .todo-status.pending { color: var(--muted); background: rgba(255,255,255,0.08); }
    .todo-status.in_progress { color: var(--accent); background: rgba(122,162,255,0.15); }
    .todo-status.completed { color: var(--good); background: rgba(49,208,170,0.15); }
    .todo-content { color: var(--text); min-width: 0; word-break: break-word; }
    @media (max-width: 700px) {
      .wrap { padding: 10px 12px; }
      .header-bar { flex-direction: column; gap: 8px; align-items: stretch; }
      .header-info { flex-wrap: nowrap; gap: 8px; }
      h1 { font-size: 13px; white-space: nowrap; }
      .pill { font-size: 11px; padding: 4px 8px; }
      .header-controls { flex-wrap: wrap; gap: 6px; }
      .input { min-width: 0; flex: 1 1 100%; box-sizing: border-box; }
      .btn { padding: 10px 12px; font-size: 12px; min-height: 40px; }
      .logs { height: calc(100vh - 210px); font-size: 11px; }
      .logs-raw { height: calc(100vh - 210px); font-size: 11px; }
      .line {
        grid-template-columns: auto auto 1fr auto;
        grid-template-rows: auto auto;
        grid-template-areas:
          "lvl cat ts actions"
          "msg msg msg msg";
        row-gap: 5px;
        column-gap: 6px;
        padding: 8px 10px;
        align-items: center;
      }
      .ts { grid-area: ts; font-size: 10px; text-align: right; }
      .lvl { grid-area: lvl; }
      .cat { grid-area: cat; font-size: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100px; }
      .msg-col { grid-area: msg; }
      .actions { grid-area: actions; flex-direction: column; gap: 3px; align-items: flex-end; }
      .toggle { padding: 3px 7px; font-size: 10px; }
      .badge { font-size: 10px; padding: 2px 6px; gap: 4px; }
      .details pre { font-size: 10.5px; padding: 8px 10px; }
      .resource-view { height: calc(100vh - 210px); }
      .rcard-title { font-size: 11px; }
    }
  </style>
</head>
<body>
<header>
  <div class="wrap">
    <div class="row header-bar">
      <div class="row header-info">
        <h1><img class="logo" src="https://files.catbox.moe/au9xqh.png" alt="VisionClaw" />VisionClaw</h1>
        <span class="pill">SSE: <span id="sse">connecting…</span></span>
        <span class="pill">Buffer: <b id="count">0</b></span>
        <span class="pill" id="tunnel-pill" style="display:none"><span id="tunnel-url-display"></span></span>
      </div>
      <div class="row header-controls">
        <input id="filter" class="input" placeholder="filter (text/category/level)" />
        <button id="expandAll" class="btn">Expand all</button>
        <button id="rawToggle" class="btn">Raw</button>
        <button id="clear" class="btn">Clear</button>
      </div>
    </div>
  </div>
</header>
<main>
  <div class="wrap">
    <div class="grid">
      <section class="panel">
        <div class="panel-head">
          <div class="tabs">
            <button class="tab active" id="tab-btn-logs">Logs<span class="scroll-dot on" id="scroll-dot" title="Auto-scroll active"></span></button>
            <button class="tab" id="tab-btn-memory">Memory</button>
            <button class="tab" id="tab-btn-skills">Skills</button>
            <button class="tab" id="tab-btn-todos">Todos</button>
            <button class="tab" id="tab-btn-config">Config</button>
          </div>
          <div class="badged">
            <div id="dot" class="dot"></div>
            <div class="meta" id="meta">/obs/events</div>
          </div>
        </div>
        <div class="panel-body">
          <div id="tab-logs" class="tab-content visible">
            <div id="logs" class="logs" aria-live="polite"></div>
            <pre id="logs-raw" class="logs-raw"></pre>
          </div>
          <div id="tab-memory" class="tab-content">
            <div class="resource-view" id="memory-view">
              <div class="resource-empty">Loading…</div>
            </div>
          </div>
          <div id="tab-skills" class="tab-content">
            <div class="resource-view" id="skills-view">
              <div class="resource-empty">Loading…</div>
            </div>
          </div>
          <div id="tab-todos" class="tab-content">
            <div class="resource-view" id="todos-view">
              <div class="resource-empty">Loading…</div>
            </div>
          </div>
          <div id="tab-config" class="tab-content">
            <div class="resource-view" id="config-view">
              <div class="resource-empty">Loading…</div>
            </div>
          </div>
        </div>
      </section>
    </div>
  </div>
</main>
<script>
  const $logs = document.getElementById('logs');
  const $logsRaw = document.getElementById('logs-raw');
  const $count = document.getElementById('count');
  const $filter = document.getElementById('filter');
  const $clear = document.getElementById('clear');
  const $expandAll = document.getElementById('expandAll');
  const $rawToggle = document.getElementById('rawToggle');
  const $sse = document.getElementById('sse');
  const $dot = document.getElementById('dot');
  const $scrollDot = document.getElementById('scroll-dot');

  const MAX = 300;
  const COLLAPSE_AT = 260;
  const NEAR_BOTTOM_PX = 150;
  let filter = '';
  let expandAll = false;
  let rawMode = false;
  let autoScroll = true;
  let programmaticScroll = false;

  function syncAutoScrollIndicator() {
    $scrollDot.classList.toggle('on', autoScroll);
    $scrollDot.classList.toggle('off', !autoScroll);
    $scrollDot.title = autoScroll ? 'Auto-scroll active' : 'Auto-scroll paused — click to resume';
  }

  $scrollDot.addEventListener('click', (e) => {
    e.stopPropagation();
    autoScroll = !autoScroll;
    syncAutoScrollIndicator();
    if (autoScroll) {
      const target = rawMode ? $logsRaw : $logs;
      programmaticScroll = true;
      target.scrollTop = target.scrollHeight;
    }
  });
  const rawLines = [];

  function isNearBottom(el) {
    if (el.scrollHeight <= el.clientHeight) return true;
    return el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM_PX;
  }

  function safeStr(v) {
    if (v === undefined || v === null) return '';
    if (typeof v === 'string') return v;
    if (typeof v === 'number' || typeof v === 'boolean') return String(v);
    try { return JSON.stringify(v); } catch { return String(v); }
  }

  function truncateBase64(obj) {
    if (obj === null || obj === undefined) return obj;
    if (typeof obj === 'string') {
      if (obj.length > 500 && /^[A-Za-z0-9+/=\\s]{200,}/.test(obj)) {
        return obj.substring(0, 80) + '…[base64 ' + formatBytes(obj.length) + ' truncated]';
      }
      if (obj.length > 2000) {
        return obj.substring(0, 500) + '…[' + formatBytes(obj.length) + ' truncated]';
      }
      return obj;
    }
    if (Array.isArray(obj)) return obj.map(truncateBase64);
    if (typeof obj === 'object') {
      const out = {};
      for (const [k, v] of Object.entries(obj)) out[k] = truncateBase64(v);
      return out;
    }
    return obj;
  }

  function formatBytes(n) {
    if (n < 1024) return n + ' B';
    if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
    return (n / 1024 / 1024).toFixed(1) + ' MB';
  }

  function stableStringify(obj) {
    if (!obj || typeof obj !== 'object') return safeStr(obj);
    try {
      const safe = truncateBase64(obj);
      const allKeys = new Set();
      JSON.stringify(safe, (k, v) => { allKeys.add(k); return v; });
      const keys = Array.from(allKeys).sort();
      return JSON.stringify(safe, keys, 2);
    } catch {
      try { return JSON.stringify(obj, null, 2); } catch { return safeStr(obj); }
    }
  }

  function formatRawLine(entry) {
    const ts = (entry.timestamp || '').replace('T', ' ').replace('Z', '');
    const tsShort = ts.length >= 19 ? ts.substring(0, 19) : ts;
    const lvl = (entry.level || 'info').toUpperCase().padEnd(5);
    const cat = (entry.category || 'system').padEnd(14);
    const msg = entry.message || '';
    const dataObj = entry.data || null;
    const dataPart = dataObj ? '  ' + JSON.stringify(truncateBase64(dataObj)) : '';
    return tsShort + '  ' + lvl + '  ' + cat + '  ' + msg + dataPart;
  }

  function syncRawView() {
    $logsRaw.textContent = rawLines.join('\\n');
    if (isNearBottom($logsRaw)) $logsRaw.scrollTop = $logsRaw.scrollHeight;
  }

  function subagentFields(entry) {
    const d = entry && entry.data ? entry.data : {};
    const subType = safeStr(d.subagent_type || '');
    const agentId = safeStr(d.subagent_agent_id || '');
    const parent = safeStr(d.parent_tool_use_id || '');
    return { subType, agentId, parent };
  }

  function match(entry) {
    if (!filter) return true;
    const f = filter.toLowerCase();
    const { subType, agentId, parent } = subagentFields(entry);
    return (
      (entry.message || '').toLowerCase().includes(f) ||
      (entry.category || '').toLowerCase().includes(f) ||
      (entry.level || '').toLowerCase().includes(f) ||
      subType.toLowerCase().includes(f) ||
      agentId.toLowerCase().includes(f) ||
      parent.toLowerCase().includes(f)
    );
  }

  function addLine(entry, forceScroll) {
    if (!match(entry)) return;

    rawLines.push(formatRawLine(entry));
    while (rawLines.length > MAX) rawLines.shift();
    if (rawMode) syncRawView();

    const line = document.createElement('div');
    line.className = 'line';
    const full = entry.message || '';

    const ts = document.createElement('div');
    ts.className = 'ts';
    const fullTs = (entry.timestamp || '').replace('T', ' ').replace('Z','');
    // Show only up to seconds (e.g. "2026-02-21 06:34:20")
    ts.textContent = fullTs.length >= 19 ? fullTs.substring(0, 19) : fullTs;

    const lvl = document.createElement('div');
    lvl.className = 'lvl ' + (entry.level || 'info');
    lvl.textContent = (entry.level || 'info').toUpperCase();

    const cat = document.createElement('div');
    const catName = entry.category || 'system';
    cat.className = 'cat cat-' + catName;
    cat.textContent = catName;

    const msgCol = document.createElement('div');
    msgCol.className = 'msg-col';

    const msgLine = document.createElement('div');
    msgLine.className = 'msg-line';

    const { subType, agentId, parent } = subagentFields(entry);
    if (subType || agentId || parent) {
      const badge = document.createElement('span');
      badge.className = 'badge';
      const label = document.createElement('span');
      label.innerHTML = '<b>' + (subType || 'subagent') + '</b>';
      badge.appendChild(label);
      if (agentId) {
        const a = document.createElement('span');
        a.className = 'dim';
        a.textContent = agentId.slice(0, 8);
        badge.appendChild(a);
      }
      if (parent) {
        const p = document.createElement('span');
        p.className = 'dim';
        p.textContent = 'parent:' + parent.slice(0, 8);
        badge.appendChild(p);
      }
      msgLine.appendChild(badge);
    }

    const msg = document.createElement('div');
    msg.className = 'msg';
    if (full.startsWith('[') && full.indexOf(']') > 0) msg.classList.add('tagged');
    msg.textContent = full;
    msgLine.appendChild(msg);

    msgCol.appendChild(msgLine);

    const details = document.createElement('div');
    details.className = 'details';
    details.style.display = 'none';
    const detailsHead = document.createElement('div');
    detailsHead.className = 'details-head';
    const left = document.createElement('div');
    left.innerHTML = 'data <code>entry.data</code>';
    const right = document.createElement('div');
    right.textContent = fullTs;
    detailsHead.appendChild(left);
    detailsHead.appendChild(right);
    const pre = document.createElement('pre');
    const dataObj = (entry && entry.data) ? entry.data : null;
    pre.textContent = dataObj ? stableStringify(dataObj) : '(no data)';
    details.appendChild(detailsHead);
    details.appendChild(pre);

    msgCol.appendChild(details);

    const actions = document.createElement('div');
    actions.className = 'actions';

    const toggle = document.createElement('button');
    toggle.className = 'toggle hidden';
    toggle.type = 'button';
    toggle.textContent = 'Expand';

    // Collapse long content by default
    const shouldCollapse = full.length > COLLAPSE_AT;
    if (shouldCollapse && !expandAll) {
      msg.classList.add('collapsed');
      toggle.classList.remove('hidden');
      toggle.textContent = 'Expand';
    } else if (shouldCollapse && expandAll) {
      toggle.classList.remove('hidden');
      toggle.textContent = 'Collapse';
    }

    toggle.addEventListener('click', () => {
      const isCollapsed = msg.classList.toggle('collapsed');
      toggle.textContent = isCollapsed ? 'Expand' : 'Collapse';
    });

    const toggleDetails = document.createElement('button');
    toggleDetails.className = 'toggle';
    toggleDetails.type = 'button';
    toggleDetails.textContent = 'Details';
    toggleDetails.addEventListener('click', () => {
      const open = details.style.display !== 'none';
      details.style.display = open ? 'none' : 'block';
      toggleDetails.classList.toggle('active', !open);
    });

    actions.appendChild(toggle);
    actions.appendChild(toggleDetails);

    line.appendChild(ts);
    line.appendChild(lvl);
    line.appendChild(cat);
    line.appendChild(msgCol);
    line.appendChild(actions);

    const shouldScroll = forceScroll || autoScroll;

    $logs.appendChild(line);

    while ($logs.childElementCount > MAX) {
      $logs.removeChild($logs.firstChild);
    }

    $count.textContent = String($logs.childElementCount);

    if (shouldScroll) {
      programmaticScroll = true;
      $logs.scrollTop = $logs.scrollHeight;
    }
  }

  let filterTimer;
  $filter.addEventListener('input', () => {
    clearTimeout(filterTimer);
    filterTimer = setTimeout(() => {
      filter = ($filter.value || '').trim();
      $logs.textContent = '';
      $count.textContent = '0';
      rawLines.length = 0;
      $logsRaw.textContent = '';
      fetch('/obs/snapshot').then(r => r.json()).then(items => {
        for (const e of items) addLine(e);
        $logs.scrollTop = $logs.scrollHeight;
        if (rawMode) syncRawView();
      }).catch(() => {});
    }, 1000);
  });

  $logs.addEventListener('scroll', () => {
    if (programmaticScroll) {
      programmaticScroll = false;
      return;
    }
    const was = autoScroll;
    autoScroll = isNearBottom($logs);
    if (was !== autoScroll) syncAutoScrollIndicator();
  });

  $expandAll.addEventListener('click', () => {
    expandAll = !expandAll;
    $expandAll.textContent = expandAll ? 'Collapse all' : 'Expand all';
    // Apply to existing rows
    document.querySelectorAll('.msg').forEach((el) => {
      const full = el.textContent || '';
      if (full.length <= COLLAPSE_AT) return;
      if (expandAll) {
        el.classList.remove('collapsed');
      } else {
        el.classList.add('collapsed');
      }
    });
    document.querySelectorAll('.toggle').forEach((btn) => {
      if (btn.classList.contains('hidden')) return;
      btn.textContent = expandAll ? 'Collapse' : 'Expand';
    });
  });

  $clear.addEventListener('click', () => {
    $logs.textContent = '';
    $count.textContent = '0';
    rawLines.length = 0;
    autoScroll = true;
    syncAutoScrollIndicator();
    $logsRaw.textContent = '';
  });

  $rawToggle.addEventListener('click', () => {
    rawMode = !rawMode;
    $rawToggle.textContent = rawMode ? 'Formatted' : 'Raw';
    $logs.style.display = rawMode ? 'none' : '';
    $logsRaw.style.display = rawMode ? 'block' : 'none';
    if (rawMode) syncRawView();
  });

  // Initial snapshot
  fetch('/obs/snapshot').then(r => r.json()).then(items => {
    for (const e of items) addLine(e);
    $logs.scrollTop = $logs.scrollHeight;
  }).catch(() => {});

  // SSE
  const es = new EventSource('/obs/events');
  es.onopen = () => {
    $sse.textContent = 'connected';
    $dot.classList.add('ok');
  };
  es.onerror = () => {
    $sse.textContent = 'disconnected';
    $dot.classList.remove('ok');
  };
  es.onmessage = (ev) => {
    try {
      const entry = JSON.parse(ev.data);
      addLine(entry);
    } catch {}
  };

  // ── Tabs ──────────────────────────────────────────────────────────────────
  const tabDefs = [
    { btn: document.getElementById('tab-btn-logs'),    pane: document.getElementById('tab-logs') },
    { btn: document.getElementById('tab-btn-memory'),  pane: document.getElementById('tab-memory') },
    { btn: document.getElementById('tab-btn-skills'),  pane: document.getElementById('tab-skills') },
    { btn: document.getElementById('tab-btn-todos'),   pane: document.getElementById('tab-todos') },
    { btn: document.getElementById('tab-btn-config'),  pane: document.getElementById('tab-config') },
  ];

  function switchTab(name) {
    tabDefs.forEach(({ btn, pane }) => {
      const active = btn.id === 'tab-btn-' + name;
      btn.classList.toggle('active', active);
      pane.classList.toggle('visible', active);
    });
    if (name === 'memory') loadMemory();
    if (name === 'skills') loadSkills();
    if (name === 'todos') loadTodos();
    if (name === 'config') loadConfig();
  }

  document.getElementById('tab-btn-logs').addEventListener('click', () => switchTab('logs'));
  document.getElementById('tab-btn-memory').addEventListener('click', () => switchTab('memory'));
  document.getElementById('tab-btn-skills').addEventListener('click', () => switchTab('skills'));
  document.getElementById('tab-btn-todos').addEventListener('click', () => switchTab('todos'));
  document.getElementById('tab-btn-config').addEventListener('click', () => switchTab('config'));

  // ── Memory ─────────────────────────────────────────────────────────────────

  function formatDate(iso) {
    return iso ? iso.replace('T', ' ').substring(0, 19) : '';
  }

  function makeRcard({ title, meta, desc, body }) {
    const card = document.createElement('div');
    card.className = 'rcard';

    const head = document.createElement('div');
    head.className = 'rcard-head';

    const left = document.createElement('div');
    left.className = 'rcard-left';
    const titleEl = document.createElement('div');
    titleEl.className = 'rcard-title';
    titleEl.textContent = title;
    left.appendChild(titleEl);
    if (desc) {
      const descEl = document.createElement('div');
      descEl.className = 'rcard-desc';
      descEl.textContent = desc;
      left.appendChild(descEl);
    }

    const right = document.createElement('div');
    right.style.display = 'flex';
    right.style.alignItems = 'center';
    right.style.gap = '8px';
    if (meta) {
      const metaEl = document.createElement('div');
      metaEl.className = 'rcard-meta';
      metaEl.textContent = meta;
      right.appendChild(metaEl);
    }
    const chevron = document.createElement('span');
    chevron.className = 'rcard-chevron';
    chevron.textContent = '▶';
    right.appendChild(chevron);

    head.appendChild(left);
    head.appendChild(right);

    const bodyEl = document.createElement('div');
    bodyEl.className = 'rcard-body';
    const pre = document.createElement('pre');
    pre.textContent = body || '(empty)';
    bodyEl.appendChild(pre);

    head.addEventListener('click', () => {
      const open = bodyEl.classList.toggle('open');
      chevron.classList.toggle('open', open);
    });

    card.appendChild(head);
    card.appendChild(bodyEl);
    return card;
  }

  function renderMemoryFiles(files) {
    const view = document.getElementById('memory-view');
    view.textContent = '';

    const toolbar = document.createElement('div');
    toolbar.className = 'resource-toolbar';
    const meta = document.createElement('span');
    meta.className = 'resource-toolbar-meta';
    meta.textContent = files.length + ' file' + (files.length !== 1 ? 's' : '') + ' in /memories';
    const btn = document.createElement('button');
    btn.className = 'btn';
    btn.textContent = 'Refresh';
    btn.addEventListener('click', () => loadMemory(true));
    toolbar.appendChild(meta);
    toolbar.appendChild(btn);
    view.appendChild(toolbar);

    if (files.length === 0) {
      const empty = document.createElement('div');
      empty.className = 'resource-empty';
      empty.textContent = 'No memory files found.';
      view.appendChild(empty);
      return;
    }

    for (const f of files) {
      view.appendChild(makeRcard({
        title: f.path,
        meta: formatBytes(f.size) + '  ' + formatDate(f.mtime),
        desc: null,
        body: f.content,
      }));
    }
  }

  let memoryLoaded = false;
  function loadMemory(force) {
    if (memoryLoaded && !force) return;
    fetch('/obs/memory').then(r => r.json()).then(files => {
      memoryLoaded = true;
      renderMemoryFiles(files);
    }).catch(() => {
      document.getElementById('memory-view').innerHTML = '<div class="resource-empty">Failed to load memory files.</div>';
    });
  }

  // ── Skills ─────────────────────────────────────────────────────────────────
  function renderSkillsList(skills) {
    const view = document.getElementById('skills-view');
    view.textContent = '';

    const toolbar = document.createElement('div');
    toolbar.className = 'resource-toolbar';
    const meta = document.createElement('span');
    meta.className = 'resource-toolbar-meta';
    meta.textContent = skills.length + ' skill' + (skills.length !== 1 ? 's' : '') + ' installed';
    const btn = document.createElement('button');
    btn.className = 'btn';
    btn.textContent = 'Refresh';
    btn.addEventListener('click', () => loadSkills(true));
    toolbar.appendChild(meta);
    toolbar.appendChild(btn);
    view.appendChild(toolbar);

    if (skills.length === 0) {
      const empty = document.createElement('div');
      empty.className = 'resource-empty';
      empty.textContent = 'No skills found.';
      view.appendChild(empty);
      return;
    }

    for (const s of skills) {
      view.appendChild(makeRcard({
        title: s.name,
        meta: null,
        desc: s.description || null,
        body: s.content,
      }));
    }
  }

  let skillsLoaded = false;
  function loadSkills(force) {
    if (skillsLoaded && !force) return;
    fetch('/obs/skills').then(r => r.json()).then(skills => {
      skillsLoaded = true;
      renderSkillsList(skills);
    }).catch(() => {
      document.getElementById('skills-view').innerHTML = '<div class="resource-empty">Failed to load skills.</div>';
    });
  }

  // ── Todos ──────────────────────────────────────────────────────────────────
  function renderTodos(todos) {
    const view = document.getElementById('todos-view');
    view.textContent = '';

    const toolbar = document.createElement('div');
    toolbar.className = 'resource-toolbar';
    const meta = document.createElement('span');
    meta.className = 'resource-toolbar-meta';
    meta.textContent = todos.length + ' item' + (todos.length !== 1 ? 's' : '');
    const btn = document.createElement('button');
    btn.className = 'btn';
    btn.textContent = 'Refresh';
    btn.addEventListener('click', () => loadTodos(true));
    toolbar.appendChild(meta);
    toolbar.appendChild(btn);
    view.appendChild(toolbar);

    if (todos.length === 0) {
      const empty = document.createElement('div');
      empty.className = 'resource-empty';
      empty.textContent = 'No active todos.';
      view.appendChild(empty);
      return;
    }

    for (const t of todos) {
      const item = document.createElement('div');
      item.className = 'todo-item';
      const status = document.createElement('span');
      status.className = 'todo-status ' + (t.status || 'pending');
      status.textContent = (t.status || 'pending').replace('_', ' ');
      const content = document.createElement('span');
      content.className = 'todo-content';
      content.textContent = t.content || '';
      item.appendChild(status);
      item.appendChild(content);
      view.appendChild(item);
    }
  }

  let todosLoaded = false;
  function loadTodos(force) {
    if (todosLoaded && !force) return;
    fetch('/obs/todos').then(r => r.json()).then(todos => {
      todosLoaded = true;
      renderTodos(todos);
    }).catch(() => {
      document.getElementById('todos-view').innerHTML = '<div class="resource-empty">Failed to load todos.</div>';
    });
  }

  // ── Config ──────────────────────────────────────────────────────────────────
  function renderConfigSection(label, obj) {
    const section = document.createElement('div');
    section.className = 'rcard';

    const head = document.createElement('div');
    head.className = 'rcard-head';
    const left = document.createElement('div');
    left.className = 'rcard-left';
    const titleEl = document.createElement('div');
    titleEl.className = 'rcard-title';
    titleEl.textContent = label;
    left.appendChild(titleEl);

    const right = document.createElement('div');
    right.style.display = 'flex';
    right.style.alignItems = 'center';
    right.style.gap = '8px';
    const chevron = document.createElement('span');
    chevron.className = 'rcard-chevron open';
    chevron.textContent = '▶';
    right.appendChild(chevron);

    head.appendChild(left);
    head.appendChild(right);

    const bodyEl = document.createElement('div');
    bodyEl.className = 'rcard-body open';

    if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
      const table = document.createElement('table');
      table.style.cssText = 'width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12px;';
      for (const [k, v] of Object.entries(obj)) {
        const tr = document.createElement('tr');
        tr.style.borderBottom = '1px solid rgba(255,255,255,0.06)';
        const tdKey = document.createElement('td');
        tdKey.style.cssText = 'padding:6px 12px;color:var(--muted);white-space:nowrap;vertical-align:top;width:200px;';
        tdKey.textContent = k;
        const tdVal = document.createElement('td');
        tdVal.style.cssText = 'padding:6px 12px;color:var(--text);word-break:break-word;';
        if (v && typeof v === 'object') {
          tdVal.textContent = JSON.stringify(v, null, 2);
          tdVal.style.whiteSpace = 'pre-wrap';
          tdVal.style.fontFamily = 'var(--mono)';
          tdVal.style.fontSize = '11px';
        } else {
          tdVal.textContent = v == null ? '(not set)' : String(v);
        }
        tr.appendChild(tdKey);
        tr.appendChild(tdVal);
        table.appendChild(tr);
      }
      bodyEl.appendChild(table);
    } else {
      const pre = document.createElement('pre');
      pre.textContent = obj != null ? JSON.stringify(obj, null, 2) : '(empty)';
      bodyEl.appendChild(pre);
    }

    head.addEventListener('click', () => {
      const open = bodyEl.classList.toggle('open');
      chevron.classList.toggle('open', open);
    });

    section.appendChild(head);
    section.appendChild(bodyEl);
    return section;
  }

  function renderConfig(data) {
    const view = document.getElementById('config-view');
    view.textContent = '';

    const toolbar = document.createElement('div');
    toolbar.className = 'resource-toolbar';
    const meta = document.createElement('span');
    meta.className = 'resource-toolbar-meta';
    meta.textContent = 'Profile: ' + (data.profile || 'default') + '  ·  v' + (data.version || '?');
    const btn = document.createElement('button');
    btn.className = 'btn';
    btn.textContent = 'Refresh';
    btn.addEventListener('click', () => loadConfig(true));
    toolbar.appendChild(meta);
    toolbar.appendChild(btn);
    view.appendChild(toolbar);

    if (data.error) {
      const empty = document.createElement('div');
      empty.className = 'resource-empty';
      empty.textContent = data.error;
      view.appendChild(empty);
      return;
    }

    const general = {
      model: data.model,
      provider: data.provider,
      gmail: data.gmail,
      anthropicApiKey: data.anthropicApiKey,
      openaiApiKey: data.openaiApiKey,
      azureOpenAIEndpoint: data.azureOpenAIEndpoint,
      azureOpenAIApiKey: data.azureOpenAIApiKey,
      azureOpenAIApiVersion: data.azureOpenAIApiVersion,
      azureOpenAIDeployment: data.azureOpenAIDeployment,
      awsRegion: data.awsRegion,
      serpApiKey: data.serpApiKey,
      skillsDir: data.skillsDir,
    };
    view.appendChild(renderConfigSection('General', general));

    if (data.owner) {
      view.appendChild(renderConfigSection('Owner', data.owner));
    }

    if (data.channels) {
      view.appendChild(renderConfigSection('Channels', data.channels));
    }

    if (data.obs) {
      view.appendChild(renderConfigSection('Observability', data.obs));
    }

    if (data.mcpServers && data.mcpServers.length > 0) {
      const mcpSection = document.createElement('div');
      mcpSection.className = 'rcard';

      const mcpHead = document.createElement('div');
      mcpHead.className = 'rcard-head';
      const mcpLeft = document.createElement('div');
      mcpLeft.className = 'rcard-left';
      const mcpTitle = document.createElement('div');
      mcpTitle.className = 'rcard-title';
      mcpTitle.textContent = 'MCP Servers';
      mcpLeft.appendChild(mcpTitle);

      const mcpRight = document.createElement('div');
      mcpRight.style.display = 'flex';
      mcpRight.style.alignItems = 'center';
      mcpRight.style.gap = '8px';
      const mcpChevron = document.createElement('span');
      mcpChevron.className = 'rcard-chevron open';
      mcpChevron.textContent = '▶';
      mcpRight.appendChild(mcpChevron);

      mcpHead.appendChild(mcpLeft);
      mcpHead.appendChild(mcpRight);

      const mcpBody = document.createElement('div');
      mcpBody.className = 'rcard-body open';
      const mcpTable = document.createElement('table');
      mcpTable.style.cssText = 'width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12px;';

      for (const srv of data.mcpServers) {
        const tr = document.createElement('tr');
        tr.style.borderBottom = '1px solid rgba(255,255,255,0.06)';
        const tdName = document.createElement('td');
        tdName.style.cssText = 'padding:6px 12px;color:var(--text);white-space:nowrap;width:200px;';
        tdName.textContent = srv.name;
        const tdStatus = document.createElement('td');
        tdStatus.style.cssText = 'padding:6px 12px;display:flex;align-items:center;gap:6px;';
        const dot = document.createElement('span');
        const ok = srv.status === 'connected';
        dot.style.cssText = 'display:inline-block;width:8px;height:8px;border-radius:50%;' +
          (ok ? 'background:var(--good);box-shadow:0 0 4px rgba(49,208,170,0.4);' : 'background:var(--bad);box-shadow:0 0 4px rgba(255,92,122,0.4);');
        const label = document.createElement('span');
        label.style.color = ok ? 'var(--good)' : 'var(--bad)';
        label.textContent = srv.status;
        tdStatus.appendChild(dot);
        tdStatus.appendChild(label);
        tr.appendChild(tdName);
        tr.appendChild(tdStatus);
        mcpTable.appendChild(tr);
      }

      mcpBody.appendChild(mcpTable);

      mcpHead.addEventListener('click', () => {
        const open = mcpBody.classList.toggle('open');
        mcpChevron.classList.toggle('open', open);
      });

      mcpSection.appendChild(mcpHead);
      mcpSection.appendChild(mcpBody);
      view.appendChild(mcpSection);
    }

    if (data.generalUsage) {
      view.appendChild(renderConfigSection('Session Usage (General)', data.generalUsage));
    }

    if (data.codingUsage) {
      view.appendChild(renderConfigSection('Session Usage (Coding)', data.codingUsage));
    }
  }

  let configLoaded = false;
  function loadConfig(force) {
    if (configLoaded && !force) return;
    fetch('/obs/config').then(r => r.json()).then(data => {
      configLoaded = true;
      renderConfig(data);
    }).catch(() => {
      document.getElementById('config-view').innerHTML = '<div class="resource-empty">Failed to load config.</div>';
    });
  }

  // ── Tunnel URL ──────────────────────────────────────────────────────────────
  (function pollTunnelUrl() {
    const $pill = document.getElementById('tunnel-pill');
    const $display = document.getElementById('tunnel-url-display');
    let attempts = 0;
    const maxAttempts = 20; // poll for up to ~60s (20 × 3s)

    function poll() {
      fetch('/obs/tunnel-url')
        .then(r => r.json())
        .then(data => {
          const url = data && data.url;
          if (url) {
            const a = document.createElement('a');
            a.href = url;
            a.target = '_blank';
            a.rel = 'noopener noreferrer';
            a.textContent = 'Public';
            a.title = url;
            $display.textContent = '';
            $display.appendChild(a);
            $pill.style.display = '';
          } else if (++attempts < maxAttempts) {
            setTimeout(poll, 3000);
          }
        })
        .catch(() => {
          if (++attempts < maxAttempts) setTimeout(poll, 3000);
        });
    }

    poll();
  })();
</script>
</body>
</html>`;
}
//# sourceMappingURL=ui.js.map