neuronDebugDashboardHtml top-level constant

String const neuronDebugDashboardHtml

Implementation

const neuronDebugDashboardHtml = r'''
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Neuron Debug Dashboard</title>
  <style>
    :root {
      --bg: #0f172a;
      --panel: #111827;
      --muted: #94a3b8;
      --text: #e2e8f0;
      --accent: #22d3ee;
      --accent-2: #6366f1;
      --border: #1f2937;
    }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      background: linear-gradient(135deg, #0f172a 0%, #0b1324 100%);
      color: var(--text);
      font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
      min-height: 100vh;
    }
    header {
      padding: 28px 32px 12px;
      display: flex;
      align-items: center;
      gap: 16px;
    }
    h1 { margin: 0; font-size: 24px; letter-spacing: -0.02em; }
    .pill {
      padding: 6px 12px;
      border-radius: 999px;
      background: rgba(255,255,255,0.06);
      color: var(--muted);
      font-size: 12px;
      border: 1px solid var(--border);
    }
    .grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
      gap: 16px;
      padding: 0 32px 24px;
    }
    .card {
      background: var(--panel);
      border: 1px solid var(--border);
      border-radius: 14px;
      padding: 16px;
      box-shadow: 0 10px 30px rgba(0,0,0,0.25);
    }
    .card h3 {
      margin: 0 0 8px;
      font-size: 14px;
      letter-spacing: 0.02em;
      color: var(--muted);
      text-transform: uppercase;
    }
    .metric {
      font-size: 26px;
      font-weight: 700;
    }
    .sub { color: var(--muted); font-size: 12px; }
    .list {
      margin: 0; padding: 0; list-style: none;
      display: flex; flex-direction: column; gap: 10px;
    }
    .row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 12px;
      border-radius: 10px;
      background: rgba(255,255,255,0.03);
      border: 1px solid var(--border);
    }
    .row .title { font-weight: 600; }
    .row .meta { color: var(--muted); font-size: 12px; }
    .badge {
      padding: 4px 10px;
      border-radius: 8px;
      background: rgba(34, 211, 238, 0.12);
      color: var(--accent);
      font-size: 12px;
      border: 1px solid rgba(34, 211, 238, 0.3);
    }
    .timeline {
      max-height: 320px;
      overflow-y: auto;
      display: flex;
      flex-direction: column;
      gap: 10px;
    }
    .chip {
      padding: 4px 8px;
      border-radius: 6px;
      font-size: 12px;
      border: 1px solid var(--border);
      color: var(--muted);
    }
    button {
      background: linear-gradient(120deg, var(--accent), var(--accent-2));
      color: #0f172a;
      border: none;
      padding: 10px 14px;
      border-radius: 10px;
      font-weight: 700;
      cursor: pointer;
      transition: transform 0.1s ease, box-shadow 0.1s ease;
    }
    button:hover { transform: translateY(-1px); box-shadow: 0 10px 25px rgba(34,211,238,0.25); }
    button:active { transform: translateY(0); }
    @media (max-width: 720px) {
      header { flex-direction: column; align-items: flex-start; }
    }
  </style>
</head>
<body>
  <header>
    <div>
      <h1>Neuron Debug Dashboard</h1>
      <div class="pill" id="protocol-pill">Connecting…</div>
    </div>
    <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
      <input id="search-box" type="search" placeholder="Search signals/computed" style="padding:10px;border-radius:10px;border:1px solid var(--border);background:#0b1224;color:var(--text);min-width:220px;">
      <button id="refresh-btn">Force Snapshot</button>
    </div>
  </header>

  <div class="grid" id="stats-grid">
    <div class="card">
      <h3>Signals</h3>
      <div class="metric" id="signals-count">0</div>
      <div class="sub">Tracked signals</div>
    </div>
    <div class="card">
      <h3>Computed</h3>
      <div class="metric" id="computed-count">0</div>
      <div class="sub">Derived values</div>
    </div>
    <div class="card">
      <h3>Controllers</h3>
      <div class="metric" id="controllers-count">0</div>
      <div class="sub">Active controllers</div>
    </div>
    <div class="card">
      <h3>Events</h3>
      <div class="metric" id="events-count">0</div>
      <div class="sub">Recent history</div>
    </div>
  </div>

  <div class="grid">
    <div class="card">
      <h3>Signals</h3>
      <ul class="list" id="signals-list"></ul>
    </div>
    <div class="card">
      <h3>Computed</h3>
      <ul class="list" id="computed-list"></ul>
    </div>
    <div class="card">
      <h3>Controllers</h3>
      <ul class="list" id="controllers-list"></ul>
    </div>
  </div>

  <div class="grid">
    <div class="card" style="grid-column: 1 / -1;">
      <h3>Event Stream</h3>
      <div class="timeline" id="timeline"></div>
    </div>
  </div>

  <div class="grid">
    <div class="card">
      <h3>Performance</h3>
      <div class="metric" id="fps-current">0</div>
      <div class="sub">FPS (current / avg)</div>
      <div class="chip" id="fps-avg">avg: 0</div>
    </div>
    <div class="card">
      <h3>Memory</h3>
      <div class="metric" id="mem-current">0</div>
      <div class="sub">RSS (current / avg)</div>
      <div class="chip" id="mem-avg">avg: 0</div>
    </div>
    <div class="card">
      <h3>Benchmarks</h3>
      <div class="timeline" id="benchmarks"></div>
    </div>
  </div>

  <script>
    const state = {
      signals: {},
      computed: {},
      controllers: [],
      history: [],
      perSignalHistory: {},
      protocol: 'unknown',
      metrics: {},
      watchList: new Set(),
    };

    const wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
    const wsUrl = wsProtocol + '://' + location.host;
    let socket;
    let searchTerm = '';
    let watched = new Set();

    document.getElementById('refresh-btn').addEventListener('click', () => {
      requestSnapshot();
    });

    document.getElementById('search-box').addEventListener('input', (e) => {
      searchTerm = (e.target.value || '').toLowerCase();
      render();
    });

    function connectWs() {
      socket = new WebSocket(wsUrl);
      socket.onopen = () => {
        setStatus('Connected');
      };
      socket.onmessage = (event) => {
        try {
          const msg = JSON.parse(event.data);
          handleMessage(msg);
        } catch (_) {}
      };
      socket.onclose = () => {
        setStatus('Reconnecting…');
        setTimeout(connectWs, 1000);
      };
      socket.onerror = () => {
        socket.close();
      };
    }

    function handleMessage(msg) {
      if (!msg || !msg.type) return;
      if (msg.protocol) state.protocol = msg.protocol;
      if (msg.type === 'snapshot') {
        applySnapshot(msg.data || {});
      } else if (msg.type === 'event') {
        pushEvent(msg.event);
      } else if (msg.type === 'heartbeat') {
        setStatus('Connected · heartbeat');
      }
    }

    function applySnapshot(data) {
      state.signals = data.signals || {};
      state.computed = data.computed || {};
      state.controllers = data.controllers || [];
      state.history = data.history || state.history;
      state.perSignalHistory = data.perSignalHistory || {};
      state.metrics = data.metrics || {};
      render();
      sendWatchList();
    }

    function pushEvent(ev) {
      if (!ev) return;
      state.history.push(ev);
      if (state.history.length > 500) state.history.shift();
      renderTimeline();
      document.getElementById('events-count').textContent = state.history.length;
    }

    async function bootstrap() {
      try {
        const snap = await fetch('/snapshot').then(r => r.json());
        state.protocol = snap.protocol || state.protocol;
        applySnapshot(snap.data || snap);

        const events = await fetch('/events').then(r => r.json());
        state.history = events.history || [];
        renderTimeline();
        setStatus('Ready');
      } catch (e) {
        setStatus('Snapshot failed');
      }
      connectWs();
    }

    function render() {
      document.getElementById('signals-count').textContent = Object.keys(state.signals).length;
      document.getElementById('computed-count').textContent = Object.keys(state.computed).length;
      document.getElementById('controllers-count').textContent = state.controllers.length;
      document.getElementById('events-count').textContent = state.history.length;
      renderList('signals-list', state.signals, 'signals');
      renderList('computed-list', state.computed, 'computed');
      renderControllers();
      renderTimeline();
      renderMetrics();
    }

    function renderList(id, items, type) {
      const list = document.getElementById(id);
      list.innerHTML = '';
      Object.entries(items).forEach(([key, value]) => {
        if (searchTerm && !key.toLowerCase().includes(searchTerm)) return;
        const li = document.createElement('li');
        li.className = 'row';
        const history = state.perSignalHistory[key] || [];
        const spark = buildSparkline(history.map((e) => e.value));
        li.innerHTML = `
          <div>
            <div class="title">${key}</div>
            <div class="meta">${value.type || typeof value}</div>
          </div>
          <div style="display:flex;align-items:center;gap:8px;">
            ${spark}
            <div class="badge">${formatValue(value.value)}</div>
            <input type="checkbox" data-id="${key}" ${state.watchList.has(key) ? 'checked' : ''} title="Watch">
          </div>
        `;
        list.appendChild(li);
      });

      list.querySelectorAll('input[type="checkbox"]').forEach((el) => {
        el.addEventListener('change', (e) => {
          const id = e.target.getAttribute('data-id');
          if (!id) return;
          if (e.target.checked) {
            state.watchList.add(id);
            // Request more history for watched signals
            sendSignalHistoryLimit(id, 200);
            watched.add(id);
          } else {
            state.watchList.delete(id);
            sendSignalHistoryLimit(id, 20);
            watched.delete(id);
          }
          sendWatchList();
        });
      });
    }

    function renderControllers() {
      const list = document.getElementById('controllers-list');
      list.innerHTML = '';
      state.controllers.forEach((controller) => {
        const li = document.createElement('li');
        li.className = 'row';
        li.innerHTML = `
          <div>
            <div class="title">${controller.id}</div>
            <div class="meta">Signals: ${controller.signals ?? controller.signalsCount ?? controller.signals}</div>
          </div>
          <div class="chip">${timeAgo(controller.createdAt)}</div>
        `;
        list.appendChild(li);
      });
    }

    function renderTimeline() {
      const wrap = document.getElementById('timeline');
      wrap.innerHTML = '';
      const items = [...state.history].slice(-100).reverse();
      items.forEach(ev => {
        const row = document.createElement('div');
        row.className = 'row';
        row.innerHTML = `
          <div>
            <div class="title">${ev.id || ''}</div>
            <div class="meta">${ev.type} · ${timeAgo(ev.timestamp)}</div>
          </div>
          <div class="badge">${formatValue(ev.value)}</div>
        `;
        wrap.appendChild(row);
      });
    }

    function renderMetrics() {
      const m = state.metrics || {};
      const fps = m.fps || {};
      const mem = m.memory || {};
      document.getElementById('fps-current').textContent = (fps.current ?? 0).toFixed ? fps.current.toFixed(1) : fps.current || 0;
      document.getElementById('fps-avg').textContent = `avg: ${(fps.average ?? 0).toFixed ? fps.average.toFixed(1) : fps.average || 0}`;
      document.getElementById('mem-current').textContent = mem.current || 0;
      document.getElementById('mem-avg').textContent = `avg: ${mem.average || 0}`;

      const bench = m.benchmarks || {};
      const benchWrap = document.getElementById('benchmarks');
      benchWrap.innerHTML = '';
      Object.entries(bench).forEach(([key, value]) => {
        const row = document.createElement('div');
        row.className = 'row';
        row.innerHTML = `
          <div>
            <div class="title">${key}</div>
            <div class="meta">${JSON.stringify(value)}</div>
          </div>
        `;
        benchWrap.appendChild(row);
      });
    }

    function formatValue(val) {
      if (val === null || val === undefined) return 'null';
      if (typeof val === 'object') return JSON.stringify(val);
      return String(val);
    }

    function timeAgo(ts) {
      if (!ts) return '';
      const now = Date.now();
      const diff = now - Number(ts);
      const sec = Math.floor(diff / 1000);
      if (sec < 60) return sec + 's ago';
      const min = Math.floor(sec / 60);
      if (min < 60) return min + 'm ago';
      const hr = Math.floor(min / 60);
      return hr + 'h ago';
    }

    function setStatus(text) {
      const pill = document.getElementById('protocol-pill');
      pill.textContent = `Protocol ${state.protocol} · ${text}`;
    }

    function requestSnapshot() {
      if (socket && socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify({ type: 'get_snapshot' }));
      } else {
        bootstrap();
      }
    }

    function sendSignalHistoryLimit(id, limit) {
      if (socket && socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify({ type: 'set_signal_history_limit', id, limit }));
      }
    }

    function sendWatchList() {
      if (socket && socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify({ type: 'set_watch_list', ids: Array.from(state.watchList) }));
      }
    }

    function buildSparkline(values) {
      if (!values || values.length === 0) return '<div class="chip">no data</div>';
      const nums = values.map((v) => {
        if (typeof v === 'number') return v;
        const parsed = Number(v);
        return Number.isFinite(parsed) ? parsed : 0;
      });
      const max = Math.max(...nums);
      const min = Math.min(...nums);
      const scale = max === min ? 1 : (max - min);
      const points = nums.map((v, i) => {
        const x = (i / Math.max(nums.length - 1, 1)) * 100;
        const y = 30 - ((v - min) / scale) * 30;
        return `${x},${y}`;
      }).join(' ');
      return `<svg width="100" height="30" viewBox="0 0 100 30" preserveAspectRatio="none">
        <polyline fill="none" stroke="var(--accent)" stroke-width="2" points="${points}" />
      </svg>`;
    }

    bootstrap();
  </script>
</body>
</html>
''';