<?php require_once dirname(__DIR__) . '/include/monitor.php'; ?>

<div id="vv-api-banner" style="display:none;background:#1a1200;border:1px solid #3a2800;border-radius:4px;
  padding:5px 10px;margin-bottom:8px;font-size:11px;color:#ff9800;">
  ⚠ Unraid API unavailable — using local reads. Check API key in host conf.
  <span id="vv-api-fallback-list" style="color:#666;margin-left:6px;"></span>
</div>

<div id="vv-monitor" style="display:grid;grid-template-columns:repeat(8,1fr);gap:12px;width:100%;box-sizing:border-box;">

    <!-- Row 1: System | Power | CPU | Memory | Network -->
    <div class="vv-card" id="vv-system" style="grid-column:span 1;">
      <div id="vv-system-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-ups-card" style="grid-column:span 1;">
      <h3>
        <span style="display:flex;align-items:center;gap:6px;">
          <svg width="9" height="15" viewBox="0 0 11 18" fill="none" style="opacity:0.4;flex-shrink:0;margin-bottom:1px;">
            <polygon points="9,0 2,10 6,10 3,18 11,6 6,6 8,0" fill="#ff9800"/>
          </svg>Power
        </span>
        <a href="/Settings/UPS" target="_blank" class="vv-card-cog" title="UPS Settings">⚙</a>
      </h3>
      <div id="vv-ups-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-cpu" style="grid-column:span 2;">
      <h3><span id="vv-cpu-title">CPU</span><a href="/Settings/CPUset" target="_blank" class="vv-card-cog" title="CPU Settings">⚙</a></h3>
      <div id="vv-cpu-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-memory" style="grid-column:span 2;">
      <h3>Memory<a href="/" target="_blank" class="vv-card-cog" title="Dashboard">⚙</a></h3>
      <div id="vv-memory-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-network" style="grid-column:span 2;">
      <h3>Network<a href="/Settings/NetworkSettings" target="_blank" class="vv-card-cog" title="Network Settings">⚙</a></h3>
      <div id="vv-network-body">Loading...</div>
    </div>

    <!-- Row 2: Scripts | Fallback | Partner | Containers & VMs -->
    <div class="vv-card" id="vv-scripts-card" style="grid-column:span 1;">
      <h3>Scripts<a href="/Apps/plugin_userscripts" target="_blank" class="vv-card-cog" title="User Scripts">⚙</a></h3>
      <div id="vv-scripts-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-fallback" style="grid-column:span 1;">
      <h3>Fallback</h3>
      <div id="vv-fallback-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-partner" style="grid-column:span 2;">
      <h3>Partner</h3>
      <div id="vv-partner-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-docker-folders" style="grid-column:span 4;">
      <h3>
        <span>Containers &amp; VMs <span id="vv-docker-count" style="font-size:10px;color:#4a4a4a;font-weight:400;text-transform:none;letter-spacing:0;margin-left:2px;"></span></span>
        <a href="/Docker" target="_blank" class="vv-card-cog" title="Docker">⚙</a>
      </h3>
      <div id="vv-docker-folders-body">Loading...</div>
    </div>

    <!-- Row 3: GPU | Transcode | Streams -->
    <div class="vv-card" id="vv-gpu-card" style="grid-column:span 2;">
      <h3>GPU<a href="/" target="_blank" class="vv-card-cog" title="Dashboard">⚙</a></h3>
      <div id="vv-gpu-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-transcode" style="grid-column:span 2;">
      <h3>Transcode</h3>
      <div id="vv-transcode-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-streams" style="grid-column:span 4;">
      <h3>Streams</h3>
      <div id="vv-streams-body">Loading...</div>
    </div>

    <!-- Row 4: Watchdog | Parity | Pools | Array -->
    <div class="vv-card" id="vv-watchdog-card" style="grid-column:span 1;">
      <h3>Watchdog</h3>
      <div id="vv-watchdog-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-parity-card" style="grid-column:span 1;">
      <h3>Parity<a href="/Main" target="_blank" class="vv-card-cog" title="Array Management">⚙</a></h3>
      <div id="vv-parity-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-storage-card" style="grid-column:3/span 2;">
      <h3>Pools<a href="/Main" target="_blank" class="vv-card-cog" title="Array Management">⚙</a></h3>
      <div id="vv-storage-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-array-card" style="grid-column:5/span 4;">
      <h3><span id="vv-array-title">Array</span><a href="/Main" target="_blank" class="vv-card-cog" title="Array Management">⚙</a></h3>
      <div id="vv-array-body">Loading...</div>
    </div>


</div>

<script>
// ── Shared helpers ────────────────────────────────────────────────────────────

function vvMeter(label, pct, text) {
  const color = `hsl(${Math.round(120 * (1 - pct / 100))},70%,45%)`;
  return `<div style="margin-bottom:7px;">
    <div style="display:flex;justify-content:space-between;font-size:11px;color:#666;margin-bottom:3px;">
      <span>${label}</span><span style="color:#999;">${text}</span>
    </div>
    <div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
      <div style="width:${pct}%;height:100%;background:${color};border-radius:3px;transition:width 0.4s;"></div>
    </div>
  </div>`;
}

// ── Rolling history ───────────────────────────────────────────────────────────

const VV_HIST_MAX = 24; // 24 × 5 s = 120 s
let vvCpuHistory        = [];
let vvLastSessions      = [];
let vvLastStreamPollAt  = 0;
let vvLastStreamNames   = [];
let vvStreamServerCount = 0;
let vvScriptsFilter     = null;
let vvLastScripts       = {};
let vvThresholds        = {util_warn:70,util_crit:90,hdd_warn:45,hdd_crit:55,ssd_warn:60,ssd_crit:70};

let vvNetRxHistory  = [];
let vvNetTxHistory  = [];
let vvPoolsOpen      = {};
let vvPoolGroupOpen  = {};
let vvLastStorageDisks = [];

// ── Canvas chart ──────────────────────────────────────────────────────────────

function vvDrawChart(canvas, data, lineColor, fillColor, grid = false) {
  if (!canvas) return;
  const W = canvas.offsetWidth || 200;
  const H = canvas.offsetHeight || 36;
  if (canvas.width !== W) canvas.width = W;
  if (canvas.height !== H) canvas.height = H;
  const ctx = canvas.getContext('2d');
  ctx.clearRect(0, 0, W, H);

  // grid lines
  if (grid) {
    ctx.save();
    ctx.setLineDash([2, 3]);
    ctx.lineWidth = 0.5;
    [25, 50, 75].forEach(pct => {
      const y = H - (pct / 100) * (H - 2) - 1;
      ctx.strokeStyle = '#2e2e2e';
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(W, y);
      ctx.stroke();
      ctx.fillStyle = '#3a3a3a';
      ctx.font = '7px monospace';
      ctx.textAlign = 'left';
      ctx.fillText(pct + '%', 2, y - 2);
    });
    ctx.restore();
  }

  if (data.length < 2) return;

  const step = W / (VV_HIST_MAX - 1);
  const pts  = data.map((v, i) => [i * step, H - (v / 100) * (H - 2) - 1]);

  // fill
  ctx.beginPath();
  ctx.moveTo(0, H);
  pts.forEach(([x, y]) => ctx.lineTo(x, y));
  ctx.lineTo((data.length - 1) * step, H);
  ctx.closePath();
  ctx.fillStyle = fillColor;
  ctx.fill();

  // line
  ctx.beginPath();
  pts.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
  ctx.strokeStyle = lineColor;
  ctx.lineWidth   = 1.5;
  ctx.stroke();
}

// ── Network helpers ───────────────────────────────────────────────────────────

function vvFmtBps(bps) {
  if (bps >= 1e9) return (bps / 1e9).toFixed(2) + ' Gb/s';
  if (bps >= 1e6) return (bps / 1e6).toFixed(1) + ' Mb/s';
  if (bps >= 1e3) return (bps / 1e3).toFixed(0) + ' Kb/s';
  return bps + ' B/s';
}

