<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
//   Monitor tab. The main dashboard — CPU per core, memory, GPUs, disks and pools, network,
//   UPS, VMs, containers, transcodes, media sessions, and the watchdog health roll-up, for
//   this host and every partner.
//
// DESIGN PRINCIPLES
//   Two poll rates, deliberately split.
//       api/monitor_fast.php carries the cheap, fast-moving values (1–2s); api/monitor.php
//       carries the full payload on a slower cycle. Everything refreshing at the fast rate
//       would put real load on the WebGUI this page exists to watch.
//
//   Served from the tmpfs cache, not live calls.
//       api_cache_writer.sh refreshes the payload every minute and the endpoint serves that.
//       ?live=1 bypasses it. A missing cache always falls back to a live call, so the cache
//       can never be why the dashboard fails to load.
//
//   Missing subsystems simply do not render.
//       No GPU, no UPS, no VMs — the corresponding card is absent rather than showing zeros
//       or an error. The page is built to be correct on hardware lacking any given part.
//
// OPERATIONAL SAFEGUARDS
//   The health roll-up must not default to healthy.
//       vv_watchdog_summary() is conjunctive across every strike set, and it reads state from
//       STATE_DIR. Six of those paths once pointed at /tmp, every read returned empty, and
//       the page reported healthy unconditionally (fixed 2026-08-02). If this panel looks
//       suspiciously green, verify the paths before believing it.
//
//   Container actions are confirmed and routed through the action endpoint, which validates
//   against real inventory.
//
//   All remote and container-supplied strings render escaped.
//
// RENDERS
//   System header, CPU per core, memory breakdown, GPU cards, storage pools and array disks,
//   network, UPS, VMs, containers, transcode sessions, media now-playing, watchdog summary,
//   partner node cards
//
// DEPENDS ON
//   include/monitor.php     required directly for initial render
//   api/monitor.php         full payload, slower cycle
//   api/monitor_fast.php    fast-moving values, 1–2s
//   api/system.php          system info
//   api/media.php           now-playing sessions
//   api/docker_action.php   container actions
//   api/flag_toggle.php     toggles
// ═══════════════════════════════════════════════════════════════════════════════════════════════
require_once dirname(__DIR__) . '/include/monitor.php';
?>
<style>
@keyframes vvRsPulse {
  0%,100% { opacity:.5; transform:scaleX(.9); }
  50%     { opacity:1;  transform:scaleX(1.05); }
}
</style>

<div id="vv-api-banner" style="display:none;border-radius:4px;padding:5px 10px;margin-bottom:8px;font-size:11px;"></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:5px;">
          <svg width="9" height="14" viewBox="0 0 11 18" fill="none" style="opacity:0.4;flex-shrink:0;">
            <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 style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="3" y="3" width="6" height="6" rx="0.8"/><line x1="4.5" y1="3" x2="4.5" y2="1.2"/><line x1="7.5" y1="3" x2="7.5" y2="1.2"/><line x1="4.5" y1="9" x2="4.5" y2="10.8"/><line x1="7.5" y1="9" x2="7.5" y2="10.8"/><line x1="3" y1="4.5" x2="1.2" y2="4.5"/><line x1="3" y1="7.5" x2="1.2" y2="7.5"/><line x1="9" y1="4.5" x2="10.8" y2="4.5"/><line x1="9" y1="7.5" x2="10.8" y2="7.5"/></svg></span>
          <span id="vv-cpu-title">CPU</span>
        </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>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="14" height="8" viewBox="0 0 14 8" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.6" y="2.5" width="12.8" height="3" rx="0.5"/><line x1="3" y1="2.5" x2="3" y2="0.8"/><line x1="5.5" y1="2.5" x2="5.5" y2="0.8"/><line x1="8" y1="2.5" x2="8" y2="0.8"/><line x1="10.5" y1="2.5" x2="10.5" y2="0.8"/><line x1="3" y1="5.5" x2="3" y2="7.2"/><line x1="5.5" y1="5.5" x2="5.5" y2="7.2"/><line x1="8" y1="5.5" x2="8" y2="7.2"/><line x1="10.5" y1="5.5" x2="10.5" y2="7.2"/></svg></span>
          Memory
        </span>
        <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>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="13" height="12" viewBox="0 0 13 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="4.5" y="0.5" width="4" height="3" rx="0.7"/><rect x="0.5" y="8" width="4" height="3" rx="0.7"/><rect x="8.5" y="8" width="4" height="3" rx="0.7"/><line x1="6.5" y1="3.5" x2="6.5" y2="6"/><line x1="6.5" y1="6" x2="2.5" y2="6"/><line x1="2.5" y1="6" x2="2.5" y2="8"/><line x1="6.5" y1="6" x2="10.5" y2="6"/><line x1="10.5" y1="6" x2="10.5" y2="8"/></svg></span>
          Network
        </span>
        <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>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="1" width="10" height="10" rx="1.5"/><line x1="1" y1="4.5" x2="11" y2="4.5"/><polyline points="3.5,7 5,8 3.5,9"/><line x1="6" y1="9" x2="8.5" y2="9"/></svg></span>
          Scripts
        </span>
        <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>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="10" height="12" viewBox="0 0 10 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><path d="M5 0.8L9.2 2.8V6.2C9.2 9 5 11.2 5 11.2C5 11.2 0.8 9 0.8 6.2V2.8Z"/></svg></span>
          Fallback
        </span>
      </h3>
      <div id="vv-fallback-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-partner" style="grid-column:span 2;">
      <h3>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.5" y="1.5" width="4" height="7" rx="0.8"/><rect x="9.5" y="1.5" width="4" height="7" rx="0.8"/><line x1="4.5" y1="5" x2="9.5" y2="5"/><circle cx="7" cy="5" r="1" fill="#888" stroke="none"/></svg></span>
          Partner
        </span>
        <img src="/plugins/varaverk/icons/varaverk.png" style="width:16px;height:16px;object-fit:contain;opacity:0.2;flex-shrink:0;" title="Varaverk">
      </h3>
      <div id="vv-partner-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-docker-folders" style="grid-column:span 4;">
      <h3>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="0.5" width="12" height="2.5" rx="0.5"/><rect x="1" y="3.8" width="12" height="2.5" rx="0.5"/><rect x="1" y="7" width="12" height="2.5" rx="0.5"/></svg></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: Rsync | GPU 0 | GPU 1 | Transcode | Streams -->
    <div class="vv-card" id="vv-rsync-card" style="grid-column:span 1;">
      <h3>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><polyline points="2,4 10,4 8,2"/><polyline points="10,8 2,8 4,10"/></svg></span>
          Rsync
        </span>
      </h3>
      <div id="vv-rsync-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-gpu-card" style="grid-column:span 1;">
      <h3>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="15" height="10" viewBox="0 0 16 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.8" y="0.8" width="14.4" height="7" rx="1.2"/><rect x="2.5" y="2.5" width="3.5" height="3.5" rx="0.5"/><line x1="8" y1="3" x2="13" y2="3"/><line x1="8" y1="5" x2="11" y2="5"/><rect x="3" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="6.5" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="10" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/></svg></span>
          <span id="vv-gpu-label">GPU</span>
        </span>
        <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-gpu1-card" style="grid-column:span 1;">
      <h3>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="15" height="10" viewBox="0 0 16 10" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.8" y="0.8" width="14.4" height="7" rx="1.2"/><rect x="2.5" y="2.5" width="3.5" height="3.5" rx="0.5"/><line x1="8" y1="3" x2="13" y2="3"/><line x1="8" y1="5" x2="11" y2="5"/><rect x="3" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="6.5" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/><rect x="10" y="7.8" width="2" height="1.8" rx="0.3" stroke="none" fill="#666"/></svg></span>
          <span id="vv-gpu1-label">GPU 1</span>
        </span>
        <a href="/" target="_blank" class="vv-card-cog" title="Dashboard">⚙</a>
      </h3>
      <div id="vv-gpu1-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-transcode" style="grid-column:span 1;">
      <h3>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="14" height="12" viewBox="0 0 14 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="0.5" y="1" width="4" height="10" rx="0.8"/><line x1="0.5" y1="3.5" x2="2.3" y2="3.5"/><line x1="0.5" y1="8.5" x2="2.3" y2="8.5"/><line x1="2.7" y1="3.5" x2="4.5" y2="3.5"/><line x1="2.7" y1="8.5" x2="4.5" y2="8.5"/><line x1="6" y1="6" x2="9.5" y2="6"/><polyline points="8.2,4.3 10.2,6 8.2,7.7"/><rect x="11" y="1" width="2.5" height="10" rx="0.5"/></svg></span>
          Transcode
        </span>
      </h3>
      <div id="vv-transcode-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-streams" style="grid-column:span 4;">
      <h3>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="11" height="12" viewBox="0 0 11 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><polygon points="1.5,1 1.5,11 10,6"/></svg></span>
          Streams
        </span>
      </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>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="14" height="9" viewBox="0 0 14 8" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><path d="M1 4C3.2 1 5 0.5 7 0.5C9 0.5 10.8 1 13 4C10.8 7 9 7.5 7 7.5C5 7.5 3.2 7 1 4Z"/><circle cx="7" cy="4" r="1.5"/></svg></span>
          Watchdog
        </span>
      </h3>
      <div id="vv-watchdog-body">Loading...</div>
    </div>

    <div class="vv-card" id="vv-parity-card" style="grid-column:span 1;">
      <h3>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="5"/><polyline points="3.5,6 5.5,8 8.5,4"/></svg></span>
          Parity
        </span>
        <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>
        <span style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><ellipse cx="6" cy="3" rx="4.5" ry="1.5"/><line x1="1.5" y1="3" x2="1.5" y2="9"/><line x1="10.5" y1="3" x2="10.5" y2="9"/><ellipse cx="6" cy="9" rx="4.5" ry="1.5"/></svg></span>
          <span id="vv-pools-title">Pools</span>
        </span>
        <span id="vv-pools-io-total" style="font-size:9px;font-weight:400;color:#333;text-transform:none;letter-spacing:0;"></span>
        <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 style="display:flex;align-items:center;gap:5px;">
          <span class="vv-ico"><svg width="14" height="12" viewBox="0 0 14 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><rect x="0.7" y="0.7" width="12.6" height="3" rx="0.8"/><rect x="0.7" y="4.5" width="12.6" height="3" rx="0.8"/><rect x="0.7" y="8.3" width="12.6" height="3" rx="0.8"/><circle cx="11.5" cy="2.2" r="0.8" fill="#888" stroke="none"/><circle cx="11.5" cy="6" r="0.8" fill="#888" stroke="none"/><circle cx="11.5" cy="9.8" r="0.8" fill="#888" stroke="none"/></svg></span>
          <span id="vv-array-title">Array</span>
        </span>
        <span id="vv-array-io-total" style="font-size:9px;font-weight:400;color:#333;text-transform:none;letter-spacing:0;"></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 vvDiskIo         = {}; // {device: [readMBs, writeMBs]}
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.arc_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` : '';
    const sid   = (s.id || '').replace(/\\/g,'\\\\').replace(/'/g,"\\'");
    listHtml += `<div onclick="vvOpenScript('${sid}')" title="Open ${name} in Scheduler"
      style="display:flex;align-items:center;gap:5px;margin-bottom:4px;font-size:11px;cursor:pointer;"
      onmouseover="this.style.background='#161616'" onmouseout="this.style.background=''">
      <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;scrollbar-width:none;-ms-overflow-style:none;">${listHtml}</div>`;
  const el = document.getElementById('vv-scripts-body');
  if (el) el.innerHTML = html;
}

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

