<!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; }
  .header { background:var(--bg2); border-bottom:1px solid var(--border); padding:12px 20px; display:flex; justify-content:space-between; align-items:center; }
  .header h1 { font-size:18px; color:var(--accent); font-weight:600; }
  .header .info { color:var(--fg2); font-size:12px; }
  .header .controls { display:flex; gap:8px; align-items:center; }
  .header .controls select, .header .controls button {
    background:var(--bg3); border:1px solid var(--border); color:var(--fg); padding:4px 8px;
    border-radius:4px; font-size:12px; cursor:pointer;
  }
  .header .controls button:hover { border-color:var(--accent); }
  .header .controls .dot { width:8px; height:8px; border-radius:50%; background:var(--green); display:inline-block; }
  .dashboard { padding:16px; display:grid; gap:12px; }
  .row { display:grid; gap:12px; }
  .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:8px; overflow:hidden; }
  .card-title { padding:8px 12px; border-bottom:1px solid var(--border); font-size:12px; font-weight:600; color:var(--fg2); text-transform:uppercase; letter-spacing:.5px; display:flex; justify-content:space-between; align-items:center; }
  .card-body { padding:12px; }
  .gauge-row { display:flex; gap:16px; align-items:center; margin-bottom:8px; }
  .gauge-label { width:60px; color:var(--fg2); font-size:12px; }
  .gauge-bar { flex:1; height:18px; 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:6px; top:1px; font-size:11px; color:var(--fg); }
  table { width:100%; border-collapse:collapse; font-size:12px; }
  th { text-align:left; padding:6px 8px; 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; }
  th:hover { color:var(--accent); }
  td { padding:5px 8px; border-bottom:1px solid var(--bg3); }
  tr:hover td { background:var(--bg3); }
  .scroll-body { max-height:350px; overflow-y:auto; }
  .chart-container { width:100%; height:180px; }
  .pct-green { color:var(--green); } .pct-orange { color:var(--orange); } .pct-red { color:var(--red); }
  .tag { display:inline-block; padding:1px 6px; border-radius:3px; font-size:11px; }
  .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:40px; color:var(--fg2); }
  .kill-btn { background:var(--bg3); border:1px solid var(--red); color:var(--red); padding:2px 8px;
    border-radius:3px; font-size:11px; 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:340px; text-align:center; }
  .modal h3 { margin-bottom:12px; color:var(--red); }
  .modal p { margin-bottom:16px; color:var(--fg); font-size:13px; }
  .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); }
