"""
Web server + embedded cyberpunk dashboard for GPU memory analysis.
"""

from __future__ import annotations

import json
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
from urllib.parse import urlparse

from .collector import collect_snapshot, kill_process

# ── HTML ────────────────────────────────────────────────────────────────────

HTML = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VRAM VIZ</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display+SC:wght@400;700;900&family=Source+Sans+3:wght@300;400;600;700&family=JetBrains+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  --bg: #000000;
  --surface: #0a0a0b;
  --surface2: #111113;
  --border: #1e1e22;
  --border-hi: #2a2a30;
  --glow: rgba(45, 212, 191, 0.12);
  --glow-strong: rgba(45, 212, 191, 0.25);
  --text: #d8d3dc;
  --text-dim: #6e6a73;
  --text-label: #4a474f;
  --accent: #2dd4bf;
  --accent-dim: rgba(45, 212, 191, 0.5);
  --red: #ef4444;
  --red-dim: rgba(239, 68, 68, 0.15);
  --yellow: #eab308;
  --orange: #f97316;
  --bar-bg: #141416;
  --bar-fill: #1a8a7a;
}

html, body { height: 100%; }

body {
  font-family: 'Source Sans 3', 'Source Sans Pro', system-ui, sans-serif;
  background: var(--bg);
  color: var(--text);
  overflow-x: hidden;
  overflow-y: auto;
  -webkit-font-smoothing: antialiased;
}

/* ── Background canvas ──────────────────────────────────── */
#geo-bg {
  position: fixed; inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* ── Scan line sweep ────────────────────────────────────── */
.scan-line {
  position: fixed;
  left: 0; right: 0;
  height: 2px;
  background: linear-gradient(90deg,
    transparent 0%,
    var(--accent-dim) 20%,
    var(--accent) 50%,
    var(--accent-dim) 80%,
    transparent 100%
  );
  opacity: 0.15;
  z-index: 1;
  pointer-events: none;
  animation: scanDown 8s linear infinite;
  box-shadow: 0 0 20px 4px var(--glow);
}
@keyframes scanDown {
  0%   { top: -2px; }
  100% { top: 100%; }
}

/* ── Container ──────────────────────────────────────────── */
.container {
  position: relative;
  z-index: 2;
  max-width: 1120px;
  margin: 0 auto;
  padding: 32px 24px 48px;
}

/* ── Header + glitch ────────────────────────────────────── */
.header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 28px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--border);
}

.title-wrap { position: relative; }

.title {
  font-family: 'Playfair Display SC', serif;
  font-size: 26px;
  font-weight: 700;
  letter-spacing: 3px;
  color: var(--text);
  position: relative;
}
.title .accent { color: var(--accent); }

.title::after {
  content: 'VRAM VIZ';
  position: absolute;
  left: 0; top: 0;
  color: var(--accent);
  opacity: 0;
  clip-path: inset(0 0 0 0);
}

@keyframes glitch {
  0%, 94%   { opacity: 0; }
  95%       { opacity: 0.6; clip-path: inset(20% 0 40% 0); transform: translateX(-3px); }
  96%       { opacity: 0; }
  97%       { opacity: 0.4; clip-path: inset(60% 0 10% 0); transform: translateX(3px); }
  98%, 100% { opacity: 0; transform: translateX(0); }
}
.title::after { animation: glitch 6s ease-in-out infinite; }

.subtitle {
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 2px;
  color: var(--text-dim);
  text-transform: uppercase;
  margin-top: 4px;
}

.status {
  display: flex; align-items: center; gap: 8px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px; color: var(--text-dim);
}
.dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent-dim);
  animation: blink 2.5s ease-in-out infinite;
}
@keyframes blink { 0%,100% { opacity:1; } 50% { opacity:.3; } }

/* ── GPU card ───────────────────────────────────────────── */
.gpu-cards {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
  margin-bottom: 20px;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 20px 24px;
  position: relative;
  overflow: hidden;
}
.card::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--accent-dim), transparent);
}

.card-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 16px;
}
.card-title {
  font-family: 'Playfair Display SC', serif;
  font-size: 14px;
  font-weight: 400;
  letter-spacing: 1.5px;
  color: var(--text);
}
.card-badge {
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
  color: var(--accent);
  background: rgba(45, 212, 191, 0.06);
  border: 1px solid rgba(45, 212, 191, 0.15);
  padding: 2px 8px;
  border-radius: 3px;
  letter-spacing: 1px;
}

