<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Glances — System Monitor</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/echarts@5/dist/echarts.min.js"></script>
<style>
  :root {
    --bg: #0d1117; --bg2: #161b22; --bg3: #21262d; --border: #30363d;
    --fg: #c9d1d9; --fg2: #8b949e; --accent: #58a6ff; --green: #3fb950;
    --orange: #d29922; --red: #f85149; --cyan: #79c0ff; --purple: #bc8cff;
  }
  * { margin:0; padding:0; box-sizing:border-box; }
  body { font-family: 'SF Mono','Fira Code',monospace; background:var(--bg); color:var(--fg); font-size:13px; height:100vh; overflow:hidden; }
  .app-header { background:var(--bg2); border-bottom:1px solid var(--border); padding:8px 16px; display:flex; justify-content:space-between; align-items:center; flex-shrink:0; }
  .app-header h1 { font-size:16px; color:var(--accent); font-weight:600; }
  .app-header .controls { display:flex; gap:6px; align-items:center; }
  .app-header .controls select, .app-header .controls button, .pane-bar button {
    background:var(--bg3); border:1px solid var(--border); color:var(--fg); padding:3px 8px;
    border-radius:4px; font-size:11px; cursor:pointer; font-family:inherit;
  }
  .app-header .controls button:hover, .pane-bar button:hover { border-color:var(--accent); }
  .split-container { display:flex; flex:1; overflow:hidden; }
  .split-h { flex-direction:row; }
  .split-v { flex-direction:column; }
  .split-container > .pane, .split-container > .split-container { flex:1; min-width:0; min-height:0; }
  .pane { display:flex; flex-direction:column; border:1px solid var(--border); overflow:hidden; }
  .pane-bar { background:var(--bg2); border-bottom:1px solid var(--border); padding:4px 8px; display:flex; justify-content:space-between; align-items:center; font-size:11px; flex-shrink:0; }
  .pane-bar .pane-info { display:flex; align-items:center; gap:6px; color:var(--fg2); overflow:hidden; }
  .pane-bar .pane-info .dot { width:6px; height:6px; border-radius:50%; display:inline-block; flex-shrink:0; }
  .pane-bar .pane-info .hostname { color:var(--cyan); font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .pane-bar .pane-controls { display:flex; gap:4px; flex-shrink:0; }
  .pane-bar button { padding:2px 6px; font-size:10px; }
  .pane-content { flex:1; overflow-y:auto; padding:8px; display:grid; gap:8px; align-content:start; }
  .dashboard { display:grid; gap:8px; }
  .row { display:grid; gap:8px; }
  .row-2 { grid-template-columns: 1fr 1fr; }
  .row-3 { grid-template-columns: 1fr 1fr 1fr; }
  @media(max-width:900px) { .row-2,.row-3 { grid-template-columns:1fr; } }
  .card { background:var(--bg2); border:1px solid var(--border); border-radius:6px; overflow:hidden; }
  .card-title { padding:6px 10px; border-bottom:1px solid var(--border); font-size:11px; font-weight:600; color:var(--fg2); text-transform:uppercase; letter-spacing:.5px; display:flex; justify-content:space-between; align-items:center; }
  .card-body { padding:8px; }
  .gauge-row { display:flex; gap:12px; align-items:center; margin-bottom:6px; }
  .gauge-label { width:55px; color:var(--fg2); font-size:11px; }
  .gauge-bar { flex:1; height:16px; background:var(--bg3); border-radius:3px; overflow:hidden; position:relative; }
  .gauge-fill { height:100%; border-radius:3px; transition:width .5s ease; }
  .gauge-text { position:absolute; right:4px; top:1px; font-size:10px; color:var(--fg); }
  table { width:100%; border-collapse:collapse; font-size:11px; }
  th { text-align:left; padding:4px 6px; color:var(--fg2); border-bottom:1px solid var(--border); font-weight:600; cursor:pointer; user-select:none; position:sticky; top:0; background:var(--bg2); z-index:1; font-size:10px; }
  th:hover { color:var(--accent); }
  td { padding:3px 6px; border-bottom:1px solid var(--bg3); }
  tr:hover td { background:var(--bg3); }
  .scroll-body { max-height:250px; overflow-y:auto; }
  .chart-container { width:100%; height:120px; }
  .pct-green { color:var(--green); } .pct-orange { color:var(--orange); } .pct-red { color:var(--red); }
  .tag { display:inline-block; padding:1px 4px; border-radius:3px; font-size:10px; }
  .tag-running { background:#0d351b; color:var(--green); }
  .tag-exited { background:#3d1416; color:var(--red); }
  .tree-indent { color:var(--fg2); margin-right:4px; }
  .fs-name { color:var(--cyan); }
  .loading { text-align:center; padding:20px; color:var(--fg2); font-size:12px; }
  .kill-btn { background:var(--bg3); border:1px solid var(--red); color:var(--red); padding:1px 6px;
    border-radius:3px; font-size:10px; cursor:pointer; font-family:inherit; }
  .kill-btn:hover { background:var(--red); color:#fff; }
  .modal-overlay { position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,.6); display:flex; align-items:center; justify-content:center; z-index:100; }
  .modal { background:var(--bg2); border:1px solid var(--border); border-radius:8px; padding:20px; min-width:380px; text-align:center; }
  .modal h3 { margin-bottom:12px; font-size:14px; }
  .modal p { margin-bottom:12px; color:var(--fg); font-size:12px; }
  .modal .btn-row { display:flex; gap:8px; justify-content:center; }
  .modal button { padding:6px 16px; border-radius:4px; border:1px solid var(--border); cursor:pointer; font-family:inherit; font-size:12px; }
  .modal .btn-kill { background:var(--red); color:#fff; border-color:var(--red); }
  .modal .btn-cancel { background:var(--bg3); color:var(--fg); }
  .modal .btn-ok { background:var(--accent); color:#fff; border-color:var(--accent); }
  .modal input[type=text] { width:100%; background:var(--bg3); border:1px solid var(--border); color:var(--fg); padding:8px 12px; border-radius:4px; font-family:inherit; font-size:13px; margin-bottom:12px; }
  .modal input[type=text]:focus { outline:none; border-color:var(--accent); }
  .hint-bar { font-size:10px; color:var(--fg2); padding:4px 8px; }
  .hint-bar span { color:var(--orange); margin-right:4px; }
</style>
</head>
<body>
<div id="app">
  <!-- Global header -->
  <div class="app-header">
    <div style="display:flex;align-items:center;gap:10px">
      <h1>Glances</h1>
    </div>
    <div class="controls">
      <select v-model.number="globalRefresh" @change="updateAllRefresh">
        <option :value="1000">1s</option>
        <option :value="2000">2s</option>
        <option :value="5000">5s</option>
        <option :value="10000">10s</option>
      </select>
      <button @click="toggleTheme">{{ darkMode ? 'Light' : 'Dark' }}</button>
    </div>
  </div>

  <!-- Endpoint input modal -->
  <div v-if="endpointModal" class="modal-overlay" @click.self="cancelEndpoint">
    <div class="modal">
      <h3 style="color:var(--accent)">Connect to Server</h3>
      <p>Enter the Glances API endpoint for the new pane:</p>
      <input type="text" v-model="endpointInput" @keyup.enter="confirmEndpoint" placeholder="http://host:61208" ref="endpointRef">
      <p style="font-size:11px;color:var(--fg2)">Examples: http://192.168.1.10:61208 &mdash; Leave as-is for local server</p>
      <div class="btn-row">
        <button class="btn-cancel" @click="cancelEndpoint">Cancel</button>
        <button class="btn-ok" @click="confirmEndpoint">Connect</button>
      </div>
    </div>
  </div>

  <!-- Kill confirmation modal -->
  <div v-if="killTarget" class="modal-overlay" @click.self="killTarget=null">
    <div class="modal">
      <h3 style="color:var(--red)">Kill Process</h3>
      <p>Send signal to <strong>{{ killTarget.name }}</strong> (PID {{ killTarget.pid }})?</p>
      <div class="btn-row">
        <button class="btn-cancel" @click="killTarget=null">Cancel</button>
        <button class="btn-kill" @click="executeKill(killTarget, 'TERM')">SIGTERM</button>
        <button class="btn-kill" @click="executeKill(killTarget, 'KILL')">SIGKILL</button>
      </div>
      <p v-if="killResult" style="margin-top:8px;font-size:11px" :style="{color: killResult.ok ? 'var(--green)' : 'var(--red)'}">{{ killResult.msg }}</p>
    </div>
  </div>

  <!-- Recursive split tree -->
  <split-node :node="tree" :depth="0" style="height:calc(100vh - 40px)"></split-node>
</div>

<!-- Recursive split-node component -->
<script type="text/x-template" id="split-node-tpl">
  <!-- Split node: renders two children side by side -->
  <div v-if="node.type==='split'" class="split-container" :class="node.dir==='h'?'split-h':'split-v'">
    <split-node :node="node.children[0]" :depth="depth+1"></split-node>
    <split-node :node="node.children[1]" :depth="depth+1"></split-node>
  </div>
  <!-- Leaf node: renders a dashboard pane -->
  <div v-else class="pane">
    <div class="pane-bar">
      <div class="pane-info">
        <span class="dot" :style="{background: node.pane.connected ? '#3fb950' : '#f85149'}"></span>
        <span class="hostname">{{ node.pane.data ? node.pane.data.system.hostname : 'connecting...' }}</span>
        <span style="color:var(--fg2);font-size:10px;opacity:.6">{{ $root.paneLabel(node.pane) }}</span>
      </div>
      <div class="pane-controls">
        <select v-model="node.pane.displayMode" style="background:var(--bg3);border:1px solid var(--border);color:var(--fg);padding:1px 4px;border-radius:3px;font-size:10px;font-family:inherit;">
          <option value="minimal">Min</option>
          <option value="normal">Norm</option>
          <option value="full">Full</option>
        </select>
        <button @click="$root.splitLeaf(node.pane.id,'h')" title="Split horizontal">&#x2194;</button>
        <button @click="$root.splitLeaf(node.pane.id,'v')" title="Split vertical">&#x2195;</button>
        <button @click="$root.closeLeaf(node.pane.id)" title="Close pane" style="color:var(--red)">&#x2715;</button>
      </div>
    </div>
    <div class="pane-content" v-if="node.pane.data">
      <!-- Info strip -->
      <div class="card">
        <div class="card-body" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:6px 12px;font-size:11px;">
          <span style="color:var(--fg2)">Uptime <strong style="color:var(--fg)">{{ $root.formatUptime(node.pane.data.system.uptime) }}</strong></span>
          <span style="color:var(--fg2)">Load <strong style="color:var(--fg)">{{ node.pane.data.system.load_avg[0].toFixed(2) }}</strong></span>
          <span style="color:var(--fg2)">Procs <strong style="color:var(--fg)">{{ node.pane.data.system.process_count }}</strong></span>
          <span v-if="node.pane.data.system.power && node.pane.data.system.power.available" style="color:var(--fg2)">Power <strong style="color:var(--orange)">{{ node.pane.data.system.power.total_power_w.toFixed(1) }}W</strong></span>
        </div>
      </div>
      <!-- CPU + Memory -->
      <div class="row row-2">
        <div class="card">
          <div class="card-title">CPU<span>{{ node.pane.data.system.cpu_count }}c &mdash; {{ node.pane.data.system.cpu_global.toFixed(1) }}%</span></div>
          <div class="card-body">
            <div class="gauge-row"><span class="gauge-label">user</span><div class="gauge-bar"><div class="gauge-fill" :style="{width:node.pane.data.system.cpu_breakdown.user+'%',background:'#58a6ff'}"></div><span class="gauge-text">{{ node.pane.data.system.cpu_breakdown.user.toFixed(1) }}%</span></div></div>
            <div class="gauge-row"><span class="gauge-label">system</span><div class="gauge-bar"><div class="gauge-fill" :style="{width:node.pane.data.system.cpu_breakdown.system+'%',background:'#f0883e'}"></div><span class="gauge-text">{{ node.pane.data.system.cpu_breakdown.system.toFixed(1) }}%</span></div></div>
            <div :ref="el => { node.pane._cpuEl = el }" class="chart-container"></div>
          </div>
        </div>
        <div class="card">
          <div class="card-title">Memory<span>{{ $root.memPct(node.pane) }}%</span></div>
          <div class="card-body">
            <div class="gauge-row"><span class="gauge-label">RAM</span><div class="gauge-bar"><div class="gauge-fill" :style="{width:$root.memPct(node.pane)+'%',background:$root.pctColor($root.memPct(node.pane))}"></div><span class="gauge-text">{{ $root.formatBytes(node.pane.data.system.memory.used) }} / {{ $root.formatBytes(node.pane.data.system.memory.total) }}</span></div></div>
            <div class="gauge-row"><span class="gauge-label">Swap</span><div class="gauge-bar"><div class="gauge-fill" :style="{width:$root.swapPct(node.pane)+'%',background:$root.pctColor($root.swapPct(node.pane))}"></div><span class="gauge-text">{{ $root.formatBytes(node.pane.data.system.memory.swap_used) }} / {{ $root.formatBytes(node.pane.data.system.memory.swap_total) }}</span></div></div>
            <div :ref="el => { node.pane._memEl = el }" class="chart-container"></div>
          </div>
        </div>
      </div>
      <!-- Sensors — normal/full -->
      <div class="card" v-if="$root.hasSensors(node.pane) && node.pane.displayMode !== 'minimal'">
        <div class="card-title">Sensors</div>
        <div class="card-body scroll-body" style="max-height:150px;">
          <template v-if="node.pane.data.system.power && node.pane.data.system.power.available">
            <div v-for="d in node.pane.data.system.power.domains" :key="d.name" class="gauge-row" style="margin-bottom:3px">
              <span class="gauge-label" style="width:80px;font-size:10px">&#9889; {{ d.name }}</span>
              <div class="gauge-bar" style="height:14px"><div class="gauge-fill" :style="{width: d.max_power_w ? Math.min(d.power_w/d.max_power_w*100,100)+'%' : '0%', background:'var(--orange)'}"></div><span class="gauge-text" style="font-size:9px">{{ d.power_w.toFixed(1) }}W</span></div>
            </div>
          </template>
          <div v-for="[name,temp] in $root.sortTemps(node.pane)" :key="name" class="gauge-row" style="margin-bottom:3px">
            <span class="gauge-label" style="width:80px;font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ name }}</span>
            <div class="gauge-bar" style="height:14px"><div class="gauge-fill" :style="{width:Math.min(temp/105*100,100)+'%',background:$root.tempGrad(temp)}"></div><span class="gauge-text" style="font-size:9px">{{ temp.toFixed(0) }}&deg;C</span></div>
          </div>
        </div>
      </div>
      <!-- Network + Disks — normal/full -->
      <div class="row row-2" v-if="node.pane.displayMode !== 'minimal'">
        <div class="card">
          <div class="card-title">Network</div>
          <div class="card-body scroll-body" style="max-height:150px;">
            <table><thead><tr><th @click="$root.pSort(node.pane,'net','interface')">Iface{{ $root.sI(node.pane,'net','interface') }}</th><th @click="$root.pSort(node.pane,'net','rx_rate')">Rx{{ $root.sI(node.pane,'net','rx_rate') }}</th><th @click="$root.pSort(node.pane,'net','tx_rate')">Tx{{ $root.sI(node.pane,'net','tx_rate') }}</th></tr></thead><tbody>
              <tr v-for="n in $root.pSorted(node.pane,'net',node.pane.data.system.networks)" :key="n.interface"><td class="fs-name">{{ n.interface }}</td><td>{{ $root.formatRate(n.rx_rate) }}</td><td>{{ $root.formatRate(n.tx_rate) }}</td></tr>
            </tbody></table>
          </div>
        </div>
        <div class="card">
          <div class="card-title">Filesystems</div>
          <div class="card-body scroll-body" style="max-height:150px;">
            <table><thead><tr><th @click="$root.pSort(node.pane,'disk','name')">FS{{ $root.sI(node.pane,'disk','name') }}</th><th @click="$root.pSort(node.pane,'disk','_pct')">Use%{{ $root.sI(node.pane,'disk','_pct') }}</th><th>Mount</th></tr></thead><tbody>
              <template v-for="d in $root.pSorted(node.pane,'disk',node.pane.data.system.disks.filter(x=>x.total>0),(x,c)=>c==='_pct'?(x.total?x.used/x.total:0):x[c])" :key="d.name">
                <tr><td class="fs-name">{{ d.name }}</td><td :class="$root.pctClass($root.diskPct(d))">{{ $root.diskPct(d) }}%</td><td>{{ d.mount_points[0] }}</td></tr>
              </template>
            </tbody></table>
          </div>
        </div>
      </div>
      <!-- GPU — full -->
      <div class="card" v-if="node.pane.data.gpu && node.pane.data.gpu.gpus.length && node.pane.displayMode === 'full'">
        <div class="card-title">GPU</div>
        <div class="card-body">
          <div v-for="gpu in node.pane.data.gpu.gpus" :key="gpu.index" style="margin-bottom:4px">
            <div style="font-size:10px;color:var(--purple);font-weight:600">[{{ gpu.index }}] {{ gpu.name }} {{ gpu.temperature }}&deg;C {{ gpu.power_usage }}W</div>
            <div class="gauge-row" style="margin-bottom:2px"><span class="gauge-label" style="width:35px">GPU</span><div class="gauge-bar" style="height:12px"><div class="gauge-fill" :style="{width:gpu.gpu_utilization+'%',background:$root.pctColor(gpu.gpu_utilization)}"></div><span class="gauge-text" style="font-size:9px">{{ gpu.gpu_utilization }}%</span></div></div>
          </div>
        </div>
      </div>
      <!-- Docker — full -->
      <div class="card" v-if="node.pane.data.docker && node.pane.data.docker.length && node.pane.displayMode === 'full'">
        <div class="card-title">Docker<span>{{ node.pane.data.docker.length }}</span></div>
        <div class="card-body scroll-body" style="max-height:100px;"><table><thead><tr><th>Name</th><th>St</th><th>CPU%</th></tr></thead><tbody>
          <tr v-for="c in node.pane.data.docker" :key="c.id"><td class="fs-name">{{ c.name }}</td><td><span class="tag" :class="c.state==='running'?'tag-running':'tag-exited'">{{ c.state }}</span></td><td>{{ (c.cpu_percent||0).toFixed(1) }}</td></tr>
        </tbody></table></div>
      </div>
      <!-- Ports — full -->
      <div class="card" v-if="node.pane.data.ports && node.pane.data.ports.length && node.pane.displayMode === 'full'">
        <div class="card-title">Ports<span>{{ node.pane.data.ports.length }}</span></div>
        <div class="card-body scroll-body" style="max-height:120px;"><table><thead><tr><th>Port</th><th>Name</th><th></th></tr></thead><tbody>
          <tr v-for="p in $root.pSorted(node.pane,'port',node.pane.data.ports)" :key="p.pid+'-'+p.port"><td style="color:var(--orange);font-weight:700">{{ p.port }}</td><td class="fs-name">{{ p.name }}</td><td><button class="kill-btn" @click="$root.confirmKill(node.pane,p)">Kill</button></td></tr>
        </tbody></table></div>
      </div>
      <!-- Processes — normal/full -->
      <div class="card" v-if="node.pane.displayMode !== 'minimal'">
        <div class="card-title">Processes<span><input v-model="node.pane.procFilter" placeholder="Filter..." style="background:var(--bg3);border:1px solid var(--border);color:var(--fg);padding:1px 6px;border-radius:3px;font-size:10px;width:100px;font-family:inherit;"></span></div>
        <div class="card-body scroll-body" style="max-height:180px;"><table><thead><tr>
          <th @click="$root.pSort(node.pane,'proc','pid')">PID{{ $root.sI(node.pane,'proc','pid') }}</th>
          <th @click="$root.pSort(node.pane,'proc','name')">Name{{ $root.sI(node.pane,'proc','name') }}</th>
          <th @click="$root.pSort(node.pane,'proc','cpu_usage')">CPU%{{ $root.sI(node.pane,'proc','cpu_usage') }}</th>
          <th @click="$root.pSort(node.pane,'proc','memory_bytes')">Mem{{ $root.sI(node.pane,'proc','memory_bytes') }}</th>
        </tr></thead><tbody>
          <tr v-for="p in $root.filtProcs(node.pane)" :key="p.pid"><td>{{ p.pid }}</td><td>{{ p.name }}</td><td :class="$root.pctClass(p.cpu_usage)">{{ p.cpu_usage.toFixed(1) }}</td><td>{{ $root.formatBytes(p.memory_bytes) }}</td></tr>
        </tbody></table></div>
      </div>
      <!-- Hints — full -->
      <div class="hint-bar" v-if="node.pane.displayMode === 'full' && $root.paneHints(node.pane).length">
        <div v-for="h in $root.paneHints(node.pane)" :key="h"><span>&#9432;</span>{{ h }}</div>
      </div>
    </div>
    <div v-else class="pane-content"><div class="loading">Connecting to {{ node.pane.endpoint }}...</div></div>
  </div>
</script>

<script>
const { createApp, ref, reactive, onMounted, onUnmounted, nextTick } = Vue;

const app = createApp({
  setup() {
    let paneIdCounter = 1;
    const darkMode = ref(true);
    const globalRefresh = ref(2000);
    const killTarget = ref(null);
    const killResult = ref(null);
    const endpointModal = ref(null); // { dir, paneId }
    const endpointInput = ref('');
    const endpointRef = ref(null);
    const allPanes = {}; // id -> pane (for lookup)

    // Tree root: { type:'leaf', pane } or { type:'split', dir:'h'|'v', children:[node,node] }
    const tree = ref(null);

    function createPane(endpoint) {
      const id = paneIdCounter++;
      const p = reactive({
        id, endpoint,
        data: null, connected: false, procFilter: '',
        displayMode: 'full',
        sorts: { net:{col:'interface',asc:true}, disk:{col:'name',asc:true}, docker:{col:'name',asc:true}, port:{col:'port',asc:true}, proc:{col:'cpu_usage',asc:false} },
        _cpuEl: null, _memEl: null,
        _cpuChart: null, _memChart: null,
        _cpuHist: Array(60).fill(0), _ramHist: Array(60).fill(0), _swapHist: Array(60).fill(0),
        _timer: null,
      });
      allPanes[id] = p;
      startPolling(p);
      return p;
    }

    function destroyPane(p) {
      stopPolling(p);
      if (p._cpuChart) { p._cpuChart.dispose(); p._cpuChart = null; }
      if (p._memChart) { p._memChart.dispose(); p._memChart = null; }
      delete allPanes[p.id];
    }

    async function fetchPane(p) {
      try {
        const base = p.endpoint.replace(/\/+$/, '');
        const resp = await fetch(base + '/api/v1/all');
        p.data = await resp.json();
        p.connected = true;
        p._cpuHist.push(p.data.system.cpu_global); p._cpuHist.shift();
        const mp = p.data.system.memory.total ? (p.data.system.memory.used / p.data.system.memory.total * 100) : 0;
        p._ramHist.push(mp); p._ramHist.shift();
        const sp = p.data.system.memory.swap_total ? (p.data.system.memory.swap_used / p.data.system.memory.swap_total * 100) : 0;
        p._swapHist.push(sp); p._swapHist.shift();
        await nextTick();
        if (p._cpuEl && !p._cpuChart) p._cpuChart = echarts.init(p._cpuEl);
        if (p._memEl && !p._memChart) p._memChart = echarts.init(p._memEl);
        if (p._cpuChart) p._cpuChart.setOption(lineChart('#58a6ff', [...p._cpuHist]));
        if (p._memChart) p._memChart.setOption(dualChart([...p._ramHist], [...p._swapHist]));
      } catch(e) { p.connected = false; }
    }

    function startPolling(p) { if (p._timer) clearInterval(p._timer); fetchPane(p); p._timer = setInterval(() => fetchPane(p), globalRefresh.value); }
    function stopPolling(p) { if (p._timer) { clearInterval(p._timer); p._timer = null; } }
    function updateAllRefresh() { Object.values(allPanes).forEach(p => startPolling(p)); }

    // --- Tree operations (iTerm2-style) ---

    // Find a node and its parent in the tree by pane id
    function findNode(node, id, parent, childIdx) {
      if (!node) return null;
      if (node.type === 'leaf' && node.pane.id === id) return { node, parent, childIdx };
      if (node.type === 'split') {
        const left = findNode(node.children[0], id, node, 0);
        if (left) return left;
        return findNode(node.children[1], id, node, 1);
      }
      return null;
    }

    // Split a leaf pane: replaces it with a split node containing the original + a new pane
    function splitLeaf(paneId, dir) {
      endpointModal.value = { dir, paneId };
      endpointInput.value = window.location.origin;
      nextTick(() => { if (endpointRef.value) endpointRef.value.focus(); });
    }

    function confirmEndpoint() {
      if (!endpointInput.value.trim() || !endpointModal.value) return;
      const { dir, paneId } = endpointModal.value;
      const url = endpointInput.value.trim();
      const newPane = createPane(url);
      const newLeaf = reactive({ type: 'leaf', pane: newPane });

      const found = findNode(tree.value, paneId, null, null);
      if (!found) { endpointModal.value = null; return; }

      const splitNode = reactive({
        type: 'split', dir,
        children: [found.node, newLeaf]
      });

      if (!found.parent) {
        // Splitting root
        tree.value = splitNode;
      } else {
        found.parent.children[found.childIdx] = splitNode;
      }

      autoDisplayModes();
      endpointModal.value = null;
      endpointInput.value = '';
    }

    function cancelEndpoint() { endpointModal.value = null; }

    // Close a leaf: replace parent split with the sibling
    function closeLeaf(paneId) {
      const found = findNode(tree.value, paneId, null, null);
      if (!found) return;
      destroyPane(found.node.pane);

      if (!found.parent) {
        // Only pane — don't close
        return;
      }

      const siblingIdx = found.childIdx === 0 ? 1 : 0;
      const sibling = found.parent.children[siblingIdx];

      // Find grandparent to replace parent with sibling
      const grandparent = findParentOf(tree.value, found.parent);
      if (!grandparent) {
        // Parent is root
        tree.value = sibling;
      } else {
        const idx = grandparent.children[0] === found.parent ? 0 : 1;
        grandparent.children[idx] = sibling;
      }
      autoDisplayModes();
    }

    function findParentOf(node, target) {
      if (!node || node.type !== 'split') return null;
      if (node.children[0] === target || node.children[1] === target) return node;
      return findParentOf(node.children[0], target) || findParentOf(node.children[1], target);
    }

    function countLeaves(node) {
      if (!node) return 0;
      if (node.type === 'leaf') return 1;
      return countLeaves(node.children[0]) + countLeaves(node.children[1]);
    }

    function forEachLeaf(node, fn) {
      if (!node) return;
      if (node.type === 'leaf') { fn(node.pane); return; }
      forEachLeaf(node.children[0], fn);
      forEachLeaf(node.children[1], fn);
    }

    function autoDisplayModes() {
      const n = countLeaves(tree.value);
      const mode = n >= 4 ? 'minimal' : n >= 2 ? 'normal' : 'full';
      forEachLeaf(tree.value, p => { p.displayMode = mode; });
    }

    // Per-pane sort
    function pSort(pane, table, col) { const s = pane.sorts[table]; if (s.col === col) s.asc = !s.asc; else { s.col = col; s.asc = table === 'proc' ? false : true; } }
    function sI(pane, table, col) { const s = pane.sorts[table]; return s.col === col ? (s.asc ? ' \u25B2' : ' \u25BC') : ''; }
    function pSorted(pane, table, arr, getVal) {
      const s = pane.sorts[table]; const sorted = [...arr];
      sorted.sort((a, b) => { let va = getVal ? getVal(a, s.col) : a[s.col]; let vb = getVal ? getVal(b, s.col) : b[s.col];
        if (va == null) va = 0; if (vb == null) vb = 0;
        if (typeof va === 'string') { va = va.toLowerCase(); vb = (vb+'').toLowerCase(); }
        return s.asc ? (va < vb ? -1 : va > vb ? 1 : 0) : (va > vb ? -1 : va < vb ? 1 : 0);
      }); return sorted;
    }
    function filtProcs(pane) {
      if (!pane.data) return [];
      let procs = [...pane.data.system.processes];
      if (pane.procFilter) { const f = pane.procFilter.toLowerCase(); procs = procs.filter(p => p.name.toLowerCase().includes(f) || p.user.toLowerCase().includes(f) || String(p.pid).includes(f)); }
      return pSorted(pane, 'proc', procs).slice(0, 80);
    }

    // Kill
    function confirmKill(pane, proc) { killTarget.value = { ...proc, _endpoint: pane.endpoint }; killResult.value = null; }
    async function executeKill(target, signal) {
      try {
        const base = target._endpoint.replace(/\/+$/, '');
        const resp = await fetch(base + '/api/v1/kill', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({pid:target.pid, signal}) });
        const r = await resp.json(); killResult.value = { ok:r.ok, msg:r.message };
        if (r.ok) setTimeout(() => { killTarget.value = null; killResult.value = null; }, 1500);
      } catch(e) { killResult.value = { ok:false, msg:'Failed: '+e.message }; }
    }

    // Helpers
    function formatBytes(b) { if (b==null||b===0) return '0 B'; const u=['B','KiB','MiB','GiB','TiB']; const i=Math.min(Math.floor(Math.log(b)/Math.log(1024)),u.length-1); return (b/Math.pow(1024,i)).toFixed(i>0?1:0)+' '+u[i]; }
    function formatRate(r) { return r ? formatBytes(r)+'/s' : '0 B/s'; }
    function formatUptime(s) { const d=Math.floor(s/86400),h=Math.floor((s%86400)/3600),m=Math.floor((s%3600)/60); return d>0?`${d}d ${h}h`:h>0?`${h}h ${m}m`:`${m}m`; }
    function pctColor(v) { return v>=90?'#f85149':v>=75?'#d29922':'#3fb950'; }
    function pctClass(v) { return v>=90?'pct-red':v>=75?'pct-orange':'pct-green'; }
    function diskPct(d) { return d.total?((d.used/d.total)*100).toFixed(0):'0'; }
    function tempGrad(t) { return t>=85?'#f85149':t>=70?'#d29922':t>=50?'#58a6ff':'#3fb950'; }
    function memPct(pane) { return pane.data ? ((pane.data.system.memory.used/pane.data.system.memory.total)*100).toFixed(1) : '0'; }
    function swapPct(pane) { return pane.data && pane.data.system.memory.swap_total ? ((pane.data.system.memory.swap_used/pane.data.system.memory.swap_total)*100).toFixed(1) : '0'; }
    function hasSensors(pane) { return (pane.data.system.temperatures && pane.data.system.temperatures.length) || (pane.data.system.power && pane.data.system.power.available); }
    function sortTemps(pane) { return pane.data && pane.data.system.temperatures ? [...pane.data.system.temperatures].sort((a,b)=>b[1]-a[1]) : []; }
    function paneLabel(pane) { try { return new URL(pane.endpoint).host; } catch { return pane.endpoint; } }
    function paneHints(pane) {
      if (!pane.data) return [];
      const h = [];
      if (!pane.data.system.power || !pane.data.system.power.available) h.push('Power: sudo chmod a+r /sys/class/powercap/intel-rapl:*/energy_uj');
      if (!pane.data.system.temperatures || !pane.data.system.temperatures.length) h.push('Temps: sudo apt install lm-sensors && sudo sensors-detect');
      return h;
    }

    // Charts
    function lineChart(color, data) {
      return { animation:false, grid:{left:30,right:8,top:8,bottom:16}, xAxis:{type:'category',data:Array(60).fill(''),show:false},
        yAxis:{type:'value',min:0,max:100,splitLine:{lineStyle:{color:'#21262d'}},axisLabel:{color:'#8b949e',fontSize:9}},
        series:[{type:'line',data,smooth:true,symbol:'none',lineStyle:{width:1.5,color},areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,colorStops:[{offset:0,color:color+'44'},{offset:1,color:color+'08'}]}}}] };
    }
    function dualChart(ram, swap) {
      return { animation:false, grid:{left:30,right:8,top:18,bottom:16}, xAxis:{type:'category',data:Array(60).fill(''),show:false},
        yAxis:{type:'value',min:0,max:100,splitLine:{lineStyle:{color:'#21262d'}},axisLabel:{color:'#8b949e',fontSize:9}},
        legend:{data:['RAM','Swap'],textStyle:{color:'#8b949e',fontSize:9},top:0,right:8},
        series:[
          {name:'RAM',type:'line',data:ram,smooth:true,symbol:'none',lineStyle:{width:1.5,color:'#bc8cff'},areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,colorStops:[{offset:0,color:'#bc8cff44'},{offset:1,color:'#bc8cff08'}]}}},
          {name:'Swap',type:'line',data:swap,smooth:true,symbol:'none',lineStyle:{width:1.5,color:'#f0883e',type:'dashed'},areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,colorStops:[{offset:0,color:'#f0883e22'},{offset:1,color:'#f0883e04'}]}}},
        ] };
    }

    function toggleTheme() {
      darkMode.value = !darkMode.value; const d = darkMode.value;
      document.documentElement.style.setProperty('--bg', d?'#0d1117':'#f6f8fa');
      document.documentElement.style.setProperty('--bg2', d?'#161b22':'#ffffff');
      document.documentElement.style.setProperty('--bg3', d?'#21262d':'#f0f0f0');
      document.documentElement.style.setProperty('--border', d?'#30363d':'#d0d7de');
      document.documentElement.style.setProperty('--fg', d?'#c9d1d9':'#1f2328');
      document.documentElement.style.setProperty('--fg2', d?'#8b949e':'#656d76');
    }

    onMounted(() => {
      const p = createPane(window.location.origin);
      tree.value = reactive({ type: 'leaf', pane: p });
      window.addEventListener('resize', () => { Object.values(allPanes).forEach(p => { p._cpuChart?.resize(); p._memChart?.resize(); }); });
    });
    onUnmounted(() => { Object.values(allPanes).forEach(p => stopPolling(p)); });

    return { tree, darkMode, globalRefresh, killTarget, killResult, endpointModal, endpointInput, endpointRef,
      splitLeaf, confirmEndpoint, cancelEndpoint, closeLeaf, updateAllRefresh, toggleTheme,
      pSort, sI, pSorted, filtProcs, confirmKill, executeKill, paneLabel, paneHints,
      memPct, swapPct, hasSensors, sortTemps, tempGrad,
      formatBytes, formatRate, formatUptime, pctColor, pctClass, diskPct };
  }
});

// Recursive split-node component
app.component('split-node', {
  template: '#split-node-tpl',
  props: { node: Object, depth: Number }
});

app.mount('#app');
</script>
</body>
</html>