// Deep-link a script row to the Scheduler tab (opens its settings/log panel there).
function vvOpenScript(id) {
  if (!id) { window.location.href = '?tab=scheduler'; return; }
  window.location.href = '?tab=scheduler&vv_script=' + encodeURIComponent(id);
}

// ── 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 vvFmtRate(mbs) {
  if (mbs >= 1000) return (mbs / 1024).toFixed(1) + ' GB/s';
  if (mbs >= 100)  return Math.round(mbs) + ' MB/s';
  return mbs.toFixed(1) + ' MB/s';
}
function vvIoChip(dev) {
  const io = vvDiskIo[dev];
  if (!io) return '';
  const r = io.r ?? 0, w = io.w ?? 0;
  if (r < 0.05 && w < 0.05) return '';
  const parts = [];
  if (r >= 0.05) parts.push(`<span style="color:#3a7a3a;">↓${vvFmtRate(r)}</span>`);
  if (w >= 0.05) parts.push(`<span style="color:#7a4a1a;">↑${vvFmtRate(w)}</span>`);
  return `<span style="font-size:9px;margin-left:5px;">${parts.join(' ')}</span>`;
}
function vvIoSum(devices) {
  let r = 0, w = 0;
  devices.forEach(dev => { const io = vvDiskIo[dev]; if (io) { r += io.r ?? 0; w += io.w ?? 0; } });
  return [r, w];
}
function vvWdRsyncToggle(el) {
  const flag = el.dataset.flag;
  const on   = el.dataset.enabled !== '1';
  el.dataset.enabled = on ? '1' : '0';
  el.style.color      = on ? '#4caf50' : '#333';
  el.style.background = on ? '#0f1a0f' : '#111';
  el.style.borderColor = on ? '#1a3a1a' : '#222';
  const fd = new FormData();
  fd.append('name', flag);
  fd.append('enabled', on ? '1' : '0');
  fetch('/plugins/varaverk/api/flag_toggle.php', { method: 'POST', body: fd })
    .then(r => r.json())
    .then(d => {
      if (!d.ok) {
        el.dataset.enabled = on ? '0' : '1';
        el.style.color      = on ? '#333' : '#4caf50';
        el.style.background = on ? '#111' : '#0f1a0f';
        el.style.borderColor = on ? '#222' : '#1a3a1a';
      }
    })
    .catch(() => {
      el.dataset.enabled = on ? '0' : '1';
      el.style.color      = on ? '#333' : '#4caf50';
      el.style.background = on ? '#111' : '#0f1a0f';
      el.style.borderColor = on ? '#222' : '#1a3a1a';
    });
}
function vvIoTotalSum(devices) {
  let tr = 0, tw = 0;
  devices.forEach(dev => { const io = vvDiskIo[dev]; if (io) { tr += io.tr ?? 0; tw += io.tw ?? 0; } });
  return [tr, tw];
}
function vvFmtGb(gb) {
  if (gb >= 1024) return (gb / 1024).toFixed(1) + ' TB';
  if (gb >= 1)    return gb.toFixed(1) + ' GB';
  return (gb * 1024).toFixed(0) + ' MB';
}