.stats-row {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 16px;
  margin-bottom: 18px;
}
.stat {
  border-left: 1px solid var(--border);
  padding-left: 12px;
}
.stat:first-child { border-left: none; padding-left: 0; }
.stat-label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  color: var(--text-label);
  margin-bottom: 4px;
}
.stat-val {
  font-family: 'JetBrains Mono', monospace;
  font-size: 18px;
  font-weight: 500;
  color: var(--text);
}
.stat-val .unit {
  font-size: 11px;
  font-weight: 300;
  color: var(--text-dim);
  margin-left: 2px;
}

/* Memory bar */
.mem-bar-wrap { margin-bottom: 18px; }
.mem-bar-labels {
  display: flex;
  justify-content: space-between;
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
  color: var(--text-dim);
  margin-bottom: 6px;
}
.mem-bar-outer {
  width: 100%; height: 10px;
  background: var(--bar-bg);
  border-radius: 2px;
  overflow: hidden;
  position: relative;
  border: 1px solid var(--border);
}
.mem-bar-inner {
  height: 100%;
  border-radius: 1px;
  transition: width .8s cubic-bezier(.4,0,.2,1);
  position: relative;
}
.mem-bar-inner::after {
  content: '';
  position: absolute;
  right: 0; top: 0; bottom: 0;
  width: 2px;
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent-dim);
}

/* Sparkline section */
.sparkline-section {
  border-top: 1px solid var(--border);
  padding-top: 14px;
}
.sparkline-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}
.sparkline-title {
  font-family: 'JetBrains Mono', monospace;
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  color: var(--text-label);
}
.sparkline-value {
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  color: var(--accent);
}
#sparkline {
  width: 100%;
  height: 60px;
  display: block;
}

/* ── Treemap ────────────────────────────────────────────── */
.treemap-wrap {
  margin-bottom: 20px;
}
.section-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 10px;
}
.section-title {
  font-family: 'Playfair Display SC', serif;
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 1.5px;
  color: var(--text);
}
.section-meta {
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
  color: var(--text-dim);
}
.treemap {
  display: flex;
  gap: 2px;
  height: 56px;
  border-radius: 4px;
  overflow: hidden;
  border: 1px solid var(--border);
}
.treemap-block {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  overflow: hidden;
  cursor: default;
  transition: opacity .2s;
  position: relative;
  min-width: 3px;
}
.treemap-block:hover { opacity: 0.8; }
.treemap-block .tm-name {
  font-family: 'JetBrains Mono', monospace;
  font-size: 9px;
  font-weight: 500;
  color: rgba(255,255,255,0.85);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
  padding: 0 4px;
  text-shadow: 0 1px 3px rgba(0,0,0,.7);
}
.treemap-block .tm-size {
  font-family: 'JetBrains Mono', monospace;
  font-size: 8px;
  color: rgba(255,255,255,0.55);
  text-shadow: 0 1px 3px rgba(0,0,0,.7);
}

/* ── Process table ──────────────────────────────────────── */
.proc-wrap {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
  position: relative;
}
.proc-wrap::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--accent-dim), transparent);
}

.proc-toolbar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 16px 24px;
  border-bottom: 1px solid var(--border);
}
.proc-toolbar .section-title { margin-bottom: 0; }

table {
  width: 100%;
  border-collapse: collapse;
}
thead th {
  text-align: left;
  padding: 10px 24px;
  font-family: 'JetBrains Mono', monospace;
  font-weight: 500;
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  color: var(--text-label);
  background: var(--surface2);
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  transition: color .15s;
}
thead th:hover { color: var(--text-dim); }
thead th.sort-asc::after  { content: ' \25B2'; font-size: 8px; color: var(--accent); }
thead th.sort-desc::after { content: ' \25BC'; font-size: 8px; color: var(--accent); }
thead th.no-sort { cursor: default; }
thead th.no-sort:hover { color: var(--text-label); }

/* Right-align numeric columns */
thead th.r { text-align: right; }