</style>
</head>
<body>
<div id="app">
  <div class="header">
    <div style="display:flex;align-items:center;gap:12px">
      <h1>Glances</h1>
      <span class="info" v-if="data">{{ data.system.hostname }} &mdash; {{ data.system.os_name }} {{ data.system.kernel_version }}</span>
    </div>
    <div class="controls">
      <span class="dot" :style="{background: connected ? '#3fb950' : '#f85149'}"></span>
      <select v-model.number="refreshInterval" @change="restartPolling">
        <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>

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

  <div v-if="!data" class="loading">Connecting to Glances API&hellip;</div>

  <div v-else class="dashboard">
    <!-- System info strip -->
    <div class="card">
      <div class="card-body" style="display:flex;gap:20px;align-items:center;flex-wrap:wrap;padding:8px 16px;">
        <span style="color:var(--fg2)">Uptime <strong style="color:var(--fg)">{{ formatUptime(data.system.uptime) }}</strong></span>
        <span style="color:var(--fg2)">Load <strong style="color:var(--fg)">{{ data.system.load_avg[0].toFixed(2) }} {{ data.system.load_avg[1].toFixed(2) }} {{ data.system.load_avg[2].toFixed(2) }}</strong></span>
        <span style="color:var(--fg2)">Processes <strong style="color:var(--fg)">{{ data.system.process_count }}</strong> ({{ data.system.thread_count }} threads)</span>
        <span v-if="data.system.battery_pct != null" style="color:var(--fg2)">Battery <strong :style="{color: data.system.battery_pct > 20 ? 'var(--green)' : 'var(--red)'}">{{ data.system.battery_pct.toFixed(0) }}%</strong> {{ data.system.battery_state }}</span>
      </div>
    </div>

    <!-- CPU + Memory + Temperatures -->
    <div class="row row-3" :class="{'row-2': !data.system.temperatures || !data.system.temperatures.length}">
      <div class="card">
        <div class="card-title">CPU<span>{{ data.system.cpu_count }} cores &mdash; {{ 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:data.system.cpu_breakdown.user+'%',background:'#58a6ff'}"></div><span class="gauge-text">{{ 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:data.system.cpu_breakdown.system+'%',background:'#f0883e'}"></div><span class="gauge-text">{{ data.system.cpu_breakdown.system.toFixed(1) }}%</span></div>
          </div>
          <div class="gauge-row">
            <span class="gauge-label">idle</span>
            <div class="gauge-bar"><div class="gauge-fill" :style="{width:data.system.cpu_breakdown.idle+'%',background:'#3fb950'}"></div><span class="gauge-text">{{ data.system.cpu_breakdown.idle.toFixed(1) }}%</span></div>
          </div>
          <div ref="cpuChart" class="chart-container"></div>
        </div>
      </div>
      <div class="card">
        <div class="card-title">Memory<span>{{ memPct }}%</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:memPct+'%',background:pctColor(memPct)}"></div><span class="gauge-text">{{ formatBytes(data.system.memory.used) }} / {{ formatBytes(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:swapPct+'%',background:pctColor(swapPct)}"></div><span class="gauge-text">{{ formatBytes(data.system.memory.swap_used) }} / {{ formatBytes(data.system.memory.swap_total) }}</span></div>
          </div>
          <div ref="memChart" class="chart-container"></div>
        </div>
      </div>
      <!-- Temperatures -->
      <div class="card" v-if="data.system.temperatures && data.system.temperatures.length">
        <div class="card-title">Temperatures<span>{{ maxTemp(data.system.temperatures).toFixed(0) }}&deg;C peak</span></div>
        <div class="card-body scroll-body" style="max-height:280px;">
          <div v-for="[name,temp] in sortedTemps" :key="name" class="gauge-row" style="margin-bottom:4px">
            <span class="gauge-label" style="width:100px;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" :title="name">{{ name }}</span>
            <div class="gauge-bar" style="height:16px">
              <div class="gauge-fill" :style="{width: Math.min(temp/105*100,100)+'%', background: tempGradient(temp)}"></div>
              <span class="gauge-text" style="font-size:10px" :style="{color: temp >= 80 ? '#fff' : 'var(--fg)'}">{{ temp.toFixed(0) }}&deg;C</span>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- Per-core CPU overlay chart -->
    <div class="card" v-if="data.system.cpus.length > 1">
      <div class="card-title">Per-Core CPU</div>
      <div class="card-body">
        <div ref="perCoreChart" style="width:100%;height:200px;"></div>
      </div>
    </div>

    <!-- Network + Disk I/O -->
    <div class="row row-2">
      <div class="card">
        <div class="card-title">Network</div>
        <div class="card-body scroll-body">
          <table>
            <thead><tr>
              <th @click="doSort('net','interface')">Interface{{ sortIcon('net','interface') }}</th>
              <th @click="doSort('net','rx_rate')">Rx Rate{{ sortIcon('net','rx_rate') }}</th>
              <th @click="doSort('net','tx_rate')">Tx Rate{{ sortIcon('net','tx_rate') }}</th>
              <th @click="doSort('net','rx_bytes')">Rx Total{{ sortIcon('net','rx_bytes') }}</th>
              <th @click="doSort('net','tx_bytes')">Tx Total{{ sortIcon('net','tx_bytes') }}</th>
            </tr></thead>
            <tbody>
              <tr v-for="net in sortedNets" :key="net.interface">
                <td class="fs-name">{{ net.interface }}</td>
                <td>{{ formatRate(net.rx_rate) }}</td>
                <td>{{ formatRate(net.tx_rate) }}</td>
                <td>{{ formatBytes(net.rx_bytes) }}</td>
                <td>{{ formatBytes(net.tx_bytes) }}</td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
      <div class="card">
        <div class="card-title">Filesystems</div>
        <div class="card-body scroll-body">
          <table>
            <thead><tr>
              <th @click="doSort('disk','name')">Filesystem{{ sortIcon('disk','name') }}</th>
              <th @click="doSort('disk','total')">Size{{ sortIcon('disk','total') }}</th>
              <th @click="doSort('disk','used')">Used{{ sortIcon('disk','used') }}</th>
              <th @click="doSort('disk','_avail')">Avail{{ sortIcon('disk','_avail') }}</th>
              <th @click="doSort('disk','_pct')">Use%{{ sortIcon('disk','_pct') }}</th>
              <th>Mounted on</th>
            </tr></thead>
            <tbody>
              <template v-for="disk in sortedDisks" :key="disk.name">
                <tr>
                  <td class="fs-name">{{ disk.name }}</td>
                  <td>{{ formatBytes(disk.total) }}</td>
                  <td>{{ formatBytes(disk.used) }}</td>
                  <td>{{ formatBytes(disk.total - disk.used) }}</td>
                  <td :class="pctClass(diskPct(disk))">{{ diskPct(disk) }}%</td>
                  <td>{{ disk.mount_points[0] }}</td>
                </tr>
                <tr v-for="(mp,j) in disk.mount_points.slice(1)" :key="disk.name+'-'+j">
                  <td colspan="5"></td>
                  <td><span class="tree-indent">{{ j === disk.mount_points.length - 2 ? '\u2514\u2500' : '\u251C\u2500' }}</span>{{ mp }}</td>
                </tr>
              </template>
            </tbody>
          </table>
        </div>
      </div>
    </div>

    <!-- GPU -->
    <div v-if="data.gpu && data.gpu.gpus.length" class="card">
      <div class="card-title">GPU<span>{{ data.gpu.driver_version }}</span></div>
      <div class="card-body">
        <div v-for="gpu in data.gpu.gpus" :key="gpu.index" style="margin-bottom:12px">
          <div style="margin-bottom:6px;font-weight:600;color:var(--purple)">[{{ gpu.index }}] {{ gpu.name }}</div>
          <div class="gauge-row">
            <span class="gauge-label">GPU</span>
            <div class="gauge-bar"><div class="gauge-fill" :style="{width:gpu.gpu_utilization+'%',background:pctColor(gpu.gpu_utilization)}"></div><span class="gauge-text">{{ gpu.gpu_utilization }}%</span></div>
          </div>
          <div class="gauge-row">
            <span class="gauge-label">VRAM</span>
            <div class="gauge-bar"><div class="gauge-fill" :style="{width:(gpu.memory_used/gpu.memory_total*100)+'%',background:pctColor(gpu.memory_used/gpu.memory_total*100)}"></div><span class="gauge-text">{{ formatBytes(gpu.memory_used) }} / {{ formatBytes(gpu.memory_total) }}</span></div>
          </div>
          <div style="display:flex;gap:12px;margin-top:4px;font-size:11px;color:var(--fg2)">
            <span>{{ gpu.temperature }}&deg;C</span>
            <span>Fan {{ gpu.fan_speed }}%</span>
            <span>{{ gpu.power_usage }}W / {{ gpu.power_limit }}W</span>
            <span>{{ gpu.sm_clock }}MHz</span>
            <span>P{{ gpu.pstate }}</span>
          </div>
        </div>
        <table v-if="data.gpu.processes.length">
          <thead><tr><th>PID</th><th>GPU</th><th>Name</th><th>User</th><th>VRAM</th><th>Type</th></tr></thead>
          <tbody>
            <tr v-for="p in data.gpu.processes" :key="p.pid">
              <td>{{ p.pid }}</td><td>{{ p.gpu_index }}</td><td>{{ p.name }}</td><td>{{ p.user }}</td>
              <td>{{ formatBytes(p.gpu_memory) }}</td><td>{{ p.process_type }}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <!-- Docker -->
    <div v-if="data.docker.length" class="card">
      <div class="card-title">Docker Containers<span>{{ data.docker.length }}</span></div>
      <div class="card-body scroll-body">
        <table>
          <thead><tr>
            <th @click="doSort('docker','name')">Name{{ sortIcon('docker','name') }}</th>
            <th @click="doSort('docker','image')">Image{{ sortIcon('docker','image') }}</th>
            <th @click="doSort('docker','state')">Status{{ sortIcon('docker','state') }}</th>
            <th @click="doSort('docker','_cpu')">CPU%{{ sortIcon('docker','_cpu') }}</th>
            <th @click="doSort('docker','_mem')">Memory{{ sortIcon('docker','_mem') }}</th>
            <th>Net I/O</th>
            <th>Ports</th>
          </tr></thead>
          <tbody>
            <tr v-for="c in sortedDocker" :key="c.id">
              <td class="fs-name">{{ c.name }}</td>
              <td>{{ c.image }}</td>
              <td><span class="tag" :class="c.state==='running'?'tag-running':'tag-exited'">{{ c.state }}</span></td>
              <td>{{ (c.cpu_percent || c.cpu_pct || 0).toFixed(1) }}%</td>
              <td>{{ formatBytes(c.memory_used || c.mem_usage || 0) }}</td>
              <td>{{ formatBytes(c.net_rx) }} / {{ formatBytes(c.net_tx) }}</td>
              <td style="max-width:200px;overflow:hidden;text-overflow:ellipsis">{{ c.ports }}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <!-- Listening Ports -->
    <div v-if="data.ports && data.ports.length" class="card">
      <div class="card-title">Listening Ports<span>{{ data.ports.length }}</span></div>
      <div class="card-body scroll-body">
        <table>
          <thead>
            <tr>
              <th @click="doSort('port','port')">Port{{ sortIcon('port','port') }}</th>
              <th @click="doSort('port','protocol')">Proto{{ sortIcon('port','protocol') }}</th>
              <th @click="doSort('port','pid')">PID{{ sortIcon('port','pid') }}</th>
              <th @click="doSort('port','name')">Name{{ sortIcon('port','name') }}</th>
              <th @click="doSort('port','user')">User{{ sortIcon('port','user') }}</th>
              <th @click="doSort('port','cpu_usage')">CPU%{{ sortIcon('port','cpu_usage') }}</th>
              <th @click="doSort('port','memory_bytes')">MEM{{ sortIcon('port','memory_bytes') }}</th>
              <th @click="doSort('port','bind_address')">Bind{{ sortIcon('port','bind_address') }}</th>
              <th>Command</th>
              <th style="width:50px"></th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="p in sortedPorts" :key="p.pid+'-'+p.port">
              <td style="color:var(--orange);font-weight:700">{{ p.port }}</td>
              <td :style="{color: p.protocol==='tcp6' ? 'var(--purple)' : 'var(--green)'}">{{ p.protocol }}</td>
              <td>{{ p.pid }}</td>
              <td class="fs-name">{{ p.name }}</td>
              <td>{{ p.user }}</td>
              <td :class="pctClass(p.cpu_usage)">{{ p.cpu_usage.toFixed(1) }}</td>
              <td>{{ formatBytes(p.memory_bytes) }}</td>
              <td style="color:var(--fg2)">{{ p.bind_address }}</td>
              <td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;color:var(--fg2)">{{ p.command }}</td>
              <td>
                <button class="kill-btn" @click="confirmKill(p)" title="Kill process">Kill</button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <!-- Processes -->
    <div class="card">
      <div class="card-title">
        Processes
        <span>
          <input v-model="procFilter" placeholder="Filter..." style="background:var(--bg3);border:1px solid var(--border);color:var(--fg);padding:2px 8px;border-radius:3px;font-size:11px;width:150px;">
        </span>
      </div>
      <div class="card-body scroll-body" style="max-height:400px">
        <table>
          <thead>
            <tr>
              <th @click="doSort('proc','pid')">PID{{ sortIcon('proc','pid') }}</th>
              <th @click="doSort('proc','name')">Name{{ sortIcon('proc','name') }}</th>
              <th @click="doSort('proc','user')">User{{ sortIcon('proc','user') }}</th>
              <th @click="doSort('proc','cpu_usage')">CPU%{{ sortIcon('proc','cpu_usage') }}</th>
              <th @click="doSort('proc','memory_usage')">MEM%{{ sortIcon('proc','memory_usage') }}</th>
              <th @click="doSort('proc','memory_bytes')">MEM{{ sortIcon('proc','memory_bytes') }}</th>
              <th @click="doSort('proc','status')">Status{{ sortIcon('proc','status') }}</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="p in filteredProcs" :key="p.pid">
              <td>{{ p.pid }}</td>
              <td>{{ p.name }}</td>
              <td>{{ p.user }}</td>
              <td :class="pctClass(p.cpu_usage)">{{ p.cpu_usage.toFixed(1) }}</td>
              <td :class="pctClass(p.memory_usage)">{{ p.memory_usage.toFixed(1) }}</td>
              <td>{{ formatBytes(p.memory_bytes) }}</td>
              <td>{{ p.status }}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

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