function vvDiskRow(disk) {
  const tempC     = disk.temp;
  const tempColor = vvTempColor(tempC, disk.transport);
  const tempStr   = tempC !== null ? `${tempC}°` : '—';
  const isParity  = disk.role === 'parity';
  const failed    = !isParity && disk.status && disk.status !== 'DISK_OK';
  const nameColor = isParity ? '#6a8faf' : failed ? '#f44336' : '#aaa';
  const pct       = disk.pct ?? 0;
  const barColor  = isParity ? '#1e3a5a'
    : failed ? '#f44336'
    : 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 failLabel = failed ? `<span style="color:#f44336;font-size:9px;margin-left:4px;">${disk.status === 'DISK_DSBL' ? 'emulated' : disk.status.replace('DISK_','').toLowerCase()}</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};display:flex;align-items:center;">${disk.name}${spinLabel}${failLabel}${vvIoChip(disk.device)}</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 ──────────────────────────────────────────────────────────────────────

let _vvFirstPoll = true;

function vvPollMonitor() {
  const _url = _vvFirstPoll ? '/plugins/varaverk/api/monitor.php' : '/plugins/varaverk/api/monitor.php?live=1';
  _vvFirstPoll = false;
  fetch(_url)
    .then(r => r.json())
    .then(d => {

      // ── API status banner ────────────────────────────────────────────────────
      const apiStatus = d._api_status ?? {};
      const banner    = document.getElementById('vv-api-banner');
      if (banner) {
        const fallbacks = apiStatus.fallbacks ?? [];
        if (fallbacks.length === 0) {
          banner.style.display = 'none';
        } else if (apiStatus.key_missing) {
          // Key not configured — informational, not alarming
          Object.assign(banner.style, {display:'', background:'#111', border:'1px solid #2a2a2a', color:'#555'});
          banner.textContent = 'API key not configured — add HOST' +
            (typeof vvLocalHostConf !== 'undefined' ? vvLocalHostConf.replace(/\D/g,'') : '1') +
            '_UNRAID_API_KEY in Scheduler → host conf to enable enhanced monitoring.';
        } else {
          // Key present but API unreachable — genuine problem
          Object.assign(banner.style, {display:'', background:'#1a1200', border:'1px solid #3a2800', color:'#ff9800'});
          banner.innerHTML = '⚠ Unraid API unreachable — using local reads. Check API key in host conf.' +
            (fallbacks.length ? ' <span style="color:#666">(' + fallbacks.join(', ') + ')</span>' : '');
        }
      }

      // ── 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` : '';

      // Load average (1/5/15m) — colour by load[0] vs core count
      const _load      = Array.isArray(sys.load_avg) ? sys.load_avg : null;
      const _cores     = sys.cpu_cores || 0;
      const _loadColor = _load && _cores
        ? (_load[0] > _cores * 2 ? '#f44336' : _load[0] > _cores ? '#ff9800' : '#888') : '#888';
      const _loadStr   = _load
        ? `${_load[0].toFixed(2)} <span style="color:#444;">·</span> ${_load[1].toFixed(2)} <span style="color:#444;">·</span> ${_load[2].toFixed(2)}`
        : '—';
      const _coreMeta  = _threadInfo ? ` <span style="color:#444;">(${_threadInfo})</span>` : '';

      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}${_coreMeta}</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;">Load</span>     <span style="color:${_loadColor};">${_loadStr}</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('');

        // Onboard phase badge for remote hosts not yet fully onboarded
        const phase = h.onboard_phase ?? null;
        const onboardBadge = (!h.is_me && phase !== null && phase < 2)
          ? (phase === 1
              ? `<div style="font-size:9px;color:#ff9800;margin-top:3px;padding:2px 5px;background:#1a1200;border:1px solid #3a2800;border-radius:3px;display:inline-block;">⏳ Awaiting onboard</div>`
              : `<div style="font-size:9px;color:#444;margin-top:3px;padding:2px 5px;background:#111;border:1px solid #222;border-radius:3px;display:inline-block;">○ Not provisioned</div>`)
          : '';

        // 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}%${rs.cpu_threads ? `<span style="color:#333;font-weight:400;"> · ${rs.cpu_threads}t</span>` : ''}</span>` : `<span style="color:#333;">—</span>`;
          const ramStr    = ramAvail ? `<span style="color:hsl(${memHue},70%,45%);font-weight:600;">${rs.mem_used_pct}%${rs.mem_total_gb ? `<span style="color:#333;font-weight:400;"> · ${rs.mem_total_gb}G</span>` : ''}</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>` : '';

          const verRow = remoteVer ? `<span style="color:#444;">unRAID</span><span style="color:#3a3a3a;grid-column:span 3;">${remoteVer}</span>` : '';
          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>
            ${verRow}
          </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>
              ${onboardBadge}
            </div>
            <span style="color:${dot};font-size:10px;white-space:nowrap;">● ${label}</span>
          </div>
          ${statsHtml}
        </div>`;
      });
      document.getElementById('vv-partner-body').innerHTML = ptHtml;
      (function() {
        const c = document.getElementById('vv-partner');
        if (!c) return;
        const remotes = ptHosts.filter(h => !h.is_me);
        c.classList.remove('vv-accent-ok','vv-accent-warn');
        if (remotes.length) {
          if (remotes.some(h => h.online)) c.classList.add('vv-accent-ok');
          else c.classList.add('vv-accent-warn');
        }
      })();

      // ── Fallback ──────────────────────────────────────────────────────────────
      (function() {
        const fb       = d.fallback ?? {};
        const fbActive = d.fallback_active ?? [];
        const state    = (fb.state ?? 'UNKNOWN').toUpperCase();
        const enabled  = fb.enabled ?? false;
        const strikes  = fb.handback_strikes ?? 0;
        const maxStrikes = fb.handback_strikes_required ?? 3;
        const ptSuspended = fb.partnership_suspended ?? false;
        const partnerLostAt = fb.partner_lost_at ?? 0;
        const suspendAfter = fb.partnership_suspend_after ?? 120;
        const interval = fb.check_interval ?? 30;
        const ptRequired = fb.partnership_required ?? false;

        const stateMap = {
          NORMAL:      ['#4caf50', '✓ Nominal'],
          FALLBACK:    ['#f44336', '⚠ Fallback active'],
          NO_INTERNET: ['#ff9800', '⚡ Internet lost'],
          DARK:        ['#9e9e9e', '◌ Dark mode'],
          UNKNOWN:     ['#444',    '— No state file'],
        };
        const [sColor, sLabel] = stateMap[state] ?? ['#444', state];

        // ── Disabled ──────────────────────────────────────────────────────────
        if (!enabled) {
          document.getElementById('vv-fallback-body').innerHTML =
            `<div style="font-size:11px;color:#444;margin-bottom:6px;">○ Disabled</div>
             <div style="font-size:10px;color:#333;">Set FALLBACK_ENABLED=true in master.conf to activate</div>`;
          return;
        }

        // ── Suspended (partnership gate) ───────────────────────────────────────
        if (ptSuspended) {
          const lostMin = partnerLostAt > 0 ? Math.floor((Date.now()/1000 - partnerLostAt) / 60) : '?';
          document.getElementById('vv-fallback-body').innerHTML =
            `<div style="font-size:11px;color:#555;margin-bottom:6px;">⊘ Suspended</div>
             <div style="font-size:10px;color:#444;margin-bottom:3px;">Partnership inactive · ${lostMin}m elapsed</div>
             <div style="font-size:10px;color:#333;">Resumes when partnership becomes active</div>`;
          return;
        }

        let fbHtml = '';

        // ── State label + outage duration ──────────────────────────────────────
        let stateExtra = '';
        if (state === 'FALLBACK' || state === 'DARK') {
          const start = parseInt(fb.failover_start ?? 0);
          if (start > 0) {
            const sec = Math.floor(Date.now() / 1000) - start;
            stateExtra = ` · ${Math.floor(sec/3600)}h ${Math.floor((sec%3600)/60)}m`;
          }
        }
        fbHtml += `<div style="font-size:11px;color:${sColor};font-weight:500;margin-bottom:6px;">${sLabel}${stateExtra}</div>`;

        // ── Handback strike progress ───────────────────────────────────────────
        if (state === 'FALLBACK' && strikes > 0) {
          const dots = Array.from({length: maxStrikes}, (_,i) =>
            `<span style="color:${i < strikes ? '#4caf50' : '#2a2a2a'};font-size:14px;line-height:1;">●</span>`
          ).join('');
          fbHtml += `<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
            <span style="font-size:10px;color:#555;">Handback</span>
            <span style="display:flex;gap:3px;">${dots}</span>
            <span style="font-size:10px;color:#555;">${strikes}/${maxStrikes}</span>
          </div>`;
        }

        // ── Tier badges (FAILOVER only) ────────────────────────────────────────
        if (state === 'FALLBACK') {
          const t2 = fb.tier2_started === 'true';
          const t3 = fb.tier3_started === 'true';
          const t4 = fb.tier4_started === 'true';
          const tier = (n, on) => `<span style="font-size:9px;padding:1px 6px;border-radius:3px;
            background:${on ? '#1a2a1a' : '#1a1a1a'};
            border:1px solid ${on ? '#2a5a2a' : '#252525'};
            color:${on ? '#4caf50' : '#333'};">T${n}</span>`;
          fbHtml += `<div style="display:flex;gap:4px;margin-bottom:8px;">
            ${tier(1,true)} ${tier(2,t2)} ${tier(3,t3)} ${tier(4,t4)}
          </div>`;
        }

        // ── Active containers ──────────────────────────────────────────────────
        if (fbActive.length) {
          fbActive.forEach(group => {
            fbHtml += `<div style="font-size:10px;color:#555;margin-bottom:4px;letter-spacing:.03em;">
              COVERING ${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:3px;">
                <span style="color:#ccc;font-size:11px;font-weight:500;">${c.name}</span>
                <span style="color:#444;font-size:9px;margin-left:8px;white-space:nowrap;">${img}</span>
              </div>`;
            });
          });
        }

        // ── NORMAL quiet state ─────────────────────────────────────────────────
        if (state === 'NORMAL' && !fbActive.length) {
          let meta = `monitoring · ${interval}s`;
          if (ptRequired) meta += ' · partnership gated';
          fbHtml += `<div style="font-size:10px;color:#2a2a2a;">${meta}</div>`;
        }

        // ── NO_INTERNET / DARK detail ──────────────────────────────────────────
        if (state === 'NO_INTERNET') {
          fbHtml += `<div style="font-size:10px;color:#555;">DDNS stopped · awaiting recovery</div>`;
        } else if (state === 'DARK') {
          fbHtml += `<div style="font-size:10px;color:#555;">Remote + internet both down</div>`;
        }

        document.getElementById('vv-fallback-body').innerHTML = fbHtml;

        // Status accent border
        const fbCard = document.getElementById('vv-fallback');
        if (fbCard) {
          fbCard.classList.remove('vv-accent-ok','vv-accent-warn','vv-accent-err');
          if (!enabled || ptSuspended)    fbCard.classList.add('vv-accent-warn');
          else if (state === 'NORMAL')    fbCard.classList.add('vv-accent-ok');
          else if (state === 'FALLBACK')  fbCard.classList.add('vv-accent-err');
          else if (state !== 'UNKNOWN')   fbCard.classList.add('vv-accent-warn');
        }
      })();

      // ── CPU title (core count + temp — watchdog data only comes from full poll) ──
      const _cpuTitleEl = document.getElementById('vv-cpu-title');
      if (_cpuTitleEl) {
        const _cpuTemp  = d.watchdog?.stability?.cpu_temp ?? null;
        const _tColor   = _cpuTemp == null ? '#888' : _cpuTemp >= 88 ? '#f44336' : _cpuTemp >= 75 ? '#ff9800' : '#4caf50';
        const _coreLbl  = sys.cpu_threads ? `CPU · ${sys.cpu_cores}c/${sys.cpu_threads}t` : 'CPU';
        _cpuTitleEl.innerHTML = _coreLbl
          + (_cpuTemp != null ? ` <span style="color:${_tColor};font-weight:400;">${_cpuTemp}°</span>` : '');
      }

      // ── 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;

        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 numDisabled  = par.num_disabled ?? 0;
        const numMissing   = par.num_missing  ?? 0;
        const degraded     = numDisabled > 0 || numMissing > 0;
        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' : degraded || (par.errors ?? 0) > 0 ? 'vv-banner-warn' : 'vv-banner-ok';
        const emulLabel    = numDisabled > 0 ? `<span style="font-size:11px;">Emulating ${numDisabled} disk${numDisabled !== 1 ? 's' : ''}</span>` : '';
        const missingLabel = numMissing  > 0 ? `<span style="font-size:11px;color:#f44336;">${numMissing} slot${numMissing !== 1 ? 's' : ''} missing</span>` : '';

        let html = `<div class="vv-banner ${parBannerCls}">
          <span>${valid ? '✓ Valid' : '✗ INVALID'}</span>
          ${emulLabel}${missingLabel}
          ${(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;
          const action = par.resync_action ?? '';
          let opLabel;
          if (/^recon/i.test(action)) {
            const disk = action.replace(/^recon\s*/i, '').trim();
            opLabel = 'Rebuilding' + (disk ? ' disk ' + disk : '');
          } else if (/^check/i.test(action)) {
            opLabel = 'Parity check';
          } else if (/^sync/i.test(action)) {
            opLabel = 'Parity sync';
          } else if (/^clear/i.test(action)) {
            const disk = action.replace(/^clear\s*/i, '').trim();
            opLabel = 'Clearing' + (disk ? ' disk ' + disk : '');
          } else {
            opLabel = action || 'Operation';
          }
          html += `<div style="font-size:11px;color:#aaa;margin-bottom:4px;">${opLabel} — ${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>
            ${numDisabled > 0 ? `<span style="color:#555;">Emulating</span><span style="color:#ff9800;">${numDisabled} disk${numDisabled !== 1 ? 's' : ''}</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 _wdCores  = sys.cpu_cores || 0;
        const loadColor = _wdCores > 0
          ? (load > _wdCores * 2 ? '#f44336' : load > _wdCores ? '#ff9800' : '#4caf50')
          : (load > 6 ? '#f44336' : load > 3 ? '#ff9800' : '#4caf50');
        const nicOk     = (stab.nic_state ?? '') === 'up';
        const sshdOk    = stab.sshd_ok ?? true;
        const zombies   = stab.zombies ?? 0;

        const uptimeSec   = sys.uptime_sec ?? 0;
        const uptimeDays  = Math.floor(uptimeSec / 86400);
        const uptimeHrs   = Math.floor((uptimeSec % 86400) / 3600);
        const uptimeStr   = uptimeDays > 0 ? `${uptimeDays}d ${uptimeHrs}h` : `${uptimeHrs}h`;
        const uptimeColor = uptimeDays === 0 ? '#ff9800' : '#4caf50';
        const stabCount   = stabNames.length;

        const fdOpen  = stab.fd_open ?? 0;
        const fdPct   = stab.fd_pct  ?? 0;
        const fdStr   = fdOpen >= 1e6 ? (fdOpen/1e6).toFixed(1)+'M' : fdOpen >= 1000 ? (fdOpen/1000).toFixed(1)+'k' : String(fdOpen);
        const fdColor = fdPct >= 50 ? '#f44336' : fdPct >= 20 ? '#ff9800' : '#4caf50';

        const cpuRow = stab.cpu_temp != null
          ? `<span style="color:#444;">CPU</span><span style="color:${wdPct(stab.cpu_temp,75,90)};">${stab.cpu_temp}°C</span>` : '';
        let statsHtml = `<div style="display:grid;grid-template-columns:auto 1fr 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>
          ${cpuRow}
          <span style="color:#444;">Zombies</span><span style="color:${zombies>0?'#ff9800':'#4caf50'};">${zombies}</span>
          <span style="color:#444;">FD open</span><span style="color:${fdColor};">${fdStr}</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>
          <span style="color:#444;">Uptime</span><span style="color:${uptimeColor};">${uptimeStr}</span>
          <span style="color:#444;">Reboots</span><span style="color:${reboots>0?'#f44336':'#4caf50'};">${reboots}/12h</span>
          <span style="color:#444;">Strikes</span><span style="color:${stabCount>0?'#ff9800':'#4caf50'};">${stabCount>0?stabCount+' active':'none'}</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;
        const wdCard = document.getElementById('vv-watchdog-card');
        if (wdCard) {
          wdCard.classList.remove('vv-accent-ok','vv-accent-warn','vv-accent-err');
          if (healthy) wdCard.classList.add('vv-accent-ok');
          else if (reboots > 0 || oom > 0 || daemonHit) wdCard.classList.add('vv-accent-err');
          else wdCard.classList.add('vv-accent-warn');
        }
      })();


      // ── Pools ─────────────────────────────────────────────────────────────────
      vvDiskIo = d.disk_io ?? {};
      vvLastStorageDisks = d.storage ?? [];
      (() => {
        const disks     = vvLastStorageDisks;
        const titleEl   = document.getElementById('vv-pools-title');
        if (titleEl && disks.length) {
          const okDisks   = disks.filter(d => d.status === 'DISK_OK' || !d.status).length;
          const totalGb   = disks.reduce((s, d) => s + (d.size_gb ?? 0), 0);
          const usedGb    = disks.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     = disks.map(d => d.temp).filter(t => t != null);
          const maxTemp   = temps.length ? Math.max(...temps) : null;
          const maxTempColor = maxTemp == null ? '#444'
            : maxTemp >= (vvThresholds.ssd_crit ?? 70) ? '#f44336'
            : maxTemp >= (vvThresholds.ssd_warn ?? 60) ? '#ff9800' : '#4caf50';
          const [pIor, pIow] = vvIoSum(disks.map(d => d.device));
          const ioParts = [];
          if (pIor >= 0.05) ioParts.push(`<span style="color:#3a7a3a;">↓${vvFmtRate(pIor)}</span>`);
          if (pIow >= 0.05) ioParts.push(`<span style="color:#7a4a1a;">↑${vvFmtRate(pIow)}</span>`);
          const ioHtml = ioParts.length ? `· <span style="font-weight:400;">${ioParts.join(' ')}</span>` : '';
          titleEl.innerHTML = `Pools
            <span style="font-size:10px;color:#444;font-weight:400;text-transform:none;letter-spacing:0;margin-left:6px;">
              ${okDisks}/${disks.length} ok
              · <span style="color:${pctColor};">${pct}%</span> used
              ${maxTemp != null ? `· <span style="color:${maxTempColor};">max ${maxTemp}°</span>` : ''}
              ${ioHtml}
            </span>`;
          const [ptr, ptw] = vvIoTotalSum(disks.map(d => d.device));
          const totEl = document.getElementById('vv-pools-io-total');
          if (totEl && (ptr > 0.001 || ptw > 0.001))
            totEl.innerHTML = `<span style="color:#3a5a3a;">↓${vvFmtGb(ptr)}</span> <span style="color:#5a3a1a;">↑${vvFmtGb(ptw)}</span>`;
        }
      })();
      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 [arrIor, arrIow] = vvIoSum(arrayDisks.map(d => d.device));
        const arrIoParts = [];
        if (arrIor >= 0.05) arrIoParts.push(`<span style="color:#3a7a3a;">↓${vvFmtRate(arrIor)}</span>`);
        if (arrIow >= 0.05) arrIoParts.push(`<span style="color:#7a4a1a;">↑${vvFmtRate(arrIow)}</span>`);
        const arrIoHtml = arrIoParts.length
          ? `· <span style="font-weight:400;">${arrIoParts.join(' ')}</span>` : '';

        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>` : ''}
            ${arrIoHtml}
          </span>`;

        const [atr, atw] = vvIoTotalSum(arrayDisks.map(d => d.device));
        const arrTotEl = document.getElementById('vv-array-io-total');
        if (arrTotEl && (atr > 0.001 || atw > 0.001))
          arrTotEl.innerHTML = `<span style="color:#3a5a3a;">↓${vvFmtGb(atr)}</span> <span style="color:#5a3a1a;">↑${vvFmtGb(atw)}</span>`;

        const numCols = window.innerWidth <= 1024 ? 2 : 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>';
      }
      (function() {
        const c = document.getElementById('vv-array-card');
        if (!c) return;
        c.classList.remove('vv-accent-ok','vv-accent-err');
        if (sys.array_state === 'STARTED') c.classList.add('vv-accent-ok');
        else if (sys.array_state && sys.array_state !== 'UNKNOWN') c.classList.add('vv-accent-err');
      })();

      // ── Rsync ────────────────────────────────────────────────────────────────
      (function() {
        const rs        = d.rsync ?? {};
        const enabled   = rs.enabled ?? true;
        const windows   = rs.windows ?? {};
        const active    = rs.active  ?? [];
        const lastSync  = rs.last_sync  ?? {};
        const bwSummary = rs.bw_summary ?? {};
        const now       = Math.floor(Date.now() / 1000);

        const el = document.getElementById('vv-rsync-body');
        if (!el) return;

        function _dur(s) {
          if (!s) return '—';
          if (s < 60)   return s + 's';
          if (s < 3600) return Math.floor(s/60) + 'm' + (s%60 ? String(s%60).padStart(2,'0')+'s' : '');
          return Math.floor(s/3600) + 'h' + Math.floor((s%3600)/60) + 'm';
        }
        function _ago(diff) {
          if (diff < 60)    return diff + 's';
          if (diff < 3600)  return Math.floor(diff/60) + 'm';
          if (diff < 86400) return Math.floor(diff/3600) + 'h';
          return Math.floor(diff/86400) + 'd';
        }
        function _fmtBytes(b) {
          if (!b) return '';
          if (b >= 1073741824) return (b/1073741824).toFixed(1) + 'G';
          if (b >= 1048576)    return (b/1048576).toFixed(0) + 'M';
          return (b/1024).toFixed(0) + 'K';
        }

        const gCol = enabled ? '#4caf50' : '#555';

        // ── Header: gate dot + window badges ──────────────────────────────────
        let html = `<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
          <span style="font-size:10px;font-weight:600;color:${gCol};">● ${enabled ? 'ENABLED' : 'DISABLED'}</span>
          <div style="display:flex;gap:3px;">`;
        const _flagMap = {
          critical:     'CRITICAL_RSYNC_ENABLED',
          intermediate: 'INTERMEDIATE_RSYNC_ENABLED',
          daily:        'DAILY_RSYNC_ENABLED',
          weekly:       'WEEKLY_RSYNC_ENABLED',
          monthly:      'MONTHLY_RSYNC_ENABLED',
        };
        [['C','critical'],['I','intermediate'],['D','daily'],['W','weekly'],['M','monthly']].forEach(([s,k]) => {
          const on   = windows[k] ?? false;
          const run  = active.some(a => a.profile?.includes(k));
          const flag = _flagMap[k];
          const bg   = run ? '#1a2a0a' : on ? '#0f1a0f' : '#111';
          const brd  = run ? '#3a6a1a' : on ? '#1a3a1a' : '#222';
          const col  = run ? '#8bc34a' : on ? '#4caf50' : '#333';
          const tog  = flag ? `data-flag="${flag}" data-enabled="${on?'1':'0'}" onclick="vvWdRsyncToggle(this)"` : '';
          const cur  = flag ? 'cursor:pointer;' : '';
          const tip  = `Toggle ${k} rsync`;
          html += `<span title="${tip}" ${tog} style="${cur}font-size:9px;padding:2px 5px;border-radius:2px;
            background:${bg};border:1px solid ${brd};color:${col};font-weight:600;">${s}</span>`;
        });
        html += `</div></div>`;

        // ── Profile activity bars (7 days) ────────────────────────────────────
        const profEntries = Object.entries(bwSummary);
        if (profEntries.length) {
          const maxRuns = Math.max(...profEntries.map(([,v]) => v.runs), 1);
          const colors  = ['#4caf50','#4a9eff','#ffb74d','#ce93d8','#ef5350','#4dd0e1'];
          html += `<div style="margin-bottom:6px;">
            <div style="font-size:9px;color:#2a2a2a;text-transform:uppercase;letter-spacing:.05em;margin-bottom:5px;">7-day profile activity</div>`;
          profEntries.forEach(([name, v], i) => {
            const pct   = Math.round(v.runs / maxRuns * 100);
            const col   = colors[i % colors.length];
            const bytes = v.bytes > 0 ? _fmtBytes(v.bytes) : '';
            const short = name.length > 18 ? name.slice(0,16) + '…' : name;
            html += `<div style="margin-bottom:4px;">
              <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:1px;">
                <span style="font-size:9px;color:#555;overflow:hidden;text-overflow:ellipsis;
                             white-space:nowrap;max-width:110px;" title="${name}">${short}</span>
                <span style="font-size:9px;color:#2a2a2a;flex-shrink:0;margin-left:4px;">
                  ${v.runs}×${bytes ? ' · ' + bytes : ''}</span>
              </div>
              <div style="height:3px;background:#111;border-radius:2px;overflow:hidden;">
                <div style="height:100%;width:${pct}%;background:${col};border-radius:2px;opacity:.7;"></div>
              </div>
            </div>`;
          });
          html += `</div>`;
        }

        // ── Active syncs ───────────────────────────────────────────────────────
        if (active.length) {
          active.forEach(a => {
            const sec = a.elapsed ?? 0;
            html += `<div style="background:#0d1a0a;border:1px solid #1a3a0a;border-radius:3px;
                                 padding:4px 8px;margin-bottom:5px;">
              <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;">
                <span style="font-size:10px;color:#8bc34a;font-weight:600;">⟳ ${a.profile}</span>
                <span style="font-size:10px;color:#6a8a4a;">${_dur(sec)}</span>
              </div>
              <div style="height:3px;background:#0a0a0a;border-radius:2px;overflow:hidden;">
                <div style="height:100%;background:#3a6a1a;border-radius:2px;
                            animation:vvRsPulse 1.4s ease-in-out infinite;width:60%;"></div>
              </div>
            </div>`;
          });
        }

        // ── Tier last-run rows with recency bars ───────────────────────────────
        const tierMeta = {
          critical:     {label:'Critical',    cadenceSec: 30*60},
          daily:        {label:'Daily',       cadenceSec: 24*3600},
          intermediate: {label:'Interm',      cadenceSec:  4*3600},
          weekly:       {label:'Weekly',      cadenceSec:  7*86400},
        };
        html += `<div style="border-top:1px solid #1a1a1a;padding-top:6px;">`;
        Object.entries(tierMeta).forEach(([key, meta]) => {
          const s    = lastSync[key];
          const ts   = s?.ts ?? 0;
          const ok   = s?.status === 'ok' || s?.status === 'success';
          const warn = s?.status === 'warn';
          const run  = s?.status === 'running';
          const err  = s && !ok && !warn && !run && ts > 0;
          const col  = run ? '#4a9eff' : ok ? '#4caf50' : warn ? '#ff9800' : err ? '#f44336' : '#2a2a2a';
          const icon = run ? '⟳' : ok ? '✓' : warn ? '!' : err ? '✗' : '—';
          const diff = ts ? now - ts : null;
          const pct  = ts ? Math.min(100, Math.round((now - ts) / meta.cadenceSec * 100)) : 0;
          const barC = pct >= 100 ? '#3a1a1a' : pct >= 75 ? '#2a1a0a' : '#0a1a0a';
          const fillC= pct >= 100 ? '#f44336' : pct >= 75 ? '#ff9800' : '#2a6a2a';

          html += `<div style="margin-bottom:4px;">
            <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:2px;">
              <span style="font-size:10px;color:#555;width:46px;flex-shrink:0;">${meta.label}</span>
              <span style="font-size:9px;color:#333;flex:1;text-align:right;margin-right:6px;">
                ${s?.duration ? _dur(s.duration) : ''}</span>
              <span style="font-size:9px;color:#2a2a2a;width:28px;text-align:right;margin-right:5px;">
                ${diff !== null ? _ago(diff) : '—'}</span>
              <span style="font-size:10px;color:${col};width:10px;text-align:right;">${icon}</span>
            </div>
            <div style="height:2px;background:${barC};border-radius:1px;overflow:hidden;">
              <div style="height:100%;width:${pct}%;background:${fillC};border-radius:1px;"></div>
            </div>
          </div>`;
        });
        html += `</div>`;

        el.innerHTML = html;

        const card = document.getElementById('vv-rsync-card');
        if (card) {
          card.classList.remove('vv-accent-ok','vv-accent-warn','vv-accent-err');
          if (!enabled) card.classList.add('vv-accent-err');
          else if (active.length) card.classList.add('vv-accent-ok');
        }
      })();

      // ── GPU ─────────────────────────────────────────────────────────────────
      // One card per installed GPU. Falls back to the legacy single-GPU payload so the
      // page still renders against a cached response written before 'gpus' existed.
      const gpuList  = (d.gpus && d.gpus.length) ? d.gpus
                     : ((d.gpu && d.gpu.available) ? [d.gpu] : []);
      const allProcs = d.gpu_procs ?? [];

      const renderGpuCard = (gpu, bodyId, labelId, fallbackLabel) => {
        const bodyEl  = document.getElementById(bodyId);
        const labelEl = document.getElementById(labelId);
        if (!bodyEl) return;

        if (!gpu || !gpu.available) {
          if (labelEl) labelEl.textContent = fallbackLabel;
          bodyEl.innerHTML = '<p style="color:#555;font-style:italic">No GPU detected</p>';
          return;
        }

        // Attribute processes to this card by UUID. Older payloads have no gpu_uuid on the
        // process rows — in that case only the first card claims them, rather than every
        // card showing the same list.
        const gpuProcs = allProcs.filter(p =>
          p.gpu_uuid ? p.gpu_uuid === gpu.uuid : (gpu.index ?? 0) === 0
        );

        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';

        if (labelEl) labelEl.textContent = 'GPU ' + (gpu.index ?? 0);

        // Process list — which apps are actually using this GPU (name + VRAM)
        let gpuProcHtml = '';
        if (procCount > 0) {
          const rows = gpuProcs.map(p => {
            const pname = (p.name || '').split('/').pop() || p.name || 'proc';
            return `<div style="display:flex;justify-content:space-between;font-size:10px;margin-bottom:2px;">
              <span style="color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${pname}</span>
              <span style="color:#555;flex-shrink:0;margin-left:6px;">${p.memory_mb} MB</span>
            </div>`;
          }).join('');
          gpuProcHtml = `<div style="margin-top:6px;border-top:1px solid #2a2a2a;padding-top:6px;">
            <div style="font-size:10px;color:#555;margin-bottom:3px;">GPU processes</div>
            <div style="max-height:60px;overflow-y:auto;scrollbar-width:none;-ms-overflow-style:none;">${rows}</div>
          </div>`;
        }

        bodyEl.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>` + gpuProcHtml;
      };

      renderGpuCard(gpuList[0], 'vv-gpu-body',  'vv-gpu-label',  'GPU');
      renderGpuCard(gpuList[1], 'vv-gpu1-body', 'vv-gpu1-label', 'GPU 1');

      // Row 3 is 8 columns wide: Rsync(1) + GPU0(1) + GPU1(1) + Transcode(1) + Streams(4).
      // On a single-GPU host the second card is hidden and Transcode widens back to 2 so the
      // row still fills exactly — otherwise it would leave a one-column hole.
      const gpu1Card = document.getElementById('vv-gpu1-card');
      const transcodeCard = document.getElementById('vv-transcode');
      const twoGpus = gpuList.length > 1;
      if (gpu1Card)      gpu1Card.style.display = twoGpus ? '' : 'none';
      if (transcodeCard) transcodeCard.style.gridColumn = twoGpus ? 'span 1' : 'span 2';

      // ── 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);
        const activeFiles = tc.active_files ?? 0;

        let activeSegHtml = '';
        if (activeFiles > 0) {
          activeSegHtml = `<div style="margin-top:8px;border-top:1px solid #2a2a2a;padding-top:6px;display:flex;align-items:center;justify-content:space-between;">
            <span style="font-size:10px;color:#555;">Active segments</span>
            <span style="font-size:10px;"><span style="color:#4caf50;font-weight:600;">${activeFiles}</span><span style="color:#444;font-size:9px;margin-left:4px;">Live TV / Direct Stream</span></span>
          </div>`;
        }

        let sessHtml = '';
        if (activeSessions.length) {
          function vvTcRow(s) {
            const meth = s.method.replace('Transcode', 'TC').replace('Direct ', '');
            return `<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>`;
          }
          const mid     = Math.ceil(activeSessions.length / 2);
          const colA    = activeSessions.slice(0, mid).map(vvTcRow).join('');
          const colB    = activeSessions.slice(mid).map(vvTcRow).join('');
          const colBDiv = colB ? `<div style="flex:1;min-width:0;border-left:1px solid #222;padding-left:8px;">${colB}</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:44px;overflow-y:auto;overflow-x:hidden;scrollbar-width:none;-ms-overflow-style:none;">
              <div style="display:flex;gap:0;">
                <div style="flex:1;min-width:0;">${colA}</div>${colBDiv}
              </div>
            </div>
          </div>`;
        }

        const nvmeColor  = ssd.available ? '#4caf50' : '#f44336';
        const rdFreed    = tc.last_rd_freed  ?? null;
        const ssdFreed   = tc.last_ssd_freed ?? null;
        const rdCleanTip  = rdFreed  ? `<span style="font-size:9px;color:#555;margin-left:4px;">↓${rdFreed}</span>`  : '';
        const ssdCleanTip = ssdFreed ? `<span style="font-size:9px;color:#555;margin-left:4px;">↓${ssdFreed}</span>` : '';
        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;flex-wrap:wrap;">
               <span style="display:inline-flex;align-items:center;background:${locColor}22;border:1px solid ${locColor};color:${locColor};
                 padding:1px 8px;border-radius:10px;font-size:11px;font-weight:600;">● ${loc}${rdCleanTip}</span>
               <span style="display:inline-flex;align-items:center;background:${nvmeColor}22;border:1px solid ${nvmeColor};color:${nvmeColor};
                 padding:1px 8px;border-radius:10px;font-size:11px;font-weight:600;">● NVMe${ssdCleanTip}</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>${activeSegHtml}${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);

// ── Fast poll: CPU, memory, network — 1-second live updates ──────────────────
function vvPollFast() {
  fetch('/plugins/varaverk/api/monitor_fast.php')
    .then(r => r.json())
    .then(d => {
      // 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
      document.getElementById('vv-memory-body').innerHTML = vvRenderMemory(d.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' : '—';

        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;
        const peakRx  = Math.max(...vvNetRxHistory, 0);
        const peakTx  = Math.max(...vvNetTxHistory, 0);

        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 style="color:#444;font-size:9px;font-weight:400;margin-left:4px;">peak ${vvFmtBps(peakRx)}</span></span>
                 <span><span style="color:#ff9800;font-size:9px;margin-right:4px;">━ OUT (TX)</span><span style="color:#ff9800;">${vvFmtBps(tx)}</span><span style="color:#444;font-size:9px;font-weight:400;margin-left:4px;">peak ${vvFmtBps(peakTx)}</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>';
      }
    })
    .catch(() => {});
}

vvPollFast();
setInterval(vvPollFast, 1000);

// 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) ──────────────────────

// Map a session's client string to a human-readable device category
function vvDeviceType(client) {
  const c = (client || '').toLowerCase();
  // Order matters: more specific patterns first
  if (/android\s*tv|androidtv|shield|nvidia\s*shield|android.*tv|tv.*android/.test(c)) return 'Android TV';
  if (/amazon|fire\s*tv|firetv|fire\s*stick|firestick/.test(c)) return 'Fire TV';
  if (/tvos|apple\s*tv/.test(c)) return 'Apple TV';
  if (/infuse/.test(c)) return 'Apple TV';
  if (/android|mobile/.test(c)) return 'Android';
  if (/ios|iphone|ipad/.test(c)) return 'iOS';
  if (/roku/.test(c)) return 'Roku';
  if (/webos|lg\s*tv|lgtv|lg /.test(c)) return 'LG';
  if (/samsung/.test(c)) return 'Samsung';
  if (/google\s*tv|googletv|chromecast/.test(c)) return 'Google TV';
  if (/kodi/.test(c)) return 'Kodi';
  if (/web|chrome|firefox|safari|browser|edge/.test(c)) return 'Browser';
  if (/desktop|theater|media\s*player|windows|linux|mac\s*os|macos/.test(c)) return 'Desktop';
  return null; // unknown — don't show
}

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('');

  // Per-chip shade: alternate bg brightness within a group to visually separate chips
  // shades[0] = base bg, shades[1] = slightly lighter, cycling per index
  function vvChipShade(cls, i) {
    const shades = {
      'vv-chip-device':  ['#0d1e2e', '#112436'],
      'vv-chip-res':     ['#0b1e1c', '#0e2422'],
      'vv-chip-codec':   ['#201500', '#261900'],
      'vv-chip-mtype':   ['#1a0d2e', '#1f1136'],
    };
    const bg = (shades[cls] ?? ['',''])[i % 2];
    return bg ? ` style="background:${bg};"` : '';
  }
  function vvChip(cls, label, i) {
    return `<span class="${cls}"${vvChipShade(cls, i)}>${label}</span>`;
  }

  // Device type summary — e.g. "3 Android  1 iOS  2 Roku"
  const devCounts = {};
  sessions.forEach(s => {
    const t = vvDeviceType(s.client);
    if (t) devCounts[t] = (devCounts[t] || 0) + 1;
  });
  const deviceBar = Object.entries(devCounts)
    .sort((a, b) => b[1] - a[1])
    .map(([t, n], i) => vvChip('vv-chip-device', `${n} ${t}`, i))
    .join('');
  const deviceSection = deviceBar
    ? `<span class="vv-chip-group-device"><span class="vv-device-sep">·</span>${deviceBar}</span>`
    : '';

  // Resolution summary — e.g. "2 4K  3 1080p  1 720p"
  function vvResLabel(h) {
    if (h >= 2160) return '4K';
    if (h >= 1080) return '1080p';
    if (h >= 720)  return '720p';
    if (h >= 480)  return '480p';
    return null;
  }
  const resCounts = {};
  sessions.forEach(s => {
    const r = vvResLabel(s.height || 0);
    if (r) resCounts[r] = (resCounts[r] || 0) + 1;
  });
  const resOrder = ['4K', '1080p', '720p', '480p'];
  const resBar = resOrder.filter(r => resCounts[r])
    .map((r, i) => vvChip('vv-chip-res', `${resCounts[r]} ${r}`, i))
    .join('');
  const resSection = resBar ? `<span class="vv-chip-group-res"><span class="vv-device-sep">·</span>${resBar}</span>` : '';

  // Codec summary — e.g. "4 H.264  2 HEVC"
  function vvCodecLabel(c) {
    if (!c) return null;
    const m = { h264: 'H.264', hevc: 'HEVC', av1: 'AV1', vp9: 'VP9', mpeg4: 'MPEG-4', mpeg2video: 'MPEG-2' };
    return m[c] || c.toUpperCase();
  }
  const codecCounts = {};
  sessions.forEach(s => {
    const c = vvCodecLabel(s.codec || '');
    if (c) codecCounts[c] = (codecCounts[c] || 0) + 1;
  });
  const codecBar = Object.entries(codecCounts)
    .sort((a, b) => b[1] - a[1])
    .map(([c, n], i) => vvChip('vv-chip-codec', `${n} ${c}`, i))
    .join('');
  const codecSection = codecBar ? `<span class="vv-chip-group-codec"><span class="vv-device-sep">·</span>${codecBar}</span>` : '';

  // Media type summary — Movies, Shows, Music, Live
  function vvMediaType(t) {
    if (!t) return null;
    const lc = t.toLowerCase();
    if (lc === 'movie')                                      return 'Movies';
    if (lc === 'episode')                                    return 'Shows';
    if (lc === 'audio' || lc === 'track' || lc === 'musicvideo') return 'Music';
    if (lc === 'livetvprogram' || lc === 'tvchannel' || lc === 'livetv' || lc === 'recording') return 'Live';
    return null;
  }
  const mtypeCounts = {};
  sessions.forEach(s => {
    const mt = vvMediaType(s.type || '');
    if (mt) mtypeCounts[mt] = (mtypeCounts[mt] || 0) + 1;
  });
  const mtypeOrder = ['Movies', 'Shows', 'Music', 'Live'];
  const mtypeBar = mtypeOrder.filter(mt => mtypeCounts[mt])
    .map((mt, i) => vvChip('vv-chip-mtype', `${mtypeCounts[mt]} ${mt}`, i))
    .join('');
  const mtypeSection = mtypeBar ? `<span class="vv-device-sep">·</span>${mtypeBar}` : '';

  if (sessions.length === 0) {
    el.innerHTML = `<div class="vv-stream-servers"><div class="vv-stream-left">${badges}</div></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">
      <div class="vv-stream-left">${badges}${mtypeSection}</div>
      <div class="vv-stream-right">${deviceSection}${resSection}${codecSection}</div>
    </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,"\\'");
    const [pIor, pIow] = vvIoSum(drives.map(d => d.device));
    const poolIoHtml = (() => {
      const parts = [];
      if (pIor >= 0.05) parts.push(`<span style="color:#3a7a3a;">↓${vvFmtRate(pIor)}</span>`);
      if (pIow >= 0.05) parts.push(`<span style="color:#7a4a1a;">↑${vvFmtRate(pIow)}</span>`);
      return parts.length ? `<span style="font-size:9px;margin-left:5px;">${parts.join(' ')}</span>` : '';
    })();

    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>` : ''}
          ${poolIoHtml}
        </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 item list — folders first, then ungrouped containers as individual rows
  const _folders = (data.folders ?? []).map(f => ({ ...f, _type: 'folder', isEmoji: false }));
  const _solo    = (data.ungrouped ?? []).map(c => ({ ...c, _type: 'container' }));
  const _items   = [..._folders, ..._solo];

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

  function renderItem(item) {
    return item._type === 'folder' ? renderFolder(item) : renderContainer(item);
  }

  // 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">${_items.map(renderItem).join('')}</div>`;
  } else {
    const perCol = Math.ceil(_items.length / _cols);
    const colDivs = Array.from({length: _cols}, (_, i) =>
      `<div class="vv-df-col">${_items.slice(i * perCol, (i + 1) * perCol).map(renderItem).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>