tbody tr {
  border-bottom: 1px solid var(--border);
  transition: background .15s;
}
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: rgba(45, 212, 191, 0.02); }

td {
  padding: 9px 24px;
  white-space: nowrap;
  font-size: 13px;
}
td.mono {
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  font-weight: 400;
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
td.name-cell {
  max-width: 260px;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: 400;
}
td.dim { color: var(--text-dim); }

/* % GPU bar */
.pct-cell { width: 160px; }
.pct-bar {
  display: flex;
  align-items: center;
  gap: 10px;
}
.pct-track {
  flex: 1; height: 4px;
  background: var(--bar-bg);
  border-radius: 2px;
  overflow: hidden;
  border: 1px solid var(--border);
}
.pct-fill {
  height: 100%;
  border-radius: 1px;
  transition: width .6s cubic-bezier(.4,0,.2,1);
}
.pct-num {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  min-width: 38px;
  text-align: right;
  color: var(--text-dim);
}

/* Kill button */
.kill-btn {
  font-family: 'JetBrains Mono', monospace;
  font-size: 10px;
  letter-spacing: 1px;
  text-transform: uppercase;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 3px;
  color: var(--text-dim);
  cursor: pointer;
  padding: 4px 10px;
  transition: all .2s;
}
.kill-btn:hover {
  border-color: var(--red);
  color: var(--red);
  background: var(--red-dim);
  box-shadow: 0 0 12px rgba(239,68,68,0.1);
}
.kill-btn:disabled { opacity: .25; cursor: not-allowed; }

/* ── Toast ──────────────────────────────────────────────── */
.toast {
  position: fixed;
  bottom: 24px; right: 24px;
  padding: 10px 18px;
  border-radius: 4px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  color: #fff;
  transform: translateY(60px);
  opacity: 0;
  transition: all .3s ease;
  z-index: 100;
  max-width: 360px;
  border: 1px solid;
}
.toast.show { transform: translateY(0); opacity: 1; }
.toast.ok  { background: rgba(16,80,60,.9); border-color: var(--accent); }
.toast.err { background: rgba(80,16,16,.9); border-color: var(--red); }

/* ── Confirm dialog ─────────────────────────────────────── */
.confirm-overlay {
  position: fixed; inset: 0;
  background: rgba(0,0,0,.75);
  display: flex; align-items: center; justify-content: center;
  z-index: 200;
  backdrop-filter: blur(6px);
}
.confirm-box {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 28px;
  min-width: 340px;
  max-width: 420px;
  position: relative;
}
.confirm-box::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--red), transparent);
}
.confirm-box h3 {
  font-family: 'Playfair Display SC', serif;
  font-size: 15px;
  letter-spacing: 1px;
  margin-bottom: 10px;
}
.confirm-box p {
  font-size: 13px;
  color: var(--text-dim);
  margin-bottom: 22px;
  line-height: 1.6;
}
.confirm-box code {
  font-family: 'JetBrains Mono', monospace;
  background: var(--bar-bg);
  padding: 2px 6px;
  border-radius: 3px;
  font-size: 11px;
  border: 1px solid var(--border);
}
.confirm-actions { display: flex; gap: 10px; justify-content: flex-end; }
.btn-cancel, .btn-confirm {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: 1px;
  text-transform: uppercase;
  padding: 7px 18px;
  border-radius: 3px;
  cursor: pointer;
  border: 1px solid var(--border);
  transition: all .15s;
}
.btn-cancel { background: var(--surface2); color: var(--text-dim); }
.btn-cancel:hover { background: var(--border); color: var(--text); }
.btn-confirm { background: rgba(239,68,68,.15); color: var(--red); border-color: rgba(239,68,68,.3); }
.btn-confirm:hover { background: rgba(239,68,68,.25); }

.empty { padding: 40px; text-align: center; color: var(--text-dim); }

@media (max-width: 700px) {
  .container { padding: 16px 12px; }
  .stats-row { grid-template-columns: repeat(3, 1fr); }
  td, th { padding: 8px 12px; }
}
</style>
</head>
<body>

<canvas id="geo-bg"></canvas>
<div class="scan-line"></div>