createApp({
  setup() {
    const data = ref(null);
    const connected = ref(false);
    const refreshInterval = ref(2000);
    const darkMode = ref(true);
    const procFilter = ref('');
    const killTarget = ref(null);
    const killResult = ref(null);
    const cpuChart = ref(null);
    const memChart = ref(null);
    const perCoreChart = ref(null);

    // Generic sort state per table: { col, asc }
    const sorts = ref({
      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 },
    });
    function doSort(table, col) {
      const s = sorts.value[table];
      if (s.col === col) s.asc = !s.asc;
      else { s.col = col; s.asc = table === 'proc' ? false : true; }
    }
    function sortIcon(table, col) {
      const s = sorts.value[table];
      return s.col === col ? (s.asc ? ' \u25B2' : ' \u25BC') : '';
    }
    let cpuChartInst = null, memChartInst = null, perCoreChartInst = null;
    let cpuHistory = Array(60).fill(0);
    let memHistory = Array(60).fill(0);
    let ramHistory = Array(60).fill(0);
    let swapHistory = Array(60).fill(0);
    let coreHistories = [];
    let timer = null;

    const memPct = computed(() => data.value ? ((data.value.system.memory.used / data.value.system.memory.total) * 100).toFixed(1) : '0');
    const swapPct = computed(() => {
      if (!data.value || !data.value.system.memory.swap_total) return '0';
      return ((data.value.system.memory.swap_used / data.value.system.memory.swap_total) * 100).toFixed(1);
    });

    // Generic sorter: handles virtual columns prefixed with _
    function genericSort(arr, table, getVal) {
      const s = sorts.value[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(); }
        if (va < vb) return s.asc ? -1 : 1;
        if (va > vb) return s.asc ? 1 : -1;
        return 0;
      });
      return sorted;
    }

    const sortedNets = computed(() => {
      if (!data.value) return [];
      return genericSort(data.value.system.networks, 'net');
    });

    const sortedDisks = computed(() => {
      if (!data.value) return [];
      return genericSort(data.value.system.disks.filter(d => d.total > 0), 'disk', (d, col) => {
        if (col === '_avail') return d.total - d.used;
        if (col === '_pct') return d.total ? d.used / d.total : 0;
        return d[col];
      });
    });

    const sortedDocker = computed(() => {
      if (!data.value) return [];
      return genericSort(data.value.docker, 'docker', (c, col) => {
        if (col === '_cpu') return c.cpu_percent || c.cpu_pct || 0;
        if (col === '_mem') return c.memory_used || c.mem_usage || 0;
        return c[col];
      });
    });

    const sortedPorts = computed(() => {
      if (!data.value || !data.value.ports) return [];
      return genericSort(data.value.ports, 'port');
    });

    const filteredProcs = computed(() => {
      if (!data.value) return [];
      let procs = [...data.value.system.processes];
      if (procFilter.value) {
        const f = procFilter.value.toLowerCase();
        procs = procs.filter(p => p.name.toLowerCase().includes(f) || p.user.toLowerCase().includes(f) || String(p.pid).includes(f));
      }
      return genericSort(procs, 'proc').slice(0, 100);
    });

    function confirmKill(p) { killTarget.value = p; killResult.value = null; }

    async function executeKill(pid, signal) {
      try {
        const resp = await fetch('/api/v1/kill', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ pid, signal })
        });
        const result = await resp.json();
        killResult.value = { ok: result.ok, msg: result.message };
        if (result.ok) setTimeout(() => { killTarget.value = null; killResult.value = null; }, 1500);
      } catch(e) { killResult.value = { ok: false, msg: 'Request failed: ' + e.message }; }
    }

    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) {
      if (!r) return '0 B/s';
      return formatBytes(r) + '/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 tempGradient(t) {
      if (t >= 85) return '#f85149';
      if (t >= 70) return '#d29922';
      if (t >= 50) return '#58a6ff';
      return '#3fb950';
    }
    function maxTemp(temps) { return temps.reduce((m, [,t]) => Math.max(m, t), 0); }
    const sortedTemps = computed(() => {
      if (!data.value || !data.value.system.temperatures) return [];
      return [...data.value.system.temperatures].sort((a, b) => b[1] - a[1]);
    });

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

    const coreColors = ['#58a6ff','#f0883e','#3fb950','#bc8cff','#f85149','#79c0ff','#d29922','#da6ce6',
      '#56d4dd','#e06c75','#98c379','#61afef','#c678dd','#e5c07b','#ff6b6b','#7ec8e3'];

    function makeSingleChart(color, history) {
      return {
        animation: false,
        grid: { left:35, right:10, top:10, bottom:20 },
        xAxis: { type:'category', data: Array(60).fill(''), show:false },
        yAxis: { type:'value', min:0, max:100, splitLine:{lineStyle:{color:'#21262d'}}, axisLabel:{color:'#8b949e',fontSize:10} },
        series: [{ type:'line', data:history, 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 makeMemChart(ramHist, swapHist) {
      return {
        animation: false,
        grid: { left:35, right:10, top:10, bottom:20 },
        xAxis: { type:'category', data: Array(60).fill(''), show:false },
        yAxis: { type:'value', min:0, max:100, splitLine:{lineStyle:{color:'#21262d'}}, axisLabel:{color:'#8b949e',fontSize:10} },
        legend: { data:['RAM','Swap'], textStyle:{color:'#8b949e',fontSize:10}, top:0, right:10 },
        series: [
          { name:'RAM', type:'line', data:ramHist, 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:swapHist, 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 makePerCoreChart(histories, count) {
      return {
        animation: false,
        grid: { left:35, right:10, top:25, bottom:20 },
        xAxis: { type:'category', data: Array(60).fill(''), show:false },
        yAxis: { type:'value', min:0, max:100, splitLine:{lineStyle:{color:'#21262d'}}, axisLabel:{color:'#8b949e',fontSize:10} },
        legend: { data: Array.from({length:count}, (_,i) => 'Core '+i), textStyle:{color:'#8b949e',fontSize:9}, top:0, type:'scroll', pageTextStyle:{color:'#8b949e'} },
        series: histories.map((h,i) => ({
          name: 'Core '+i, type:'line', data:[...h], smooth:true, symbol:'none',
          lineStyle:{width:1.2, color:coreColors[i%coreColors.length]},
        }))
      };
    }

    async function fetchData() {
      try {
        const resp = await fetch('/api/v1/all');
        data.value = await resp.json();
        connected.value = true;
        cpuHistory.push(data.value.system.cpu_global); cpuHistory.shift();
        const rp = parseFloat(memPct.value); ramHistory.push(rp); ramHistory.shift();
        const sp = parseFloat(swapPct.value); swapHistory.push(sp); swapHistory.shift();
        // Per-core histories
        const nc = data.value.system.cpus.length;
        while (coreHistories.length < nc) coreHistories.push(Array(60).fill(0));
        for (let i = 0; i < nc; i++) {
          coreHistories[i].push(data.value.system.cpus[i].usage);
          coreHistories[i].shift();
        }
        await nextTick();
        if (cpuChart.value && !cpuChartInst) cpuChartInst = echarts.init(cpuChart.value);
        if (memChart.value && !memChartInst) memChartInst = echarts.init(memChart.value);
        if (perCoreChart.value && !perCoreChartInst) perCoreChartInst = echarts.init(perCoreChart.value);
        if (cpuChartInst) cpuChartInst.setOption(makeSingleChart('#58a6ff',[...cpuHistory]));
        if (memChartInst) memChartInst.setOption(makeMemChart([...ramHistory],[...swapHistory]));
        if (perCoreChartInst) perCoreChartInst.setOption(makePerCoreChart(coreHistories, nc));
      } catch(e) { connected.value = false; }
    }

    function restartPolling() {
      if (timer) clearInterval(timer);
      timer = setInterval(fetchData, refreshInterval.value);
    }

    onMounted(() => { fetchData(); restartPolling(); window.addEventListener('resize', () => { cpuChartInst?.resize(); memChartInst?.resize(); perCoreChartInst?.resize(); }); });
    onUnmounted(() => { if (timer) clearInterval(timer); });

    return { data, connected, refreshInterval, darkMode, procFilter, cpuChart, memChart, perCoreChart,
      sorts, doSort, sortIcon, sortedNets, sortedDisks, sortedDocker, sortedPorts, filteredProcs,
      killTarget, killResult, confirmKill, executeKill,
      memPct, swapPct, formatBytes, formatRate, formatUptime, pctColor, pctClass,
      diskPct, tempGradient, maxTemp, sortedTemps, toggleTheme, restartPolling };
  }
}).mount('#app');
</script>
</body>
</html>