function vvDrawNetChart(canvas, rxData, txData, maxBps) {
  if (!canvas) return;
  const W = canvas.offsetWidth || 200;
  const H = canvas.offsetHeight || 52;
  if (canvas.width !== W) canvas.width = W;
  if (canvas.height !== H) canvas.height = H;
  const ctx = canvas.getContext('2d');
  ctx.clearRect(0, 0, W, H);

  const scale = maxBps > 0 ? maxBps : 1;
  const toY   = v => H - (v / scale) * (H - 2) - 1;
  const step  = W / (VV_HIST_MAX - 1);

  // grid — labels show actual bandwidth at each line
  ctx.save();
  ctx.setLineDash([2, 3]);
  ctx.lineWidth = 0.5;
  [0.25, 0.5, 0.75].forEach(frac => {
    const y = H - frac * (H - 2) - 1;
    ctx.strokeStyle = '#2e2e2e';
    ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
    ctx.fillStyle = '#3a3a3a'; ctx.font = '7px monospace'; ctx.textAlign = 'left';
    ctx.fillText(vvFmtBps(maxBps * frac), 2, y - 2);
  });
  ctx.restore();

  const drawLine = (data, lineColor, fillColor) => {
    if (data.length < 2) return;
    const pts = data.map((v, i) => [i * step, toY(v)]);
    ctx.beginPath(); ctx.moveTo(0, H);
    pts.forEach(([x, y]) => ctx.lineTo(x, y));
    ctx.lineTo((data.length - 1) * step, H);
    ctx.closePath(); ctx.fillStyle = fillColor; ctx.fill();
    ctx.beginPath();
    pts.forEach(([x, y], i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
    ctx.strokeStyle = lineColor; ctx.lineWidth = 1.5; ctx.stroke();
  };

  drawLine(txData, '#ff9800', 'rgba(255,152,0,0.10)');
  drawLine(rxData, '#4caf50', 'rgba(76,175,80,0.12)');
}

// ── CPU helpers ───────────────────────────────────────────────────────────────

function vvCoreColor(freqMhz, maxMhz, minMhz) {
  if (!freqMhz || !maxMhz || maxMhz === minMhz) return '#4caf50';
  const t = Math.max(0, Math.min(1, (freqMhz - minMhz) / (maxMhz - minMhz)));
  // blue(220°) → green(120°) → red(0°)  as t goes 0→1
  const hue = Math.round((1 - t) * 220);
  return `hsl(${hue},70%,45%)`;
}

function vvRenderCpu(cpu) {
  const overall = cpu.overall ?? 0;
  const cores   = cpu.cores  ?? [];

  // Overall usage bar
  let html = vvMeter('Overall', overall, overall + '%');

  // Per-core vertical bars
  if (cores.length) {
    html += `<div class="vv-cpu-cores" style="display:flex;align-items:flex-end;gap:3px;height:54px;margin:10px 0 4px;overflow:hidden;">`;
    cores.forEach(c => {
      const usePct = c.usage_pct ?? 0;
      const hue    = Math.round(120 * (1 - usePct / 100));
      const color  = `hsl(${hue},70%,45%)`;
      const barH   = Math.max(2, Math.round(usePct * 0.46)); // max ~46px at 100%
      const label  = c.freq_mhz ? (c.freq_mhz >= 1000 ? (c.freq_mhz/1000).toFixed(1)+'G' : c.freq_mhz+'M') : '';
      html += `<div class="vv-cpu-core" style="flex:1;display:flex;flex-direction:column;align-items:center;gap:1px;min-width:10px;">
        <div style="font-size:8px;color:#555;line-height:1;">${label}</div>
        <div style="width:100%;height:46px;background:#1a1a1a;border-radius:2px;display:flex;align-items:flex-end;overflow:hidden;">
          <div style="width:100%;height:${barH}px;background:${color};border-radius:2px 2px 0 0;transition:height 0.4s;"></div>
        </div>
        <div style="font-size:8px;color:#555;line-height:1;">${c.core}</div>
      </div>`;
    });
    html += `</div>`;

    // Freq legend
    html += `<div style="display:flex;justify-content:space-between;font-size:9px;color:#444;margin-bottom:8px;">
      <span style="color:hsl(120,70%,45%)">Low</span>
      <span style="color:hsl(60,70%,45%)">Med</span>
      <span style="color:hsl(0,70%,45%)">High</span>
    </div>`;
  }

  // Canvas chart
  html += `<canvas id="vv-cpu-canvas" style="width:100%;height:60px;display:block;"></canvas>`;
  return html;
}

// ── Memory helpers ────────────────────────────────────────────────────────────

const VV_MEM_COLORS = {
  system: '#5c6bc0',
  vm:     '#f57c00',
  zfs:    '#0097a7',
  docker: '#388e3c',
  free:   '#37474f',
};

function vvFmtGib(kb) {
  return (kb / 1048576).toFixed(1) + ' GiB';
}

function vvMemRow(label, kb, totalKb, color) {
  const pct = totalKb > 0 ? Math.round(kb / totalKb * 100) : 0;
  return `<div style="margin-bottom:5px;">
    <div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:2px;">
      <span style="color:${color};font-weight:500;">${label}</span>
      <span style="color:#777;">${vvFmtGib(kb)}</span>
    </div>
    <div style="background:#1a1a1a;border-radius:3px;height:5px;overflow:hidden;">
      <div style="width:${pct}%;height:100%;background:${color};border-radius:3px;transition:width 0.4s;"></div>
    </div>
  </div>`;
}

function vvRenderMemory(mem) {
  const total  = mem.total_kb ?? 1;
  const usedKb = total - (mem.free_kb ?? 0);
  const usedPct = Math.round(usedKb / total * 100);
  const totalColor = usedPct >= 85 ? '#f44336' : usedPct >= 65 ? '#ff9800' : '#ccc';

  const procs = mem.top_procs ?? [];
  const procStrip = procs.map(p =>
    `<span style="white-space:nowrap;">${p.name}&nbsp;<span style="color:#aaa;font-weight:600;">${vvFmtGib(p.kb)}</span></span>`
  ).join('<span style="color:#333;margin:0 5px;">·</span>');

  let html = `<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px;">
    <div style="font-size:14px;font-weight:600;color:${totalColor};white-space:nowrap;">
      ${vvFmtGib(usedKb)} <span style="font-size:11px;color:#555;font-weight:400;">/ ${vvFmtGib(total)}</span>
    </div>
    <div style="font-size:10px;color:#666;text-align:right;margin-left:10px;overflow:hidden;min-width:0;white-space:nowrap;text-overflow:ellipsis;">${procStrip}</div>
  </div>`;
  html += vvMemRow('System', mem.system_kb ?? 0, total, VV_MEM_COLORS.system)
        + vvMemRow('VM',     mem.vm_kb     ?? 0, total, VV_MEM_COLORS.vm)
        + vvMemRow('ZFS',    mem.zfs_kb    ?? 0, total, VV_MEM_COLORS.zfs)
        + vvMemRow('Docker', mem.docker_kb ?? 0, total, VV_MEM_COLORS.docker)
        + vvMemRow('Free',   mem.free_kb   ?? 0, total, VV_MEM_COLORS.free);

  // Swap — only shown when swap is configured and has some usage
  const swapTotal = mem.swap_total_kb ?? 0;
  const swapUsed  = mem.swap_used_kb  ?? 0;
  if (swapTotal > 0) {
    const swapPct   = Math.round(swapUsed / swapTotal * 100);
    const swapColor = swapPct >= 50 ? '#f44336' : swapPct >= 20 ? '#ff9800' : '#607d8b';
    html += `<div style="margin-top:6px;padding-top:6px;border-top:1px solid #1a1a1a;">
      <div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:2px;">
        <span style="color:${swapColor};font-weight:500;">Swap</span>
        <span style="color:#777;">${vvFmtGib(swapUsed)} / ${vvFmtGib(swapTotal)}</span>
      </div>
      <div style="background:#1a1a1a;border-radius:3px;height:5px;overflow:hidden;">
        <div style="width:${swapPct}%;height:100%;background:${swapColor};border-radius:3px;transition:width 0.4s;"></div>
      </div>
    </div>`;
  }
  return html;
}

// ── Scripts render (filter-aware) ─────────────────────────────────────────────

function vvRenderScripts() {
  const sc      = vvLastScripts;
  const scList  = sc.scripts ?? [];
  const running = sc.running_count ?? 0;
  const ok      = sc.ok_count ?? 0;
  const warn    = sc.warn_count ?? 0;
  const errors  = sc.error_count ?? 0;
  const now     = Math.floor(Date.now() / 1000);

  function vvScriptPill(type, count, color, bg, border, icon) {
    const active = vvScriptsFilter === type;
    return `<span onclick="vvScriptsFilterSet('${type}')"
      style="font-size:10px;color:${color};background:${bg};border:1px solid ${active ? color : border};
      padding:1px 7px;border-radius:10px;cursor:pointer;font-weight:${active ? '700' : '400'};">${icon} ${count} ${type}</span>`;
  }

  let html = `<div style="display:flex;gap:5px;flex-wrap:wrap;margin-bottom:8px;">`;
  if (running > 0) html += vvScriptPill('running', running, '#4fc3f7', '#0a2233', '#1e4060', '●');
  if (ok > 0)      html += vvScriptPill('ok',      ok,      '#4caf50', '#0a1f0a', '#1a3a1a', '✓');
  if (warn > 0)    html += vvScriptPill('warn',    warn,    '#ff9800', '#1f1200', '#3a2200', '!');
  if (errors > 0)  html += vvScriptPill('error',   errors,  '#f44336', '#2a0a0a', '#5a1a1a', '✗');
  if (!running && !ok && !warn && !errors) html += `<span style="font-size:10px;color:#555;">No recent runs</span>`;
  html += `</div>`;

  const filtered = vvScriptsFilter ? scList.filter(s => s.status === vvScriptsFilter) : scList;
  let listHtml = '';
  filtered.forEach(s => {
    const diff  = now - (s.last_ts ?? now);
    const ago   = diff < 60 ? diff + 's' : diff < 3600 ? Math.floor(diff / 60) + 'm' : diff < 86400 ? Math.floor(diff / 3600) + 'h' : Math.floor(diff / 86400) + 'd';
    const icon  = s.status === 'running' ? '●' : s.status === 'ok' ? '✓' : s.status === 'warn' ? '!' : s.status === 'error' ? '✗' : '?';
    const color = s.status === 'running' ? '#4fc3f7' : s.status === 'ok' ? '#4caf50' : s.status === 'warn' ? '#ff9800' : s.status === 'error' ? '#f44336' : '#555';
    const name  = s.name.length > 24 ? s.name.slice(0, 23) + '…' : s.name;
    const dur   = s.duration != null ? ` ${s.duration}s` : '';
    listHtml += `<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px;font-size:11px;">
      <span style="color:${color};flex-shrink:0;width:10px;text-align:center;">${icon}</span>
      <span style="color:#888;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${name}</span>
      <span style="color:#444;font-size:10px;flex-shrink:0;">${ago}${dur}</span>
    </div>`;
  });

  if (!filtered.length) listHtml = `<p style="color:#555;font-style:italic;font-size:12px;">${vvScriptsFilter ? 'None in this group' : 'No script logs found'}</p>`;
  html += `<div style="max-height:133px;overflow-y:auto;">${listHtml}</div>`;
  const el = document.getElementById('vv-scripts-body');
  if (el) el.innerHTML = html;
}

function vvScriptsFilterSet(type) {
  vvScriptsFilter = vvScriptsFilter === type ? null : type;
  vvRenderScripts();
}

// ── Disk / storage helpers (module-level so vvRenderPools can call them) ─────

function vvTempColor(tempC, transport) {
  if (tempC === null) return '#444';
  const isSsd = transport === 'nvme' || transport === 'ssd';
  const warn  = isSsd ? vvThresholds.ssd_warn : vvThresholds.hdd_warn;
  const crit  = isSsd ? vvThresholds.ssd_crit : vvThresholds.hdd_crit;
  return tempC >= crit ? '#f44336' : tempC >= warn ? '#ff9800' : '#4caf50';
}

function vvFmt(v) { return v >= 1024 ? (v / 1024).toFixed(1) + ' TB' : v + ' GB'; }

function vvDiskRow(disk) {
  const tempC     = disk.temp;
  const tempColor = vvTempColor(tempC, disk.transport);
  const tempStr   = tempC !== null ? `${tempC}°` : '—';
  const isParity  = disk.role === 'parity';
  const nameColor = isParity ? '#6a8faf' : '#aaa';
  const pct       = disk.pct ?? 0;
  const barColor  = isParity ? '#1e3a5a'
    : pct >= vvThresholds.util_crit ? '#f44336'
    : pct >= vvThresholds.util_warn ? '#ff9800'
    : '#4caf50';
  const barWidth  = isParity ? '100' : pct;
  const spinLabel = (!isParity && !disk.mounted) ? `<span style="color:#555;font-size:9px;margin-left:4px;">↓</span>` : '';
  const right     = isParity
    ? `<span style="color:#444;font-size:10px;">${vvFmt(disk.size_gb)}</span>`
    : `<span style="color:#555;font-size:10px;">${vvFmt(disk.used_gb)} / ${vvFmt(disk.size_gb)}</span>`;
  return `<div style="margin-bottom:7px;">
    <div style="display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:3px;">
      <span style="color:${nameColor};">${disk.name}${spinLabel}</span>
      ${right}
      <span style="color:${tempColor};font-size:10px;margin-left:6px;flex-shrink:0;">${tempStr}</span>
    </div>
    <div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
      <div style="width:${barWidth}%;height:100%;background:${barColor};border-radius:3px;transition:width 0.4s;"></div>
    </div>
  </div>`;
}

function vvDiskCol(disks) {
  return `<div style="flex:1;min-width:0;">${disks.map(vvDiskRow).join('')}</div>`;
}

// ── Poll ──────────────────────────────────────────────────────────────────────

function vvPollMonitor() {
  fetch('/plugins/varaverk/api/monitor.php')
    .then(r => r.json())
    .then(d => {

      // ── API status banner ────────────────────────────────────────────────────
      const apiStatus = d._api_status ?? {};
      const banner    = document.getElementById('vv-api-banner');
      if (banner) {
        const hasFallbacks = apiStatus.fallbacks && apiStatus.fallbacks.length > 0;
        banner.style.display = hasFallbacks ? '' : 'none';
        const listEl = document.getElementById('vv-api-fallback-list');
        if (listEl && hasFallbacks) listEl.textContent = '(' + apiStatus.fallbacks.join(', ') + ')';
      }

      // ── Thresholds (from dynamix.cfg via backend) ────────────────────────────
      if (d.thresholds) vvThresholds = d.thresholds;

      // ── System ──────────────────────────────────────────────────────────────
      const sys = d.system ?? {};
      const now = new Date();
      const timeStr = now.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
      const dateStr = now.toLocaleDateString([], {weekday:'short', day:'numeric', month:'long', year:'numeric'});
      const tz = Intl.DateTimeFormat().resolvedOptions().timeZone.split('/').pop().replace('_',' ');
      const arrayColor = (sys.array_state === 'STARTED') ? '#4caf50' : '#f44336';
      const ver = (sys.version || '').replace('version=','').replace(/"/g,'');

      // Running container + VM counts for system card
      const _runningCtrs = [...(d.docker_folders?.folders ?? []), {containers: d.docker_folders?.ungrouped ?? []}]
        .flatMap(f => f.containers).filter(c => c.running).length;
      const _runningVMs  = (d.vms?.vms ?? []).filter(v => v.state === 'running').length;
      const _threadInfo  = sys.cpu_threads ? `${sys.cpu_cores}c / ${sys.cpu_threads}t` : '';

      document.getElementById('vv-system-body').innerHTML =
        `<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;">
           <div style="min-width:0;">
             <div style="font-size:14px;font-weight:700;color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${sys.name}</div>
             <div style="font-size:10px;color:#555;margin-top:2px;">${sys.comment || '&nbsp;'}</div>
           </div>
           <div style="display:flex;align-items:flex-start;gap:6px;flex-shrink:0;margin-left:6px;">
             <svg width="32" height="54" viewBox="0 0 38 64" fill="none" style="opacity:0.15;pointer-events:none;">
               <rect x="1" y="1" width="36" height="62" rx="3" stroke="#aaa" stroke-width="1.2" fill="#111"/>
               <rect x="1" y="1" width="36" height="10" rx="3" fill="#1c1c1c" stroke="#aaa" stroke-width="1.2"/>
               <circle cx="19" cy="6" r="2.5" stroke="#ff9800" stroke-width="1" fill="none"/>
               <line x1="19" y1="3.8" x2="19" y2="2.2" stroke="#ff9800" stroke-width="1"/>
               <rect x="26" y="4" width="4" height="2" rx="0.5" fill="#555"/>
               <rect x="3" y="14" width="16" height="44" rx="1" fill="#0d0d0d" stroke="#444" stroke-width="0.6"/>
               <line x1="3" y1="18" x2="19" y2="18" stroke="#333" stroke-width="0.6"/>
               <line x1="3" y1="22" x2="19" y2="22" stroke="#333" stroke-width="0.6"/>
               <line x1="3" y1="26" x2="19" y2="26" stroke="#333" stroke-width="0.6"/>
               <line x1="3" y1="30" x2="19" y2="30" stroke="#333" stroke-width="0.6"/>
               <line x1="3" y1="34" x2="19" y2="34" stroke="#333" stroke-width="0.6"/>
               <line x1="3" y1="38" x2="19" y2="38" stroke="#333" stroke-width="0.6"/>
               <line x1="3" y1="42" x2="19" y2="42" stroke="#333" stroke-width="0.6"/>
               <line x1="3" y1="46" x2="19" y2="46" stroke="#333" stroke-width="0.6"/>
               <line x1="3" y1="50" x2="19" y2="50" stroke="#333" stroke-width="0.6"/>
               <rect x="21" y="14" width="14" height="44" rx="1" fill="#08080f" stroke="#444" stroke-width="0.6" opacity="0.7"/>
               <rect x="5"  y="60" width="5" height="2" rx="1" fill="#333"/>
               <rect x="28" y="60" width="5" height="2" rx="1" fill="#333"/>
             </svg>
             <a href="/" target="_blank" class="vv-card-cog" title="Dashboard" style="margin-top:1px;">⚙</a>
           </div>
         </div>
         <div style="font-size:20px;font-weight:300;color:#ccc;line-height:1;">${timeStr}</div>
         <div style="font-size:10px;color:#555;margin-bottom:10px;">${dateStr} &middot; ${tz}</div>
         <div style="display:grid;grid-template-columns:auto 1fr;gap:3px 8px;font-size:11px;">
           <span style="color:#444;">Model</span>    <span style="color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${sys.cpu_model}${_threadInfo ? ` <span style="color:#444;">(${_threadInfo})</span>` : ''}</span>
           <span style="color:#444;">Array</span>    <span style="color:${arrayColor};font-weight:600;">${sys.array_state}</span>
           <span style="color:#444;">Uptime</span>   <span style="color:#888;">${sys.uptime}</span>
           <span style="color:#444;">Running</span>  <span style="color:#888;">${_runningCtrs} ctr${_runningCtrs !== 1 ? 's' : ''}${_runningVMs > 0 ? ` · ${_runningVMs} VM` : ''}</span>
           <span style="color:#444;">Version</span>  <span style="color:#3a3a3a;">${ver}</span>
         </div>`;

      // ── Partner ──────────────────────────────────────────────────────────────
      const pt          = d.partner ?? {};
      const ptHosts     = pt.hosts ?? [];
      const ptRemote    = d.remote_hosts ?? {};
      const ptStatus = pt.enabled
        ? `<span style="color:#4caf50;">enabled</span> · sync every ${pt.sync_min}min`
        : `<span style="color:#555;">disabled</span>`;

      let ptHtml = `<div style="font-size:10px;color:#666;margin-bottom:8px;">${ptStatus}</div>`;
      ptHosts.forEach(h => {
        const dot   = h.online === null ? '#555' : h.online ? '#4caf50' : '#f44336';
        const label = h.online === null ? 'unknown' : h.online ? 'online' : 'offline';
        const tags  = [
          h.is_me    ? `<span style="background:#1a3a1a;color:#4caf50;font-size:8px;padding:1px 5px;border-radius:3px;margin-left:4px;">US</span>` : '',
          h.is_owner ? `<span style="background:#1a2a3a;color:#4a9eff;font-size:8px;padding:1px 5px;border-radius:3px;margin-left:4px;">OWNER</span>` : '',
        ].join('');

        // Remote stats from Unraid API (only available for non-self hosts with an API key)
        const rs = !h.is_me ? (ptRemote[h.id] ?? null) : null;
        let statsHtml = '';
        if (rs && rs.available) {
          const cpuAvail  = rs.cpu_load > 0;
          const ramAvail  = rs.mem_used_pct > 0;
          const cpuHue    = cpuAvail ? Math.round(120 * (1 - rs.cpu_load / 100)) : 0;
          const memHue    = ramAvail ? Math.round(120 * (1 - rs.mem_used_pct / 100)) : 0;
          const cpuStr    = cpuAvail ? `<span style="color:hsl(${cpuHue},70%,45%);font-weight:600;">${rs.cpu_load}%</span>` : `<span style="color:#333;">—</span>`;
          const ramStr    = ramAvail ? `<span style="color:hsl(${memHue},70%,45%);font-weight:600;">${rs.mem_used_pct}%</span>` : `<span style="color:#333;">—</span>`;
          const arrColor  = rs.array_state === 'Started' || rs.array_state === 'STARTED' ? '#4caf50' : '#f44336';
          const uptimeStr = rs.uptime && rs.uptime !== '—' ? rs.uptime : '—';

          // Version mismatch warning — scripts will refuse sync operations until versions match
          const myVer     = (sys.version || '').replace('version=','').replace(/"/g,'').trim();
          const remoteVer = (rs.version  || '').trim();
          const verMismatch = myVer && remoteVer && myVer !== remoteVer;
          const verWarn = verMismatch
            ? `<div style="font-size:10px;color:#ff9800;margin-top:4px;padding:3px 6px;background:#1a1000;border:1px solid #3a2800;border-radius:3px;">
                ⚠ Version mismatch: local ${myVer} · remote ${remoteVer}<br>
                <span style="color:#555;">Script sync ops are gated until versions match</span>
               </div>` : '';

          statsHtml = `<div style="display:grid;grid-template-columns:auto 1fr auto 1fr;gap:2px 8px;font-size:10px;margin-top:5px;margin-bottom:2px;">
            <span style="color:#444;">CPU</span>${cpuStr}
            <span style="color:#444;">RAM</span>${ramStr}
            <span style="color:#444;">Array</span>
            <span style="color:${arrColor};font-weight:600;">${rs.array_state}</span>
            <span style="color:#444;">Uptime</span>
            <span style="color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${uptimeStr}</span>
          </div>${verWarn}`;
        } else if (rs && rs.no_api_key) {
          statsHtml = `<div style="font-size:10px;color:#444;margin-top:4px;">API key not configured — complete Onboard to enable</div>`;
        } else if (rs && !rs.available) {
          statsHtml = `<div style="font-size:10px;color:#444;margin-top:4px;">API unreachable</div>`;
        }

        ptHtml += `<div style="margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid #222;">
          <div style="display:flex;align-items:center;justify-content:space-between;">
            <div>
              <span style="font-size:10px;color:#555;margin-right:4px;">${h.id}</span>
              <span style="font-size:12px;color:#ccc;font-weight:500;">${h.owner || h.hostname}</span>${tags}
              <div style="font-size:10px;color:#555;margin-top:1px;">${h.hostname}</div>
            </div>
            <span style="color:${dot};font-size:10px;white-space:nowrap;">● ${label}</span>
          </div>
          ${statsHtml}
        </div>`;
      });
      document.getElementById('vv-partner-body').innerHTML = ptHtml;

      // ── Fallback ──────────────────────────────────────────────────────────────
      const fb      = d.fallback ?? {};
      const fbActive = d.fallback_active ?? [];
      const state   = (fb.state ?? 'UNKNOWN').toUpperCase();
      const stateMap = {
        NORMAL:      ['#4caf50', '✓ Nominal'],
        FAILOVER:    ['#f44336', '⚠ Failover active'],
        NO_INTERNET: ['#ff9800', '⚡ Internet lost'],
        DARK:        ['#9e9e9e', '◌ Dark mode'],
        UNKNOWN:     ['#444',    '— No state file'],
      };
      const [sColor, sLabel] = stateMap[state] ?? ['#444', state];

      let fbHtml = `<div style="font-size:11px;color:${sColor};margin-bottom:8px;">${sLabel}`;
      if (state === 'FAILOVER') {
        const start = parseInt(fb.failover_start ?? 0);
        if (start > 0) {
          const sec = Math.floor(Date.now() / 1000) - start;
          fbHtml += ` · ${Math.floor(sec/3600)}h ${Math.floor((sec%3600)/60)}m`;
        }
      }
      fbHtml += `</div>`;

      if (fbActive.length) {
        fbActive.forEach(group => {
          fbHtml += `<div style="font-size:10px;color:#888;margin-bottom:4px;">Running for ${group.hostname}</div>`;
          group.containers.forEach(c => {
            const img = c.image.includes('/') ? c.image.split('/').pop() : c.image;
            fbHtml += `<div style="display:flex;justify-content:space-between;align-items:center;
                         background:#1a1a1a;border-radius:4px;padding:4px 8px;margin-bottom:4px;">
              <span style="color:#ccc;font-size:11px;font-weight:500;">${c.name}</span>
              <span style="color:#555;font-size:9px;margin-left:8px;white-space:nowrap;">${img}</span>
            </div>`;
          });
        });
      } else if (state === 'NORMAL') {
        fbHtml += `<div style="font-size:10px;color:#333;">No containers active</div>`;
      }
      document.getElementById('vv-fallback-body').innerHTML = fbHtml;

      // ── CPU ─────────────────────────────────────────────────────────────────
      const cpu = d.cpu ?? {};
      document.getElementById('vv-cpu-body').innerHTML = vvRenderCpu(cpu);

      vvCpuHistory.push(cpu.overall ?? 0);
      if (vvCpuHistory.length > VV_HIST_MAX) vvCpuHistory.shift();
      vvDrawChart(document.getElementById('vv-cpu-canvas'), vvCpuHistory, '#4caf50', 'rgba(76,175,80,0.18)', true);

      // ── Memory ──────────────────────────────────────────────────────────────
      const mem = d.mem ?? {};
      document.getElementById('vv-memory-body').innerHTML = vvRenderMemory(mem);

      // ── Network ─────────────────────────────────────────────────────────────
      const net = d.net ?? {};
      if (net.available) {
        const rx = net.rx_bps ?? 0;
        const tx = net.tx_bps ?? 0;
        const linkMbps  = net.speed_mbps ?? 0;
        const linkLabel = linkMbps >= 1000 ? (linkMbps / 1000) + ' Gb/s' : linkMbps ? linkMbps + ' Mb/s' : '—';
        const linkBps   = linkMbps * 1e6;

        vvNetRxHistory.push(rx);
        vvNetTxHistory.push(tx);
        if (vvNetRxHistory.length > VV_HIST_MAX) vvNetRxHistory.shift();
        if (vvNetTxHistory.length > VV_HIST_MAX) vvNetTxHistory.shift();

        const maxSeen = Math.max(...vvNetRxHistory, ...vvNetTxHistory, 1);
        const maxBps  = maxSeen * 1.25; // auto-scale with 25% headroom

        const ipRows = [
          net.local_ip ? `<div><span style="color:#555;font-size:9px;">LAN&nbsp;&nbsp;</span>${net.local_ip}</div>` : '',
          net.ext_ip   ? `<div><span style="color:#555;font-size:9px;">EXT&nbsp;&nbsp;</span>${net.ext_ip}</div>`   : '',
          net.ts_ip    ? `<div><span style="color:#555;font-size:9px;">TS&nbsp;&nbsp;&nbsp;</span>${net.ts_ip}</div>`  : '',
        ].filter(Boolean).join('');

        document.getElementById('vv-network-body').innerHTML =
          `<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;">
             <div>
               <div style="font-size:12px;color:#888;margin-bottom:4px;">${net.iface} &nbsp;·&nbsp; ${linkLabel}</div>
               <div style="display:flex;gap:16px;font-size:13px;font-weight:600;">
                 <span><span style="color:#4caf50;font-size:9px;margin-right:4px;">━ IN (RX)</span><span style="color:#4caf50;">${vvFmtBps(rx)}</span></span>
                 <span><span style="color:#ff9800;font-size:9px;margin-right:4px;">━ OUT (TX)</span><span style="color:#ff9800;">${vvFmtBps(tx)}</span></span>
               </div>
             </div>
             <div style="text-align:right;font-size:11px;color:#aaa;line-height:1.6;">${ipRows}</div>
           </div>
           <canvas id="vv-net-canvas" style="width:100%;height:110px;display:block;"></canvas>`;

        vvDrawNetChart(document.getElementById('vv-net-canvas'), vvNetRxHistory, vvNetTxHistory, maxBps);
      } else {
        document.getElementById('vv-network-body').innerHTML = '<p style="color:#555;font-style:italic">No network interface detected</p>';
      }

      // ── CPU title ────────────────────────────────────────────────────────────
      const _cpuTitleEl = document.getElementById('vv-cpu-title');
      if (_cpuTitleEl && sys.cpu_threads) _cpuTitleEl.textContent = `CPU · ${sys.cpu_cores}c/${sys.cpu_threads}t`;

      // ── UPS / Power ─────────────────────────────────────────────────────────
      const ups = d.ups ?? {};
      if (ups.available) {
        const onBatt    = ups.status === 'ONBATT';
        const statCls   = onBatt ? 'vv-banner-err' : ups.status === 'ONLINE' ? 'vv-banner-ok' : 'vv-banner-warn';
        const statColor = onBatt ? '#f44336' : ups.status === 'ONLINE' ? '#4caf50' : '#ff9800';
        const loadPct   = ups.load_pct ?? 0;
        const loadHue   = Math.round(120 * (1 - loadPct / 100));
        const bPct      = ups.bcharge ?? 0;
        const bHue      = Math.round(120 * (bPct / 100));
        const bColor    = onBatt ? '#f44336' : `hsl(${bHue},70%,45%)`;
        const timeLeft  = ups.timeleft != null ? ups.timeleft.toFixed(1) + ' min' : '—';
        const watts     = ups.watts != null ? ups.watts + ' W' : '—';
        const lineV     = ups.line_v != null ? ups.line_v + ' V' : '—';
        const outV      = ups.output_v != null ? ups.output_v + ' V' : '—';
        const xfers     = ups.num_xfers ?? 0;
        const batIcon   = bPct >= 80 ? '▰▰▰▰' : bPct >= 60 ? '▰▰▰▱' : bPct >= 40 ? '▰▰▱▱' : bPct >= 20 ? '▰▱▱▱' : '▱▱▱▱';

        document.getElementById('vv-ups-body').innerHTML =
          `<div class="vv-banner ${statCls}" style="margin-bottom:8px;">
             <span>${ups.status}${onBatt ? ' — ON BATTERY' : ''}</span>
             <span style="font-size:11px;font-weight:400;opacity:0.8;">${ups.model}</span>
           </div>
           <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;">
             <div>
               <div style="font-size:10px;color:#555;margin-bottom:3px;">Load</div>
               <div style="font-size:15px;font-weight:600;color:hsl(${loadHue},70%,45%);line-height:1;">${loadPct.toFixed(0)}<span style="font-size:10px;font-weight:400;color:#555;">%</span></div>
               <div style="font-size:10px;color:#444;margin-top:2px;">${watts}</div>
             </div>
             <div>
               <div style="font-size:10px;color:#555;margin-bottom:3px;">Battery</div>
               <div style="font-size:15px;font-weight:600;color:${bColor};line-height:1;">${bPct.toFixed(0)}<span style="font-size:10px;font-weight:400;color:#555;">%</span></div>
               <div style="font-size:10px;color:#444;margin-top:2px;">${timeLeft}</div>
             </div>
           </div>
           <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:8px;">
             <div style="background:#1a1a1a;border-radius:3px;height:5px;overflow:hidden;">
               <div style="width:${loadPct}%;height:100%;background:hsl(${loadHue},70%,45%);transition:width 0.4s;"></div>
             </div>
             <div style="background:#1a1a1a;border-radius:3px;height:5px;overflow:hidden;">
               <div style="width:${bPct}%;height:100%;background:${bColor};transition:width 0.4s;"></div>
             </div>
           </div>
           <div style="display:grid;grid-template-columns:auto 1fr auto 1fr;gap:3px 8px;font-size:10px;">
             <span style="color:#444;">In</span>    <span style="color:#777;">${lineV}</span>
             <span style="color:#444;">Out</span>   <span style="color:#777;">${outV}</span>
             <span style="color:#444;">Xfers</span> <span style="color:${xfers > 0 ? '#ff9800' : '#444'};">${xfers}</span>
             <span style="color:#444;">Test</span>  <span style="color:#555;">${ups.selftest || '—'}</span>
           </div>`;
      } else {
        document.getElementById('vv-ups-body').innerHTML = '<p style="color:#555;font-style:italic;font-size:12px;">No UPS detected</p>';
      }

      // ── Parity ──────────────────────────────────────────────────────────────
      const par = d.parity ?? {};
      (function() {
        const valid      = par.valid !== false;
        const inProg     = par.in_progress;
        const validColor = valid ? '#4caf50' : '#f44336';
        const validLabel = valid ? 'Parity is valid' : 'Parity is INVALID';

        function vvRelTime(ts) {
          if (!ts) return '';
          const diff = Math.floor(Date.now() / 1000) - ts;
          const d = Math.floor(diff / 86400), h = Math.floor((diff % 86400) / 3600), m = Math.floor((diff % 3600) / 60);
          const parts = [];
          if (d) parts.push(d + ' day' + (d !== 1 ? 's' : ''));
          if (h) parts.push(h + ' hour' + (h !== 1 ? 's' : ''));
          if (!d && m) parts.push(m + ' minute' + (m !== 1 ? 's' : ''));
          return parts.join(', ') + ' ago';
        }
        function vvDueIn(ts) {
          if (!ts) return '—';
          const diff = ts - Math.floor(Date.now() / 1000);
          if (diff <= 0) return 'overdue';
          const d = Math.floor(diff / 86400), h = Math.floor((diff % 86400) / 3600), m = Math.floor((diff % 3600) / 60);
          const parts = [];
          if (d) parts.push(d + ' day' + (d !== 1 ? 's' : ''));
          if (h) parts.push(h + ' hour' + (h !== 1 ? 's' : ''));
          if (!d && m) parts.push(m + ' minute' + (m !== 1 ? 's' : ''));
          return 'Due in: ' + parts.join(', ');
        }
        function vvFmtDate(ts) {
          if (!ts) return '—';
          return new Date(ts * 1000).toLocaleString([], {weekday:'short',day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'});
        }

        const exitColor  = par.exit_label === 'Completed' ? '#4caf50' : par.exit_label === 'Aborted' ? '#ff9800' : '#f44336';
        const errColor   = (par.errors ?? 0) > 0 ? '#f44336' : '#444';
        const speedStr   = par.last_speed_mb ? ` · ${par.last_speed_mb} MB/s` : '';
        const nextDate   = vvFmtDate(par.next_ts);
        const dueIn      = vvDueIn(par.next_ts);
        const parBannerCls = !valid ? 'vv-banner-err' : (par.errors ?? 0) > 0 ? 'vv-banner-warn' : 'vv-banner-ok';

        let html = `<div class="vv-banner ${parBannerCls}">
          <span>${valid ? '✓ Valid' : '✗ INVALID'}</span>
          ${(par.errors ?? 0) > 0 ? `<span style="font-size:11px;">${par.errors} error${par.errors !== 1 ? 's' : ''}</span>` : ''}
        </div>`;

        if (inProg) {
          const pct = par.resync_pct ?? 0;
          html += `<div style="font-size:11px;color:#aaa;margin-bottom:4px;">Check in progress — ${pct}%</div>
          <div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;margin-bottom:10px;">
            <div style="width:${pct}%;height:100%;background:#4caf50;border-radius:3px;transition:width 2s;"></div>
          </div>`;
        }

        html += `
          <div style="display:grid;grid-template-columns:auto 1fr;gap:3px 10px;font-size:11px;">
            <span style="color:#555;">Last check</span>
            <span style="color:#888;">${par.last_date ?? '—'}${speedStr}</span>
            <span style="color:#555;">Status</span>
            <span style="color:${exitColor};">${par.exit_label ?? '—'}</span>
            <span style="color:#555;">Errors</span>
            <span style="color:${errColor};">${par.errors ?? 0}</span>
            <span style="color:#555;">Next check</span>
            <span style="color:#888;">${nextDate}</span>
          </div>
          <div style="font-size:10px;color:#555;margin-top:8px;">${dueIn}</div>`;

        document.getElementById('vv-parity-body').innerHTML = html;
      })();

      // ── Watchdog ─────────────────────────────────────────────────────────────
      (function() {
        const wd  = d.watchdog ?? {};
        const el  = document.getElementById('vv-watchdog-body');
        if (!el) return;

        const healthy      = wd.healthy ?? true;
        const ctrStrikes   = wd.ctr_strikes ?? {};
        const ctrNames     = Object.keys(ctrStrikes);
        const rwLevel      = wd.rw_level ?? 0;
        const rwPaused     = wd.rw_paused ?? [];
        const rwStopped    = wd.rw_stopped ?? [];
        const daemonHit    = (wd.daemon_strikes ?? 0) > 0;
        const oom          = wd.oom_count ?? 0;
        const reboots      = wd.reboots_12h ?? 0;
        const restartCount = wd.restart_count ?? 0;
        const restarts     = wd.restarts_24h ?? [];

        const stab         = wd.stability ?? {};
        const stabStrikes  = stab.strikes ?? {};
        const stabNames    = Object.keys(stabStrikes);

        const storWd       = wd.storage_wd ?? {};
        const growthStr    = storWd.growth_strikes ?? {};
        const logStr       = storWd.log_strikes ?? {};
        const storIssues   = Object.keys(growthStr).length + Object.keys(logStr).length;

        const netWd        = wd.network_wd ?? {};
        const npmStrikes   = netWd.npm_strikes ?? 0;

        const issueCount   = ctrNames.length + stabNames.length + storIssues
                           + (daemonHit ? 1 : 0) + (oom > 0 ? 1 : 0)
                           + (reboots > 0 ? 1 : 0) + (npmStrikes > 0 ? 1 : 0);
        const bannerCls    = healthy ? 'vv-banner-ok' : (reboots || oom || daemonHit ? 'vv-banner-err' : 'vv-banner-warn');
        const bannerTxt    = healthy ? '✓ All clear' : `⚠ ${issueCount} issue${issueCount !== 1 ? 's' : ''}`;

        let html = `<div class="vv-banner ${bannerCls}">${bannerTxt}`;
        if (reboots > 0) html += `<span style="font-size:10px;">${reboots} reboot${reboots !== 1 ? 's' : ''}/12h</span>`;
        html += `</div>`;

        // ── Alerts (critical items) ──────────────────────────────────────────
        if (rwLevel > 0) {
          const rwColor = rwLevel >= 3 ? '#f44336' : rwLevel >= 2 ? '#ff9800' : '#fdd835';
          const rwLabel = ['', 'Soft', 'Medium', 'Hard'][rwLevel] ?? `L${rwLevel}`;
          html += `<div style="font-size:11px;color:${rwColor};margin-bottom:4px;">⚡ Resource mgr: ${rwLabel}`;
          if (rwPaused.length) html += ` · ${rwPaused.length} paused`;
          if (rwStopped.length) html += ` · ${rwStopped.length} stopped`;
          html += `</div>`;
        }
        if (daemonHit) html += `<div style="font-size:11px;color:#f44336;margin-bottom:4px;">✗ Docker daemon strikes</div>`;
        if (oom > 0)   html += `<div style="font-size:11px;color:#f44336;margin-bottom:4px;">✗ OOM events: ${oom}</div>`;

        // ── System stats grid ────────────────────────────────────────────────
        function wdPct(v, warn, crit) {
          return v >= crit ? '#f44336' : v >= warn ? '#ff9800' : '#4caf50';
        }
        const ramFree   = stab.ram_free_gb ?? 0;
        const ramColor  = ramFree < 6 ? '#f44336' : ramFree < 12 ? '#ff9800' : '#4caf50';
        const load      = stab.load_1min ?? 0;
        const loadColor = load > 6 ? '#f44336' : load > 3 ? '#ff9800' : '#4caf50';
        const nicOk     = (stab.nic_state ?? '') === 'up';
        const sshdOk    = stab.sshd_ok ?? true;
        const zombies   = stab.zombies ?? 0;

        let statsHtml = `<div style="display:grid;grid-template-columns:auto 1fr;gap:2px 8px;font-size:11px;margin-top:8px;margin-bottom:6px;">
          <span style="color:#444;">rootfs</span><span style="color:${wdPct(stab.rootfs_pct??0,75,90)};">${stab.rootfs_pct??0}%</span>
          <span style="color:#444;">/var/log</span><span style="color:${wdPct(stab.log_pct??0,75,90)};">${stab.log_pct??0}%</span>
          <span style="color:#444;">/tmp</span><span style="color:${wdPct(stab.tmp_pct??0,75,90)};">${stab.tmp_pct??0}%</span>
          <span style="color:#444;">RAM free</span><span style="color:${ramColor};">${ramFree}GB</span>
          <span style="color:#444;">Load</span><span style="color:${loadColor};">${load}</span>
          ${stab.cpu_temp != null ? `<span style="color:#444;">CPU</span><span style="color:${wdPct(stab.cpu_temp,75,90)};">${stab.cpu_temp}°C</span>` : ''}
          <span style="color:#444;">Zombies</span><span style="color:${zombies>0?'#ff9800':'#4caf50'};">${zombies}</span>
          <span style="color:#444;">${stab.nic??'nic'}</span><span style="color:${nicOk?'#4caf50':'#f44336'};">● ${stab.nic_state??'?'}</span>
          <span style="color:#444;">sshd</span><span style="color:${sshdOk?'#4caf50':'#f44336'};">${sshdOk?'● ok':'✗ down'}</span>
          <span style="color:#444;">NPM</span><span style="color:${npmStrikes>0?'#ff9800':'#4caf50'};">${npmStrikes>0?npmStrikes+'× strikes':'● ok'}</span>
        </div>`;
        html += statsHtml;

        // ── Stability strikes (system watchdog) ──────────────────────────────
        if (stabNames.length) {
          html += `<div style="font-size:10px;color:#555;margin-bottom:3px;text-transform:uppercase;letter-spacing:.05em;">Stability strikes</div>`;
          stabNames.forEach(k => {
            html += `<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:2px;">
              <span style="color:#ff9800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${k}</span>
              <span style="color:#555;flex-shrink:0;margin-left:6px;">${stabStrikes[k]}×</span>
            </div>`;
          });
        }

        // ── Storage watchdog strikes ─────────────────────────────────────────
        if (storIssues > 0) {
          html += `<div style="font-size:10px;color:#555;margin-top:${stabNames.length?6:0}px;margin-bottom:3px;text-transform:uppercase;letter-spacing:.05em;">Storage watchdog</div>`;
          Object.entries(growthStr).forEach(([k, v]) => {
            html += `<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:2px;">
              <span style="color:#ff9800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">↑ ${k}</span>
              <span style="color:#555;flex-shrink:0;margin-left:6px;">${v}×</span>
            </div>`;
          });
          Object.entries(logStr).forEach(([k, v]) => {
            const shortKey = k.length > 22 ? '…' + k.slice(-22) : k;
            html += `<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:2px;">
              <span style="color:#ff9800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;" title="${k}">log ${shortKey}</span>
              <span style="color:#555;flex-shrink:0;margin-left:6px;">${v}×</span>
            </div>`;
          });
        }

        // ── Container strikes ────────────────────────────────────────────────
        if (ctrNames.length) {
          html += `<div style="font-size:10px;color:#555;margin-top:${stabNames.length||storIssues?6:0}px;margin-bottom:3px;text-transform:uppercase;letter-spacing:.05em;">Container strikes</div>`;
          ctrNames.forEach(name => {
            html += `<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:2px;">
              <span style="color:#f44336;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${name}</span>
              <span style="color:#555;flex-shrink:0;margin-left:6px;">${ctrStrikes[name]}×</span>
            </div>`;
          });
        }

        // ── Recent restarts ──────────────────────────────────────────────────
        if (restartCount > 0) {
          const hasAbove = ctrNames.length || stabNames.length || storIssues;
          html += `<div style="font-size:10px;color:#555;margin-top:${hasAbove?6:0}px;margin-bottom:3px;text-transform:uppercase;letter-spacing:.05em;">Restarts 24h <span style="color:#ff9800;">${restartCount}</span></div>`;
          const now = Math.floor(Date.now() / 1000);
          restarts.forEach(r => {
            const diff = now - r.ts;
            const ago  = diff < 3600 ? Math.floor(diff / 60) + 'm' : Math.floor(diff / 3600) + 'h';
            html += `<div style="display:flex;justify-content:space-between;font-size:10px;color:#666;margin-bottom:2px;">
              <span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${r.name}</span>
              <span style="flex-shrink:0;margin-left:6px;color:#444;">${ago}</span>
            </div>`;
          });
        }

        el.innerHTML = html;
      })();


      // ── Pools ─────────────────────────────────────────────────────────────────
      vvLastStorageDisks = d.storage ?? [];
      document.getElementById('vv-storage-body').innerHTML = vvRenderPools(vvLastStorageDisks);

      // ── Array disks — summary header + min 3 columns ─────────────────────────
      const arrayDisks = d.array_disks ?? [];
      if (arrayDisks.length) {
        const dataDisks   = arrayDisks.filter(d => d.role === 'data');
        const okDisks     = dataDisks.filter(d => d.status === 'DISK_OK').length;
        const errDisks    = dataDisks.length - okDisks;
        const temps       = arrayDisks.map(d => d.temp).filter(t => t != null);
        const maxTemp     = temps.length ? Math.max(...temps) : null;
        const maxTempColor = maxTemp == null ? '#444' : maxTemp >= (vvThresholds.hdd_crit ?? 55) ? '#f44336' : maxTemp >= (vvThresholds.hdd_warn ?? 45) ? '#ff9800' : '#4caf50';
        const totalUsedGb = dataDisks.reduce((s, d) => s + (d.used_gb ?? 0), 0);
        const totalSizeGb = dataDisks.reduce((s, d) => s + (d.size_gb ?? 0), 0);
        const arrPct      = totalSizeGb > 0 ? Math.round(totalUsedGb / totalSizeGb * 100) : 0;
        const arrPctColor = arrPct >= (vvThresholds.util_crit ?? 90) ? '#f44336' : arrPct >= (vvThresholds.util_warn ?? 70) ? '#ff9800' : '#4caf50';

        const titleEl = document.getElementById('vv-array-title');
        if (titleEl) titleEl.innerHTML = `Array
          <span style="font-size:10px;color:#444;font-weight:400;text-transform:none;letter-spacing:0;margin-left:6px;">
            ${okDisks}/${dataDisks.length} ok
            ${errDisks > 0 ? `<span style="color:#f44336;margin-left:4px;">· ${errDisks} err</span>` : ''}
            · <span style="color:${arrPctColor};">${arrPct}%</span> used
            ${maxTemp != null ? `· <span style="color:${maxTempColor};">max ${maxTemp}°</span>` : ''}
          </span>`;

        const numCols = Math.max(3, Math.ceil(arrayDisks.length / 6));
        const perCol  = Math.ceil(arrayDisks.length / numCols);
        const cols    = [];
        for (let i = 0; i < numCols; i++) cols.push(arrayDisks.slice(i * perCol, (i + 1) * perCol));
        document.getElementById('vv-array-body').innerHTML =
          `<div style="display:flex;gap:10px;">${cols.map(vvDiskCol).join('')}</div>`;
      } else {
        document.getElementById('vv-array-body').innerHTML = '<p style="color:#555;font-style:italic;font-size:12px;">No array disks</p>';
      }

      // ── GPU ─────────────────────────────────────────────────────────────────
      const gpu     = d.gpu ?? {};
      const gpuProcs = d.gpu_procs ?? [];
      if (gpu.available) {
        const vramPct   = gpu.memory_total > 0 ? Math.round(gpu.memory_used / gpu.memory_total * 100) : 0;
        const utilPct   = gpu.utilization ?? 0;
        const temp      = gpu.temperature ?? 0;
        const tempColor = temp >= 85 ? '#f44336' : temp >= 70 ? '#ff9800' : '#4caf50';
        const powerStr  = gpu.power_w != null ? gpu.power_w + ' W' : '—';
        const procCount = gpuProcs.length;
        const procColor = procCount > 0 ? '#4caf50' : '#555';

        document.getElementById('vv-gpu-body').innerHTML =
          // header row: name + process count pill
          `<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
             <div style="font-size:11px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${gpu.name}</div>
             <div style="margin-left:8px;background:${procColor}22;border:1px solid ${procColor};color:${procColor};
                         padding:1px 8px;border-radius:10px;font-size:11px;white-space:nowrap;">${procCount} proc${procCount !== 1 ? 's' : ''}</div>
           </div>` +
          vvMeter('VRAM',   vramPct,        `${gpu.memory_used} / ${gpu.memory_total} MB`) +
          vvMeter('GPU',    utilPct,         `${utilPct}%`) +
          vvMeter('Encode', gpu.enc_pct ?? 0, `${gpu.enc_pct ?? 0}%`) +
          vvMeter('Decode', gpu.dec_pct ?? 0, `${gpu.dec_pct ?? 0}%`) +
          `<div style="display:flex;justify-content:space-between;font-size:11px;margin-top:6px;margin-bottom:8px;">
             <span style="color:#666;">Temp</span>
             <span style="color:${tempColor};font-weight:bold;">${temp}°C</span>
             <span style="color:#666;margin-left:12px;">Power</span>
             <span style="color:#aaa;font-weight:bold;">${powerStr}</span>
           </div>`;
      } else {
        document.getElementById('vv-gpu-body').innerHTML = '<p style="color:#555;font-style:italic">No GPU detected</p>';
      }

      // ── Scripts ─────────────────────────────────────────────────────────────
      vvLastScripts = d.scripts ?? {};
      vvRenderScripts();

      // ── Transcode ───────────────────────────────────────────────────────────
      const tc = d.transcode ?? {};
      if (!tc.available) {
        document.getElementById('vv-transcode-body').innerHTML =
          '<p style="color:#555;font-style:italic">No transcode state — ramdisk_setup.sh not yet run.</p>';
      } else {
        const loc      = tc.is_ramdisk ? 'Ramdisk' : 'SSD';
        const locColor = tc.is_ramdisk ? '#4caf50' : '#ff9800';

        // RAM disk bar
        const rd      = tc.ramdisk ?? {};
        const rdUsed  = rd.used_mb ?? 0;
        const rdSize  = rd.size_mb ?? 0;
        const rdPct   = rdSize > 0 ? Math.round(rdUsed / rdSize * 100) : 0;
        const rdHue   = Math.round(120 * (1 - rdPct / 100));

        // SSD bar
        const ssd     = tc.ssd ?? {};
        const ssdUsed = ssd.used_mb ?? 0;
        const ssdSize = ssd.size_mb ?? 0;
        const ssdPct  = ssdSize > 0 ? Math.round(ssdUsed / ssdSize * 100) : 0;
        const ssdHue  = Math.round(120 * (1 - ssdPct / 100));

        // Flip info
        const ago = tc.last_flip_ago;
        let flipStr = 'never';
        if (ago !== null && ago !== undefined) {
          if (ago < 60)        flipStr = ago + 's ago';
          else if (ago < 3600) flipStr = Math.floor(ago / 60) + 'm ago';
          else                 flipStr = Math.floor(ago / 3600) + 'h ' + Math.floor((ago % 3600) / 60) + 'm ago';
        }

        // Server icon helper
        function vvSrvIcon(type, name) {
          const cfg = { emby: ['#4caf50','#fff','E'], jellyfin: ['#00A4DC','#fff','JF'], plex: ['#E5A00D','#000','P'] };
          const [bg, fg, lbl] = cfg[type] ?? ['#555','#fff', (type[0] || '?').toUpperCase()];
          return `<span title="${name}" style="display:inline-flex;align-items:center;justify-content:center;
            width:18px;height:18px;background:${bg};border-radius:3px;font-size:8px;font-weight:bold;
            color:${fg};flex-shrink:0;">${lbl}</span>`;
        }

        // Active sessions from shared streams data
        const activeSessions = vvLastSessions.filter(s => s.is_tc);
        const typeLabel = t => ({ LiveTvProgram:'Live TV', Movie:'Movie', Episode:'TV', Audio:'Music' }[t] ?? t);

        let sessHtml = '';
        if (activeSessions.length) {
          let rowsHtml = '';
          activeSessions.forEach(s => {
            const meth = s.method.replace('Transcode', 'TC').replace('Direct ', '');
            rowsHtml += `<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px;min-width:0;overflow:hidden;">
              ${vvSrvIcon(s.server_type, s.server)}
              <span style="font-size:10px;color:#888;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${s.title}</span>
              <span style="font-size:9px;color:#555;flex-shrink:0;">${typeLabel(s.type)}</span>
              <span style="font-size:9px;color:#ff9800;flex-shrink:0;">${meth}</span>
            </div>`;
          });
          sessHtml = `<div style="margin-top:8px;border-top:1px solid #2a2a2a;padding-top:6px;">
            <div style="font-size:10px;color:#555;margin-bottom:4px;">Active transcodes <span style="color:#ff9800;font-weight:600;">${activeSessions.length}</span></div>
            <div style="max-height:57px;overflow-y:auto;overflow-x:hidden;">${rowsHtml}</div>
          </div>`;
        }

        const nvmeColor = ssd.available ? '#4caf50' : '#f44336';
        document.getElementById('vv-transcode-body').innerHTML =
          `<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
             <div style="display:flex;gap:6px;align-items:center;">
               <span style="background:${locColor}22;border:1px solid ${locColor};color:${locColor};
                 padding:1px 8px;border-radius:10px;font-size:11px;font-weight:600;">● ${loc}</span>
               <span style="background:${nvmeColor}22;border:1px solid ${nvmeColor};color:${nvmeColor};
                 padding:1px 8px;border-radius:10px;font-size:11px;font-weight:600;">● NVMe</span>
             </div>
             <span style="font-size:10px;color:#555;">Flips: ${tc.flip_count_hour ?? 0}/hr · ${flipStr}</span>
           </div>
           <div style="margin-bottom:7px;">
             <div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px;">
               <span style="color:#666;">Ramdisk</span>
               <span style="color:#555;font-size:10px;">${rdUsed} / ${rdSize} MB</span>
             </div>
             <div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
               <div style="width:${rdPct}%;height:100%;background:hsl(${rdHue},70%,45%);border-radius:3px;transition:width 0.4s;"></div>
             </div>
           </div>
           <div style="margin-bottom:4px;">
             <div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px;">
               <span style="color:#666;">SSD Fallback</span>
               <span style="color:#555;font-size:10px;">${ssd.available ? ssdUsed + ' / ' + ssdSize + ' MB' : '—'}</span>
             </div>
             <div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
               <div style="width:${ssdPct}%;height:100%;background:hsl(${ssdHue},70%,45%);border-radius:3px;transition:width 0.4s;"></div>
             </div>
           </div>${sessHtml}`;
      }

      // ── Containers and VMs ──────────────────────────────────────────────────
      const dfData = d.docker_folders ?? { available: false, folders: [], ungrouped: [] };
      dfData.vms = d.vms ?? { available: false, vms: [] };
      vvRenderDockerFolders(dfData);

    })
    .catch(() => {});
}

vvPollMonitor();
setInterval(vvPollMonitor, 2000);

// Pin pools card width to CPU card width across rows
function vvSyncCardWidths() {
  const cpu   = document.getElementById('vv-cpu');
  const pools = document.getElementById('vv-storage-card');
  if (!cpu || !pools) return;
  const w = cpu.offsetWidth;
  if (w === 0) return;
  pools.style.flex       = 'none';
  pools.style.boxSizing  = 'border-box';
  pools.style.width      = w + 'px';
}
new ResizeObserver(vvSyncCardWidths).observe(document.getElementById('vv-cpu'));
requestAnimationFrame(vvSyncCardWidths);

// ── Media streams (slower poll — media server API calls) ──────────────────────

function vvFmtSec(sec) {
  const h = Math.floor(sec / 3600);
  const m = Math.floor((sec % 3600) / 60);
  const s = sec % 60;
  return h > 0
    ? `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`
    : `${m}:${String(s).padStart(2,'0')}`;
}

function vvRenderStreams() {
  if (vvStreamServerCount === 0) return;
  const sessions = vvLastSessions;
  const names    = vvLastStreamNames;
  const el       = document.getElementById('vv-streams-body');
  if (!el) return;

  // Per-server counts from full session list
  const serverCounts = {};
  sessions.forEach(s => serverCounts[s.server] = (serverCounts[s.server] || 0) + 1);
  const badges = names.map(n => {
    const cnt = serverCounts[n] ?? 0;
    return cnt > 0
      ? `<span class="vv-server-badge">${n} <b style="color:#ccc;">${cnt}</b></span>`
      : `<span class="vv-server-badge" style="color:#444;">${n}</span>`;
  }).join('');

  if (sessions.length === 0) {
    el.innerHTML = `<div class="vv-stream-servers">${badges}</div>`
      + '<p class="vv-stream-empty">Nothing playing</p>';
    return;
  }

  const elapsed = Math.max(0, Math.floor(Date.now() / 1000) - vvLastStreamPollAt);

  function vvStreamRow(s) {
    const isLive   = s.type === 'LiveTvProgram' || s.dur_sec === 0;
    const inc      = s.paused ? 0 : elapsed;
    const curSec   = Math.max(0, (s.pos_sec ?? 0) + inc);
    const barColor = isLive ? '#1e3a5a' : (s.is_tc ? '#e65100' : '#4caf50');
    const tcColor  = s.paused ? '#fdd835' : '#555';

    const icon      = s.paused ? '⏸' : '▶';
    const iconColor = s.paused ? '#fdd835' : isLive ? '#2196f3' : s.is_tc ? '#ff9800' : '#aaa';

    let timeStr = '';
    let pct = s.pct ?? 0;
    if (isLive) {
      pct = 75;
      timeStr = `<span style="color:${tcColor};">${vvFmtSec(curSec)}</span>`;
    } else if (s.dur_sec > 0) {
      pct = Math.min(100, Math.round(curSec / s.dur_sec * 100));
      timeStr = `<span style="color:${tcColor};">${vvFmtSec(curSec)} / ${vvFmtSec(s.dur_sec)}</span>`;
    }

    return `<div>
      <div style="display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:2px;">
        <span style="color:${s.paused ? '#fdd835' : '#aaa'};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">
          <span style="color:${iconColor};">${icon}</span> ${s.title}</span>
        <span style="color:#444;font-size:10px;margin-left:6px;flex-shrink:0;">${s.server}</span>
      </div>
      <div style="display:flex;justify-content:space-between;font-size:10px;color:#555;margin-bottom:3px;">
        <span>${s.user}</span>
        ${timeStr}
      </div>
      <div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
        <div style="width:${pct}%;height:100%;background:${barColor};border-radius:3px;transition:width 0.9s linear;"></div>
      </div>
    </div>`;
  }

  // Left-fill columns of 3 — col 1 fills first, col 2 opens when col 1 hits 3
  const shown  = sessions.slice(0, 12);
  const perCol = 3;
  const sCols  = [];
  for (let i = 0; i < shown.length; i += perCol) sCols.push(shown.slice(i, i + perCol));

  const vvStreamCol = col =>
    `<div style="flex:1;min-width:0;">${col.map(s =>
      `<div style="margin-bottom:8px;">${vvStreamRow(s)}</div>`
    ).join('')}</div>`;

  const overflow = sessions.length > 12
    ? `<div style="font-size:10px;color:#555;margin-top:4px;">+${sessions.length - 12} more not shown</div>`
    : '';

  el.innerHTML = `<div class="vv-stream-servers">${badges}</div>
    <div style="display:flex;gap:10px;">${sCols.map(vvStreamCol).join('')}</div>${overflow}`;
}

function vvPollStreams() {
  fetch('/plugins/varaverk/api/media.php')
    .then(r => r.json())
    .then(d => {
      vvLastSessions      = d.sessions ?? [];
      vvLastStreamNames   = d.server_names ?? [];
      vvStreamServerCount = d.server_count ?? 0;
      vvLastStreamPollAt  = Math.floor(Date.now() / 1000);

      const el = document.getElementById('vv-streams-body');
      if (vvStreamServerCount === 0) {
        el.innerHTML = '<p class="vv-stream-empty">No media servers detected.<br>'
          + '<span>Add EMBY_API_KEY / JELLYFIN_API_KEY / PLEX_TOKEN to master.conf to configure.</span></p>';
        return;
      }
      vvRenderStreams();
    })
    .catch(() => {});
}

vvPollStreams();
setInterval(vvPollStreams, 12000);
setInterval(vvRenderStreams, 1000);

// ── Pools card ────────────────────────────────────────────────────────────────

function vvTogglePool(name) {
  vvPoolsOpen[name] = !vvPoolsOpen[name];
  document.getElementById('vv-storage-body').innerHTML = vvRenderPools(vvLastStorageDisks);
}

function vvTogglePoolGroup(groupName) {
  vvPoolGroupOpen[groupName] = !vvPoolGroupOpen[groupName];
  document.getElementById('vv-storage-body').innerHTML = vvRenderPools(vvLastStorageDisks);
}

function vvRenderPools(disks) {
  if (!disks.length) return '<p style="color:#555;font-style:italic;font-size:12px;">No pools found</p>';

  // Group drives by pool name
  const poolMap = {};
  disks.forEach(d => {
    const n = d.name;
    if (!poolMap[n]) poolMap[n] = [];
    poolMap[n].push(d);
  });

  // Build super-groups: pools whose names share a common prefix + trailing digits
  const superGroupMap = {}; // groupName -> [poolName, ...]
  Object.keys(poolMap).forEach(poolName => {
    const match = poolName.match(/^(.+?)(\d+)$/);
    const gName = match ? match[1] : poolName;
    if (!superGroupMap[gName]) superGroupMap[gName] = [];
    superGroupMap[gName].push(poolName);
  });

  // Render a single pool row (with optional drive-expand toggle)
  function renderPoolRow(poolName, drives) {
    const isOpen      = !!vvPoolsOpen[poolName];
    const multi       = drives.length > 1;
    const totalGb     = drives.reduce((s, d) => s + (d.size_gb ?? 0), 0);
    const usedGb      = drives.reduce((s, d) => s + (d.used_gb ?? 0), 0);
    const pct         = totalGb > 0 ? Math.round(usedGb / totalGb * 100) : 0;
    const pctColor    = pct >= (vvThresholds.util_crit ?? 90) ? '#f44336' : pct >= (vvThresholds.util_warn ?? 70) ? '#ff9800' : '#4caf50';
    const temps       = drives.map(d => d.temp).filter(t => t != null);
    const maxTemp     = temps.length ? Math.max(...temps) : null;
    const tempColor   = maxTemp != null ? vvTempColor(maxTemp, drives[0].transport) : '#444';
    const allOk       = drives.every(d => d.status === 'DISK_OK' || !d.status);
    const statusColor = allOk ? '#aaa' : '#f44336';
    const sName       = poolName.replace(/\\/g,'\\\\').replace(/'/g,"\\'");

    let html = `<div style="margin-bottom:8px;">
      <div style="display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:3px;
                  ${multi ? 'cursor:pointer;' : ''}" ${multi ? `onclick="vvTogglePool('${sName}')"` : ''}>
        <span style="color:${statusColor};display:flex;align-items:center;gap:5px;">
          ${multi ? `<span style="color:#555;font-size:9px;width:8px;">${isOpen ? '▾' : '▸'}</span>` : '<span style="width:8px;display:inline-block;"></span>'}
          ${poolName}
          ${multi ? `<span style="font-size:9px;color:#444;background:#1a1a1a;padding:0 4px;border-radius:2px;">${drives.length} drives</span>` : ''}
        </span>
        <span style="display:flex;align-items:center;gap:8px;">
          ${maxTemp != null ? `<span style="color:${tempColor};font-size:10px;">${maxTemp}°</span>` : ''}
          <span style="color:#555;font-size:10px;">${vvFmt(usedGb)} / ${vvFmt(totalGb)}</span>
        </span>
      </div>
      <div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
        <div style="width:${pct}%;height:100%;background:${pctColor};border-radius:3px;transition:width 0.4s;"></div>
      </div>
    </div>`;

    if (multi && isOpen) {
      html += `<div style="padding-left:12px;border-left:1px solid #252525;margin-bottom:8px;margin-top:-4px;">`;
      drives.forEach(d => { html += vvDiskRow({ ...d, name: d.device || d.name }); });
      html += `</div>`;
    }
    return html;
  }

  let html = '';
  Object.entries(superGroupMap).forEach(([groupName, poolNames]) => {
    if (poolNames.length === 1) {
      // Single pool — render directly, no super-group header
      html += renderPoolRow(poolNames[0], poolMap[poolNames[0]]);
    } else {
      // Super-group: aggregate header + expandable list of individual pools
      const isGroupOpen = !!vvPoolGroupOpen[groupName];
      const sGroup      = groupName.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
      const allDrives   = poolNames.flatMap(n => poolMap[n]);
      const totalGb     = allDrives.reduce((s, d) => s + (d.size_gb ?? 0), 0);
      const usedGb      = allDrives.reduce((s, d) => s + (d.used_gb ?? 0), 0);
      const pct         = totalGb > 0 ? Math.round(usedGb / totalGb * 100) : 0;
      const pctColor    = pct >= (vvThresholds.util_crit ?? 90) ? '#f44336' : pct >= (vvThresholds.util_warn ?? 70) ? '#ff9800' : '#4caf50';
      const temps       = allDrives.map(d => d.temp).filter(t => t != null);
      const maxTemp     = temps.length ? Math.max(...temps) : null;
      const tempColor   = maxTemp != null ? vvTempColor(maxTemp, allDrives[0].transport) : '#444';

      html += `<div style="margin-bottom:${isGroupOpen ? '2' : '8'}px;">
        <div style="display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:3px;cursor:pointer;"
             onclick="vvTogglePoolGroup('${sGroup}')">
          <span style="color:#aaa;display:flex;align-items:center;gap:5px;">
            <span style="color:#555;font-size:9px;width:8px;">${isGroupOpen ? '▾' : '▸'}</span>
            ${groupName}
            <span style="font-size:9px;color:#444;background:#1a1a1a;padding:0 4px;border-radius:2px;">${poolNames.length} pools</span>
          </span>
          <span style="display:flex;align-items:center;gap:8px;">
            ${maxTemp != null ? `<span style="color:${tempColor};font-size:10px;">${maxTemp}°</span>` : ''}
            <span style="color:#555;font-size:10px;">${vvFmt(usedGb)} / ${vvFmt(totalGb)}</span>
          </span>
        </div>
        <div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
          <div style="width:${pct}%;height:100%;background:${pctColor};border-radius:3px;transition:width 0.4s;"></div>
        </div>
      </div>`;

      if (isGroupOpen) {
        html += `<div style="padding-left:12px;border-left:1px solid #252525;margin-bottom:8px;margin-top:2px;">`;
        poolNames.forEach(poolName => { html += renderPoolRow(poolName, poolMap[poolName]); });
        html += `</div>`;
      }
    }
  });

  return html;
}

// ── System clock tick (updates time every 30s without a full poll) ────────────
function vvTickClock() {
  const el = document.getElementById('vv-system-body');
  if (!el) return;
  const now = new Date();
  const timeStr = now.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
  const t = el.querySelector('.vv-clock');
  if (t) t.textContent = timeStr;
}
setInterval(vvTickClock, 30000);

// ── Containers and VMs card ───────────────────────────────────────────────────

let vvDfFolderOpen = {};
let vvDfActive     = null;
let vvDfData       = null;

function vvToggleFolder(id) {
  vvDfFolderOpen[id] = !vvDfFolderOpen[id];
  vvRenderDockerFolders(vvDfData);
}

function vvToggleContainer(name) {
  vvDfActive = vvDfActive === name ? null : name;
  vvRenderDockerFolders(vvDfData);
}

function vvDockerAction(action, name, webui) {
  if (action === 'webui') { window.open(webui, '_blank'); return; }
  if (action === 'edit') {
    window.location.href = '/Docker?action=template&xmlTemplate=' +
      encodeURIComponent('/boot/config/plugins/dockerMan/templates-user/my-' + name + '.xml') + '&update=true';
    return;
  }
  const fd = new FormData();
  fd.set('action', action);
  fd.set('name',   name);
  fetch('/plugins/varaverk/api/docker_action.php', { method: 'POST', body: fd })
    .then(r => r.json())
    .then(() => { vvDfActive = null; setTimeout(vvPollMonitor, 1500); })
    .catch(() => {});
}

function vvRenderDockerFolders(data) {
  vvDfData = data;
  const el = document.getElementById('vv-docker-folders-body');
  if (!el || !data) return;

  // Update container count badge in header
  const allCtrs   = [...(data.folders ?? []).flatMap(f => f.containers), ...(data.ungrouped ?? [])];
  const totalCtrs = allCtrs.length;
  const runCtrs   = allCtrs.filter(c => c.running).length;
  const countEl   = document.getElementById('vv-docker-count');
  if (countEl && totalCtrs > 0) countEl.textContent = `${runCtrs}/${totalCtrs}`;

  const osIcon     = os => ({ windows:'🪟', macos:'🍎', bsd:'🦬' })[os] ?? '🐧';
  const stateColor = s  => ({ running:'#4caf50', paused:'#ff9800' })[s] ?? '#444';
  const stateLabel = s  => ({ running:'Running', paused:'Paused', 'shut off':'Off', crashed:'Crashed' })[s] ?? s;

  // ── VMs section ─────────────────────────────────────────────────────────────
  let html = '';
  const vms = data.vms?.vms ?? [];
  if (!vms.length) {
    html += '<div class="vv-df-empty">No VMs configured</div>';
  } else {
    vms.forEach(vm => {
      const sc    = stateColor(vm.state);
      const pulse = vm.state === 'running' ? 'animation:vv-pulse-dot 1s ease-in-out infinite;' : '';
      const parts = [];
      if (vm.vcpus)  parts.push(vm.vcpus + ' vCPU');
      if (vm.mem_mb) parts.push(vm.mem_mb >= 1024 ? (vm.mem_mb/1024).toFixed(0)+' GB' : vm.mem_mb+' MB');
      const meta = parts.length ? `<span class="vv-df-vm-meta"> · ${parts.join(' · ')}</span>` : '';
      html += `<div class="vv-df-vm-row">
        <span class="vv-df-vm-icon">${osIcon(vm.os)}</span>
        <span style="width:7px;height:7px;border-radius:50%;background:${sc};flex-shrink:0;${pulse}"></span>
        <span class="vv-df-cname">${vm.name}</span>
        <span style="font-size:11px;font-weight:600;color:${sc};flex-shrink:0;">${stateLabel(vm.state)}</span>
        ${meta}
      </div>`;
    });
  }

  if (!data.available) {
    html += '<div class="vv-df-empty">Docker not available</div>';
    el.innerHTML = html;
    return;
  }

  function renderContainer(c) {
    const dot      = c.running ? '#4caf50' : '#555';
    const pulse    = c.running ? 'animation:vv-pulse-dot 1s ease-in-out infinite;' : '';
    const active   = vvDfActive === c.name;
    const sShort   = c.status ? c.status.replace(/^Up\s+/, '').split(' ').slice(0,2).join(' ') : '—';
    const sn       = c.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
    const sw       = (c.webui||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'");

    let actionBar = '';
    if (active) {
      const ta = c.running ? 'stop' : 'start';
      const tl = c.running ? '⏹ Stop' : '▶ Start';
      const tc = c.running ? '#f44336' : '#4caf50';
      const ws = c.webui ? '' : 'opacity:0.3;pointer-events:none;';
      actionBar = `<div class="vv-df-actions">
        <button onclick="event.stopPropagation();vvDockerAction('${ta}','${sn}','')"
          class="vv-btn-sm" style="border-color:${tc};color:${tc};">${tl}</button>
        <button onclick="event.stopPropagation();vvDockerAction('webui','${sn}','${sw}')"
          class="vv-btn-sm vv-run-btn" style="${ws}">🌐 WebUI</button>
        <button onclick="event.stopPropagation();vvDockerAction('edit','${sn}','')"
          class="vv-btn-sm vv-edit-btn">✎ Edit</button>
      </div>`;
    }
    return `<div class="vv-df-container${active ? ' vv-df-active' : ''}"
                 onclick="event.stopPropagation();vvToggleContainer('${sn}')">
      <span class="vv-df-dot" style="background:${dot};${pulse}"></span>
      <span class="vv-df-cname">${c.name}</span>
      <span class="vv-df-status">${sShort}</span>
    </div>${actionBar}`;
  }

  function renderFolder(f) {
    const open    = !!vvDfFolderOpen[f.id];
    const total   = f.containers.length;
    const running = f.containers.filter(c => c.running).length;
    const bColor  = running === total ? '#4caf50' : running === 0 ? '#555' : '#ff9800';
    const badge   = `<span style="font-size:10px;color:${bColor};flex-shrink:0;margin-left:auto;">${running}/${total}</span>`;
    const sid     = f.id.replace(/\\/g,'\\\\').replace(/'/g,"\\'");

    let iconHtml = '';
    if (f.isEmoji) {
      iconHtml = `<span style="font-size:11px;flex-shrink:0;">${f.icon}</span>`;
    } else if (f.icon) {
      iconHtml = `<img src="${f.icon}" style="width:13px;height:13px;object-fit:contain;border-radius:2px;flex-shrink:0;" onerror="this.style.display='none'">`;
    }

    let out = `<div class="vv-df-folder">
      <div class="vv-df-folder-hdr" onclick="vvToggleFolder('${sid}')">
        <span class="vv-df-chevron">${open ? '▾' : '▸'}</span>
        ${iconHtml}
        <span class="vv-df-fname">${f.name}</span>
        ${badge}
      </div>`;
    if (open) {
      out += '<div class="vv-df-folder-body">';
      f.containers.forEach(c => { out += renderContainer(c); });
      out += '</div>';
    }
    out += '</div>';
    return out;
  }

  // Build folder list — ungrouped gets a 📁 emoji icon
  const allFolders = (data.folders ?? []).map(f => ({ ...f, isEmoji: false }));
  const ug = data.ungrouped ?? [];
  if (ug.length) allFolders.push({ id:'__ungrouped__', name:'Ungrouped', icon:'📁', isEmoji:true, containers:ug });

  if (!allFolders.length) {
    html += '<div class="vv-df-empty">No containers found</div>';
    el.innerHTML = html;
    return;
  }

  // Column count: 3 big / 2 intermediate / 1 small
  const _w    = window.innerWidth;
  const _cols = _w > 1400 ? 3 : _w > 640 ? 2 : 1;

  if (_cols === 1) {
    html += `<div class="vv-df-col">${allFolders.map(renderFolder).join('')}</div>`;
  } else {
    const perCol = Math.ceil(allFolders.length / _cols);
    const colDivs = Array.from({length: _cols}, (_, i) =>
      `<div class="vv-df-col">${allFolders.slice(i * perCol, (i + 1) * perCol).map(renderFolder).join('')}</div>`
    ).join('');
    html += `<div class="vv-df-cols">${colDivs}</div>`;
  }

  el.innerHTML = html;
}

// Close action bar when clicking outside the card
document.addEventListener('click', () => {
  if (vvDfActive !== null) { vvDfActive = null; vvRenderDockerFolders(vvDfData); }
});

// ── Array actions ─────────────────────────────────────────────────────────────
function vvArrayAction(action) {
  const labels = {stop: 'Stop Array', shutdown: 'Shutdown', restart: 'Restart'};
  if (!confirm(`${labels[action] ?? action} — are you sure?`)) return;
  fetch('/plugins/varaverk/api/system.php', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({action}),
  }).then(r => r.json()).then(d => {
    if (!d.ok) alert('Error: ' + (d.error ?? 'unknown'));
  }).catch(() => alert('Request failed'));
}
</script>