<div class="container">
  <div class="header">
    <div class="title-wrap">
      <div class="title">VRAM <span class="accent">VIZ</span></div>
      <div class="subtitle">GPU Memory Analyzer</div>
    </div>
    <div class="status"><div class="dot"></div><span id="status-text">connecting</span></div>
  </div>

  <div id="gpu-cards" class="gpu-cards"></div>

  <div class="treemap-wrap">
    <div class="section-header">
      <div class="section-title">Memory Map</div>
      <div class="section-meta" id="treemap-meta"></div>
    </div>
    <div class="treemap" id="treemap"></div>
  </div>

  <div class="proc-wrap">
    <div class="proc-toolbar">
      <div class="section-title">Processes</div>
      <div class="section-meta" id="total-tracked"></div>
    </div>
    <table>
      <thead>
        <tr>
          <th data-col="name">Process</th>
          <th data-col="pid" class="r">PID</th>
          <th data-col="dedicated_bytes" class="r">Dedicated</th>
          <th data-col="shared_bytes" class="r">Shared</th>
          <th data-col="gpu_pct">% GPU</th>
          <th class="no-sort" style="width:80px"></th>
        </tr>
      </thead>
      <tbody id="proc-body"></tbody>
    </table>
  </div>
</div>

<div class="toast" id="toast"></div>
<div class="confirm-overlay" id="confirm" style="display:none"></div>

<script>
// ── Config ───────────────────────────────────────────────
const REFRESH_MS = 3000;
const HISTORY_LEN = 80;
const ACCENT = '#2dd4bf';
const ACCENT_DIM = 'rgba(45,212,191,0.25)';

// ── State ────────────────────────────────────────────────
let sortCol = 'dedicated_bytes', sortAsc = false;
let lastData = null;
let killing = new Set();
let utilHistory = [];

// ── Color helpers ────────────────────────────────────────
function pctColor(pct) {
  if (pct >= 40) return '#ef4444';
  if (pct >= 20) return '#f97316';
  if (pct >= 5)  return '#eab308';
  return ACCENT;
}
function barGradient(pct) {
  if (pct >= 80) return 'linear-gradient(90deg, #991b1b, #ef4444)';
  if (pct >= 60) return 'linear-gradient(90deg, #9a3412, #f97316)';
  if (pct >= 40) return 'linear-gradient(90deg, #854d0e, #eab308)';
  return 'linear-gradient(90deg, #115e59, ' + ACCENT + ')';
}
function treemapColor(i) {
  const palette = [
    '#1a8a7a','#1e6e8a','#2a5a8a','#4a3a7a',
    '#6a2a6a','#7a2a4a','#3a6a4a','#5a5a2a',
    '#2a4a6a','#4a2a5a','#6a4a2a','#2a6a5a',
  ];
  return palette[i % palette.length];
}

// ── Geometric mesh background ────────────────────────────
(function initMesh() {
  const c = document.getElementById('geo-bg');
  const ctx = c.getContext('2d');
  let w, h, pts = [];
  const N = 60, CONNECT = 140, SPEED = 0.15;

  function resize() {
    w = c.width = window.innerWidth;
    h = c.height = window.innerHeight;
  }
  window.addEventListener('resize', resize);
  resize();

  for (let i = 0; i < N; i++) {
    pts.push({
      x: Math.random() * w,
      y: Math.random() * h,
      vx: (Math.random() - 0.5) * SPEED,
      vy: (Math.random() - 0.5) * SPEED,
    });
  }

  function frame() {
    ctx.clearRect(0, 0, w, h);
    for (const p of pts) {
      p.x += p.vx; p.y += p.vy;
      if (p.x < 0 || p.x > w) p.vx *= -1;
      if (p.y < 0 || p.y > h) p.vy *= -1;
    }
    for (let i = 0; i < N; i++) {
      for (let j = i + 1; j < N; j++) {
        const dx = pts[i].x - pts[j].x;
        const dy = pts[i].y - pts[j].y;
        const d = Math.sqrt(dx*dx + dy*dy);
        if (d < CONNECT) {
          const alpha = (1 - d / CONNECT) * 0.08;
          ctx.strokeStyle = `rgba(45,212,191,${alpha})`;
          ctx.lineWidth = 0.5;
          ctx.beginPath();
          ctx.moveTo(pts[i].x, pts[i].y);
          ctx.lineTo(pts[j].x, pts[j].y);
          ctx.stroke();
        }
      }
    }
    for (const p of pts) {
      ctx.fillStyle = 'rgba(45,212,191,0.12)';
      ctx.beginPath();
      ctx.arc(p.x, p.y, 1.2, 0, Math.PI * 2);
      ctx.fill();
    }
    requestAnimationFrame(frame);
  }
  frame();
})();

// ── GPU card rendering ───────────────────────────────────
function renderGpus(gpus) {
  document.getElementById('gpu-cards').innerHTML = gpus.map(g => `
    <div class="card">
      <div class="card-header">
        <div class="card-title">GPU ${g.index}: ${g.name}</div>
        <div class="card-badge">${g.used_pct}% USED</div>
      </div>
      <div class="stats-row">
        <div class="stat">
          <div class="stat-label">Memory</div>
          <div class="stat-val">${Math.round(g.used_mb)}<span class="unit">/ ${Math.round(g.total_mb)} MB</span></div>
        </div>
        <div class="stat">
          <div class="stat-label">Temp</div>
          <div class="stat-val">${g.temperature != null ? g.temperature + '<span class="unit">\u00B0C</span>' : '\u2014'}</div>
        </div>
        <div class="stat">
          <div class="stat-label">GPU Load</div>
          <div class="stat-val">${g.utilization_pct != null ? g.utilization_pct + '<span class="unit">%</span>' : '\u2014'}</div>
        </div>
        <div class="stat">
          <div class="stat-label">Power</div>
          <div class="stat-val">${g.power_w != null ? g.power_w + '<span class="unit">W</span>' : '\u2014'}</div>
        </div>
        <div class="stat">
          <div class="stat-label">Free</div>
          <div class="stat-val">${Math.round(g.free_mb)}<span class="unit">MB</span></div>
        </div>
      </div>
      <div class="mem-bar-wrap">
        <div class="mem-bar-labels">
          <span>${Math.round(g.used_mb)} MB used</span>
          <span>${Math.round(g.free_mb)} MB free</span>
        </div>
        <div class="mem-bar-outer">
          <div class="mem-bar-inner" style="width:${g.used_pct}%;background:${barGradient(g.used_pct)}"></div>
        </div>
      </div>
      <div class="sparkline-section">
        <div class="sparkline-header">
          <span class="sparkline-title">Utilization History</span>
          <span class="sparkline-value" id="spark-val">${g.utilization_pct != null ? g.utilization_pct + '%' : '--'}</span>
        </div>
        <canvas id="sparkline"></canvas>
      </div>
    </div>
  `).join('');
}

// ── Sparkline chart ──────────────────────────────────────
function drawSparkline() {
  const canvas = document.getElementById('sparkline');
  if (!canvas || !utilHistory.length) return;
  const ctx = canvas.getContext('2d');
  const dpr = window.devicePixelRatio || 1;
  const rect = canvas.getBoundingClientRect();
  canvas.width = rect.width * dpr;
  canvas.height = rect.height * dpr;
  ctx.scale(dpr, dpr);

  const W = rect.width, H = rect.height;
  const pad = { top: 4, bottom: 4, left: 0, right: 0 };
  const gW = W - pad.left - pad.right;
  const gH = H - pad.top - pad.bottom;

  ctx.clearRect(0, 0, W, H);

  // grid lines
  ctx.strokeStyle = 'rgba(255,255,255,0.03)';
  ctx.lineWidth = 0.5;
  for (let pct of [25, 50, 75]) {
    const y = pad.top + gH * (1 - pct / 100);
    ctx.beginPath();
    ctx.moveTo(pad.left, y);
    ctx.lineTo(W - pad.right, y);
    ctx.stroke();
  }

  const n = utilHistory.length;
  const step = gW / (HISTORY_LEN - 1);

  function getXY(i) {
    const idx = n - HISTORY_LEN + i;
    const val = idx >= 0 ? utilHistory[idx] : 0;
    return {
      x: pad.left + i * step,
      y: pad.top + gH * (1 - Math.min(val, 100) / 100),
    };
  }

  // filled area
  const grad = ctx.createLinearGradient(0, pad.top, 0, H - pad.bottom);
  grad.addColorStop(0, 'rgba(45,212,191,0.18)');
  grad.addColorStop(1, 'rgba(45,212,191,0.01)');

  ctx.beginPath();
  ctx.moveTo(pad.left, H - pad.bottom);
  for (let i = 0; i < HISTORY_LEN; i++) {
    const p = getXY(i);
    if (i === 0) ctx.lineTo(p.x, p.y);
    else {
      const prev = getXY(i - 1);
      const cpx = (prev.x + p.x) / 2;
      ctx.bezierCurveTo(cpx, prev.y, cpx, p.y, p.x, p.y);
    }
  }
  ctx.lineTo(W - pad.right, H - pad.bottom);
  ctx.closePath();
  ctx.fillStyle = grad;
  ctx.fill();

  // line
  ctx.beginPath();
  for (let i = 0; i < HISTORY_LEN; i++) {
    const p = getXY(i);
    if (i === 0) ctx.moveTo(p.x, p.y);
    else {
      const prev = getXY(i - 1);
      const cpx = (prev.x + p.x) / 2;
      ctx.bezierCurveTo(cpx, prev.y, cpx, p.y, p.x, p.y);
    }
  }
  ctx.strokeStyle = ACCENT;
  ctx.lineWidth = 1.5;
  ctx.stroke();

  // endpoint dot
  if (n > 0) {
    const last = getXY(HISTORY_LEN - 1);
    ctx.beginPath();
    ctx.arc(last.x, last.y, 3, 0, Math.PI * 2);
    ctx.fillStyle = ACCENT;
    ctx.fill();
    ctx.beginPath();
    ctx.arc(last.x, last.y, 6, 0, Math.PI * 2);
    ctx.fillStyle = 'rgba(45,212,191,0.2)';
    ctx.fill();
  }
}

// ── Treemap ──────────────────────────────────────────────
function renderTreemap(procs) {
  const el = document.getElementById('treemap');
  const meta = document.getElementById('treemap-meta');
  const total = procs.reduce((s, p) => s + p.dedicated_bytes, 0);
  if (!total) { el.innerHTML = ''; meta.textContent = ''; return; }

  meta.textContent = procs.length + ' processes';

  el.innerHTML = procs
    .filter(p => p.dedicated_bytes > 0)
    .map((p, i) => {
      const frac = p.dedicated_bytes / total;
      const pct = (frac * 100);
      const bg = treemapColor(i);
      const showLabel = pct > 5;
      return `<div class="treemap-block" style="flex:${frac};background:${bg}"
                   title="${p.name} — ${p.dedicated_human} (${p.gpu_pct}% GPU)">
        ${showLabel ? `<span class="tm-name">${p.name}</span><span class="tm-size">${p.dedicated_human}</span>` : ''}
      </div>`;
    }).join('');
}

// ── Process table ────────────────────────────────────────
function sortedProcs(procs) {
  const cpy = [...procs];
  cpy.sort((a, b) => {
    let va = a[sortCol], vb = b[sortCol];
    if (typeof va === 'string') { va = va.toLowerCase(); vb = vb.toLowerCase(); }
    if (va < vb) return sortAsc ? -1 : 1;
    if (va > vb) return sortAsc ? 1 : -1;
    return 0;
  });
  return cpy;
}

function renderProcs(procs, total) {
  const body = document.getElementById('proc-body');
  document.getElementById('total-tracked').textContent = 'Total: ' + total;

  const items = sortedProcs(procs);
  if (!items.length) {
    body.innerHTML = '<tr><td colspan="6" class="empty">No processes found</td></tr>';
    return;
  }

  body.innerHTML = items.map(p => `
    <tr>
      <td class="name-cell" title="${p.exe}">${p.name}</td>
      <td class="mono dim">${p.pid}</td>
      <td class="mono">${p.dedicated_human}</td>
      <td class="mono dim">${p.shared_human}</td>
      <td class="pct-cell">
        <div class="pct-bar">
          <div class="pct-track">
            <div class="pct-fill" style="width:${Math.min(p.gpu_pct,100)}%;background:${pctColor(p.gpu_pct)}"></div>
          </div>
          <span class="pct-num">${p.gpu_pct}%</span>
        </div>
      </td>
      <td>
        <button class="kill-btn" onclick="confirmKill(${p.pid},'${p.name.replace(/'/g,"\\'")}')"
                ${killing.has(p.pid)?'disabled':''}>
          ${killing.has(p.pid)?'...':'KILL'}
        </button>
      </td>
    </tr>
  `).join('');

  document.querySelectorAll('thead th[data-col]').forEach(th => {
    th.classList.remove('sort-asc','sort-desc');
    if (th.dataset.col === sortCol) th.classList.add(sortAsc ? 'sort-asc' : 'sort-desc');
  });
}

// ── Column sort ──────────────────────────────────────────
document.querySelectorAll('thead th[data-col]').forEach(th => {
  th.addEventListener('click', () => {
    const col = th.dataset.col;
    if (sortCol === col) sortAsc = !sortAsc;
    else { sortCol = col; sortAsc = col === 'name'; }
    if (lastData) renderProcs(lastData.processes, lastData.total_tracked);
  });
});

// ── Data refresh ─────────────────────────────────────────
async function refresh() {
  try {
    const r = await fetch('/api/gpu');
    const data = await r.json();
    lastData = data;

    if (data.gpus.length && data.gpus[0].utilization_pct != null) {
      utilHistory.push(data.gpus[0].utilization_pct);
      if (utilHistory.length > HISTORY_LEN * 2) utilHistory = utilHistory.slice(-HISTORY_LEN);
    }

    renderGpus(data.gpus);
    renderTreemap(data.processes);
    renderProcs(data.processes, data.total_tracked);
    drawSparkline();
    document.getElementById('status-text').textContent = 'live \u00B7 ' + (REFRESH_MS/1000) + 's';
  } catch(e) {
    document.getElementById('status-text').textContent = 'connection error';
  }
}

// ── Kill flow ────────────────────────────────────────────
function showToast(msg, ok) {
  const t = document.getElementById('toast');
  t.textContent = msg;
  t.className = 'toast show ' + (ok ? 'ok' : 'err');
  setTimeout(() => t.classList.remove('show'), 3500);
}

function confirmKill(pid, name) {
  const ov = document.getElementById('confirm');
  ov.style.display = 'flex';
  ov.innerHTML = `
    <div class="confirm-box">
      <h3>Terminate Process</h3>
      <p>Kill <strong>${name}</strong> with PID <code>${pid}</code>?<br>
         GPU memory will be freed immediately.</p>
      <div class="confirm-actions">
        <button class="btn-cancel" onclick="closeConfirm()">Cancel</button>
        <button class="btn-confirm" onclick="doKill(${pid})">Terminate</button>
      </div>
    </div>`;
}
function closeConfirm() { document.getElementById('confirm').style.display = 'none'; }

async function doKill(pid) {
  closeConfirm();
  killing.add(pid);
  if (lastData) renderProcs(lastData.processes, lastData.total_tracked);
  try {
    const r = await fetch('/api/kill/' + pid, { method: 'POST' });
    const d = await r.json();
    showToast(d.ok ? d.message : d.error, d.ok);
  } catch(e) {
    showToast('Request failed', false);
  }
  killing.delete(pid);
  setTimeout(refresh, 600);
}

// ── Init ─────────────────────────────────────────────────
refresh();
setInterval(refresh, REFRESH_MS);
window.addEventListener('resize', drawSparkline);
</script>
</body>
</html>"""


# ── HTTP Server ─────────────────────────────────────────────────────────────

class _Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        path = urlparse(self.path).path
        if path == "/api/gpu":
            self._json_response(200, collect_snapshot())
        elif path == "/":
            body = HTML.encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        else:
            self._json_response(404, {"error": "not found"})

    def do_POST(self):
        path = urlparse(self.path).path
        if path.startswith("/api/kill/"):
            try:
                pid = int(path.split("/")[-1])
            except ValueError:
                self._json_response(400, {"ok": False, "error": "invalid pid"})
                return
            self._json_response(200, kill_process(pid))
        else:
            self._json_response(404, {"error": "not found"})

    def _json_response(self, code: int, data: dict):
        body = json.dumps(data).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, fmt, *args):
        pass


class _ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    daemon_threads = True


def run_server(port: int = 5111):
    server = _ThreadedHTTPServer(("0.0.0.0", port), _Handler)
    print(f"  VRAM VIZ running at http://localhost:{port}")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\n  Shutting down.")
        server.server_close()
