#!/usr/bin/env python3
"""
dashboard_server.py — Live Trading Bot Dashboard
=================================================
Serves a real-time HTML dashboard on HTTP (default port 8080).

Endpoints:
  GET /         → dashboard HTML  (JS auto-refreshes data every 60 s)
  GET /api/data → JSON snapshot   (process info + agent PnL + log tail)

Managed by watcher_v4.py alongside trading_bot and shadow_bot.
Access via:  http://<server-ip>:8080/
SSH tunnel:  ssh -L 8080:localhost:8080 user@jackal
"""

import os, json, subprocess
from datetime import datetime, timezone, timedelta
from http.server import HTTPServer, BaseHTTPRequestHandler

PORT         = 8080
WORK_DIR     = "/opt/services/bots/trading_bot/Stock_Bot"
SHADOW_DIR   = "/opt/services/bots/shadow_bot/Stock_Bot"
HANDOFF      = os.path.join(WORK_DIR,   "logs/claude_handoff.json")
SHADOW_HANDOFF = os.path.join(SHADOW_DIR, "logs/claude_handoff.json")
BOT_LOG      = os.path.join(WORK_DIR,   "trading.log")
SHADOW_LOG   = os.path.join(SHADOW_DIR, "trading.log")

# ── System helpers ─────────────────────────────────────────────────────────────

def proc_info(pattern):
    """Return process info dict for the first PID matching pattern."""
    try:
        r = subprocess.run(["pgrep", "-f", pattern], capture_output=True, timeout=5)
        if r.returncode != 0:
            return {"running": False}
        pid = int(r.stdout.decode().strip().split()[0])
        ram_mb = 0
        try:
            with open(f"/proc/{pid}/status") as f:
                for line in f:
                    if line.startswith("VmRSS:"):
                        ram_mb = int(line.split()[1]) // 1024
                        break
        except Exception:
            pass
        cpu_pct = 0.0
        try:
            c = subprocess.run(["ps", "-p", str(pid), "-o", "pcpu="],
                               capture_output=True, text=True, timeout=5)
            cpu_pct = float(c.stdout.strip()) if c.stdout.strip() else 0.0
        except Exception:
            pass
        return {"running": True, "pid": pid, "ram_mb": ram_mb, "cpu_pct": round(cpu_pct, 1)}
    except Exception:
        return {"running": False}

def sys_mem():
    try:
        mem = {}
        with open("/proc/meminfo") as f:
            for line in f:
                p = line.split()
                if len(p) >= 2:
                    mem[p[0].rstrip(":")] = int(p[1])
        total_mb = mem.get("MemTotal",     0) // 1024
        avail_mb = mem.get("MemAvailable", 0) // 1024
        used_mb  = total_mb - avail_mb
        return {
            "total_mb": total_mb,
            "used_mb":  used_mb,
            "avail_mb": avail_mb,
            "used_pct": round(used_mb / total_mb * 100, 1) if total_mb else 0,
        }
    except Exception:
        return {"total_mb": 0, "used_mb": 0, "avail_mb": 0, "used_pct": 0}

def load_handoff(path):
    try:
        with open(path) as f:
            return json.load(f)
    except Exception:
        return {}

def tail_log(path, n=50):
    try:
        r = subprocess.run(["tail", "-n", str(n), path],
                           capture_output=True, text=True, timeout=5)
        lines = r.stdout.strip().split("\n") if r.stdout.strip() else []
        return [l for l in lines if l.strip()]
    except Exception:
        return []

def market_status():
    try:
        import pytz
        tz  = pytz.timezone("America/New_York")
        now = datetime.now(tz)
        if now.weekday() >= 5:
            open_flag = False
        else:
            t = (now.hour, now.minute)
            open_flag = (9, 30) <= t <= (16, 0)
        if open_flag:
            return {"is_open": True, "label": "OPEN", "next": ""}
        # next open
        nxt = now.replace(hour=9, minute=30, second=0, microsecond=0)
        if now >= nxt:
            nxt += timedelta(days=1)
        while nxt.weekday() >= 5:
            nxt += timedelta(days=1)
        diff = nxt - now
        h, rem = divmod(int(diff.total_seconds()), 3600)
        m = rem // 60
        return {"is_open": False, "label": "CLOSED", "next": f"{h}h {m}m"}
    except Exception:
        return {"is_open": False, "label": "UNKNOWN", "next": ""}

def agents_stats(handoff):
    agents = handoff.get("agents", [])
    if not agents:
        return {"total_pnl": 0, "total_capital": 0, "total_trades": 0,
                "win_rate": 0, "count": 0, "agents": []}
    total_pnl    = sum(a.get("total_pnl",  0) for a in agents)
    total_cap    = sum(a.get("capital", 1000) for a in agents)
    total_trades = sum(a.get("trades",     0) for a in agents)
    total_wins   = sum(a.get("wins",       0) for a in agents)
    rows = []
    for a in sorted(agents, key=lambda x: x.get("total_pnl", 0), reverse=True):
        t = a.get("trades", 0)
        w = a.get("wins",   0)
        rows.append({
            "name":       a.get("name", "?"),
            "type":       a.get("type", "base"),
            "pnl":        round(a.get("total_pnl",  0), 2),
            "capital":    round(a.get("capital", 1000), 2),
            "trades":     t,
            "wins":       w,
            "win_rate":   round(w / t * 100, 1) if t else 0,
            "train_count":a.get("train_count", 0),
            "val_acc":    round(a.get("val_acc", 0) * 100, 1),
            "gross_profit": round(a.get("gross_profit", 0), 2),
            "gross_loss":   round(a.get("gross_loss",   0), 2),
        })
    return {
        "total_pnl":     round(total_pnl,    2),
        "total_capital": round(total_cap,     2),
        "total_trades":  total_trades,
        "win_rate":      round(total_wins / total_trades * 100, 1) if total_trades else 0,
        "count":         len(agents),
        "agents":        rows,
    }

def build_data():
    now_utc = datetime.now(timezone.utc)
    t_hand  = load_handoff(HANDOFF)
    s_hand  = load_handoff(SHADOW_HANDOFF)

    watcher  = proc_info("watcher_v4")
    trading  = proc_info("trading_bot/Stock_Bot/StockTrading")
    shadow   = proc_info("shadow_bot/Stock_Bot/StockTrading")
    dash     = proc_info("dashboard_server")

    return {
        "timestamp":   now_utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
        "market":      market_status(),
        "processes": {
            "watcher": watcher,
            "trading": trading,
            "shadow":  shadow,
            "dashboard": dash,
        },
        "system":      sys_mem(),
        "trading":     agents_stats(t_hand),
        "shadow":      agents_stats(s_hand),
        "trading_log": tail_log(BOT_LOG,    50),
        "shadow_log":  tail_log(SHADOW_LOG, 30),
    }

# ── Embedded HTML ──────────────────────────────────────────────────────────────

DASHBOARD_HTML = r"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Trading Bot — Live Dashboard</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  --bg:       #0d1117;
  --bg2:      #161b22;
  --bg3:      #21262d;
  --border:   #30363d;
  --text:     #e6edf3;
  --muted:    #8b949e;
  --green:    #3fb950;
  --red:      #f85149;
  --blue:     #58a6ff;
  --yellow:   #e3b341;
  --purple:   #bc8cff;
  --orange:   #ffa657;
  --radius:   8px;
  --shadow:   0 4px 24px rgba(0,0,0,.4);
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  font-size: 14px;
  line-height: 1.5;
  min-height: 100vh;
}

/* ── Header ──────────────────────────────────────────────── */
header {
  background: var(--bg2);
  border-bottom: 1px solid var(--border);
  padding: 12px 24px;
  display: flex;
  align-items: center;
  gap: 20px;
  position: sticky;
  top: 0;
  z-index: 100;
}
.logo { font-size: 18px; font-weight: 700; color: var(--blue); letter-spacing: -.5px; }
.live-badge {
  display: flex; align-items: center; gap: 6px;
  background: rgba(63,185,80,.12);
  border: 1px solid rgba(63,185,80,.3);
  border-radius: 20px; padding: 3px 10px;
  font-size: 11px; font-weight: 600; color: var(--green); text-transform: uppercase;
}
.pulse-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--green);
  animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
  0%,100% { opacity: 1; transform: scale(1); }
  50%      { opacity: .5; transform: scale(.8); }
}
.header-clock { margin-left: auto; font-size: 13px; color: var(--muted); font-variant-numeric: tabular-nums; }
.market-badge {
  padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .5px;
}
.market-open   { background: rgba(63,185,80,.15); border:1px solid rgba(63,185,80,.4); color: var(--green); }
.market-closed { background: rgba(248,81,73,.12); border:1px solid rgba(248,81,73,.3); color: var(--red); }
.refresh-info  { font-size: 12px; color: var(--muted); }
.refresh-info span { color: var(--blue); font-variant-numeric: tabular-nums; }

/* ── Layout ───────────────────────────────────────────────── */
main { max-width: 1600px; margin: 0 auto; padding: 20px 24px 40px; }
.section-title {
  font-size: 11px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: 1px;
  margin: 24px 0 10px;
}

/* ── Cards ────────────────────────────────────────────────── */
.card {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 20px;
  box-shadow: var(--shadow);
}
.card-title { font-size: 11px; color: var(--muted); font-weight: 600;
              text-transform: uppercase; letter-spacing: .8px; margin-bottom: 8px; }

/* ── Process status row ────────────────────────────────────── */
.proc-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
@media(max-width:900px){ .proc-grid { grid-template-columns: repeat(2,1fr); } }

.proc-card { display: flex; flex-direction: column; gap: 6px; }
.proc-header { display: flex; align-items: center; gap: 8px; }
.proc-indicator {
  width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0;
}
.proc-indicator.up   { background: var(--green); box-shadow: 0 0 6px var(--green); animation: pulse 2s ease-in-out infinite; }
.proc-indicator.down { background: var(--red); }
.proc-name  { font-weight: 600; font-size: 13px; }
.proc-meta  { font-size: 12px; color: var(--muted); padding-left: 17px; font-variant-numeric: tabular-nums; }
.proc-down-label { font-size: 12px; color: var(--red); padding-left: 17px; }

/* ── Metric summary row ─────────────────────────────────────── */
.metric-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 12px; }
@media(max-width:900px){ .metric-grid { grid-template-columns: repeat(2,1fr); } }

.metric-label { font-size: 11px; color: var(--muted); font-weight:600; text-transform:uppercase; letter-spacing:.8px; }
.metric-value {
  font-size: 28px; font-weight: 700; line-height: 1.2; margin-top: 4px;
  font-variant-numeric: tabular-nums; letter-spacing: -1px;
}
.metric-value.pos { color: var(--green); }
.metric-value.neg { color: var(--red); }
.metric-value.neutral { color: var(--text); }
.metric-sub { font-size: 12px; color: var(--muted); margin-top: 2px; }

/* ── Bot comparison ─────────────────────────────────────────── */
.compare-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media(max-width:700px){ .compare-grid { grid-template-columns: 1fr; } }

.bot-card-title { display:flex; align-items:center; gap:8px; margin-bottom:12px; }
.bot-label { font-weight:700; font-size:15px; }
.bot-type-badge {
  font-size:10px; font-weight:700; padding:2px 7px; border-radius:10px;
  background: rgba(88,166,255,.15); border: 1px solid rgba(88,166,255,.3); color: var(--blue);
  text-transform:uppercase;
}
.shadow-badge {
  background: rgba(188,140,255,.12); border-color: rgba(188,140,255,.3); color: var(--purple);
}
.bot-stats { display:grid; grid-template-columns:repeat(3,1fr); gap:8px; }
.bstat-label { font-size:10px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px; }
.bstat-value { font-size:18px; font-weight:700; font-variant-numeric:tabular-nums; margin-top:2px; }
.bstat-value.pos { color:var(--green); }
.bstat-value.neg { color:var(--red); }

/* ── Progress bar ──────────────────────────────────────────── */
.progress-wrap { margin-top:10px; }
.progress-label { display:flex; justify-content:space-between; margin-bottom:4px;
                  font-size:11px; color:var(--muted); }
.progress-bar { height:4px; background:var(--bg3); border-radius:2px; overflow:hidden; }
.progress-fill { height:100%; border-radius:2px; transition: width .5s ease; }
.fill-blue   { background: var(--blue); }
.fill-green  { background: var(--green); }
.fill-yellow { background: var(--yellow); }
.fill-red    { background: var(--red); }

/* ── Agent table ────────────────────────────────────────────── */
.agent-table-wrap { overflow-x:auto; }
table { width:100%; border-collapse:collapse; font-size:13px; }
thead th {
  padding: 8px 12px; text-align:left; font-size:11px; font-weight:600;
  color:var(--muted); text-transform:uppercase; letter-spacing:.5px;
  border-bottom: 1px solid var(--border);
  cursor:pointer; user-select:none; white-space:nowrap;
}
thead th:hover { color:var(--text); }
thead th .sort-arrow { margin-left:4px; opacity:.4; }
thead th.sorted .sort-arrow { opacity:1; color:var(--blue); }
tbody tr { border-bottom: 1px solid rgba(48,54,61,.6); }
tbody tr:last-child { border-bottom:none; }
tbody tr:hover { background: rgba(255,255,255,.03); }
tbody td { padding: 8px 12px; font-variant-numeric:tabular-nums; }
.agent-name { font-weight:600; }
.clone-badge {
  font-size:9px; font-weight:700; padding:1px 5px; border-radius:8px;
  background:rgba(227,179,65,.12); border:1px solid rgba(227,179,65,.3); color:var(--yellow);
  text-transform:uppercase; vertical-align:middle; margin-left:5px;
}
.td-pnl.pos { color:var(--green); font-weight:600; }
.td-pnl.neg { color:var(--red); font-weight:600; }
.td-pnl.zero { color:var(--muted); }

/* ── Log viewer ─────────────────────────────────────────────── */
.log-grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
@media(max-width:900px){ .log-grid { grid-template-columns:1fr; } }

.log-box {
  background:var(--bg); border:1px solid var(--border); border-radius:var(--radius);
  padding:12px 14px; height:280px; overflow-y:auto; font-family:'Courier New',monospace;
  font-size:11.5px; line-height:1.6;
}
.log-box::-webkit-scrollbar { width:4px; }
.log-box::-webkit-scrollbar-track { background:transparent; }
.log-box::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
.log-line { white-space:pre-wrap; word-break:break-all; }
.log-info  { color:var(--muted); }
.log-warn  { color:var(--yellow); }
.log-error { color:var(--red); }
.log-trade { color:var(--green); font-weight:600; }
.log-time  { color:var(--blue); }

/* ── System bar ─────────────────────────────────────────────── */
.sys-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; }
@media(max-width:700px){ .sys-grid { grid-template-columns:1fr; } }

/* ── Footer ─────────────────────────────────────────────────── */
.footer { text-align:center; font-size:11px; color:var(--muted); padding:20px 0 8px; }

/* ── Fade in on update ──────────────────────────────────────── */
@keyframes fadeIn { from{opacity:.4} to{opacity:1} }
.updated { animation: fadeIn .4s ease; }
</style>
</head>
<body>

<header>
  <div class="logo">⚡ StockBot</div>
  <div class="live-badge"><div class="pulse-dot"></div>Live</div>
  <div id="market-badge" class="market-badge">—</div>
  <div id="market-next" style="font-size:12px;color:var(--muted)"></div>
  <div class="header-clock">
    <div id="live-clock"></div>
  </div>
  <div class="refresh-info">Next update in <span id="countdown">60</span>s</div>
</header>

<main>

  <!-- Process status -->
  <div class="section-title">Process Status</div>
  <div class="proc-grid" id="proc-grid"></div>

  <!-- Summary metrics -->
  <div class="section-title">Trading Bot — Summary</div>
  <div class="metric-grid" id="trading-metrics"></div>

  <!-- Bot comparison -->
  <div class="section-title">Live vs Shadow</div>
  <div class="compare-grid">
    <div class="card" id="trading-card"></div>
    <div class="card" id="shadow-card"></div>
  </div>

  <!-- System resources -->
  <div class="section-title">System Resources</div>
  <div class="sys-grid" id="sys-grid"></div>

  <!-- Agent table: trading -->
  <div class="section-title">Agent Leaderboard — Trading Bot</div>
  <div class="card"><div class="agent-table-wrap"><table id="agent-table"><thead></thead><tbody></tbody></table></div></div>

  <!-- Agent table: shadow -->
  <div class="section-title">Agent Leaderboard — Shadow Bot</div>
  <div class="card"><div class="agent-table-wrap"><table id="shadow-agent-table"><thead></thead><tbody></tbody></table></div></div>

  <!-- Logs -->
  <div class="section-title">Log Tails</div>
  <div class="log-grid">
    <div>
      <div class="card-title" style="margin-bottom:6px">Trading Bot</div>
      <div class="log-box" id="trading-log"></div>
    </div>
    <div>
      <div class="card-title" style="margin-bottom:6px">Shadow Bot</div>
      <div class="log-box" id="shadow-log"></div>
    </div>
  </div>

  <div class="footer" id="last-updated">Last updated: —</div>

</main>

<script>
// ── Utilities ───────────────────────────────────────────────────────────────
const fmtPnl = v => {
  const s = v >= 0 ? '+' : '';
  return `${s}$${Math.abs(v).toFixed(2)}`;
};
const pnlClass = v => v > 0 ? 'pos' : v < 0 ? 'neg' : 'zero';
const fmtMb = mb => mb >= 1024 ? `${(mb/1024).toFixed(1)} GB` : `${mb} MB`;

function fmtLogLine(line) {
  if (!line) return '';
  let cls = 'log-info';
  if (/\[WARN\]|\[WARNING\]/i.test(line))  cls = 'log-warn';
  if (/\[ERROR\]|\[CRITICAL\]/i.test(line))cls = 'log-error';
  if (/BUY|SELL|TRADE|profit|loss/i.test(line)) cls = 'log-trade';
  const esc = line.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  return `<div class="log-line ${cls}">${esc}</div>`;
}

// ── Clock ────────────────────────────────────────────────────────────────────
function updateClock() {
  const now = new Date();
  document.getElementById('live-clock').textContent =
    now.toLocaleTimeString('en-US', {hour12:false}) + ' UTC' +
    (now.getTimezoneOffset() >= 0 ? '-' : '+') +
    String(Math.abs(now.getTimezoneOffset()/60)).padStart(2,'0');
}
setInterval(updateClock, 1000);
updateClock();

// ── Countdown ────────────────────────────────────────────────────────────────
let countdown = 60;
setInterval(() => {
  countdown--;
  if (countdown <= 0) { countdown = 60; fetchData(); }
  document.getElementById('countdown').textContent = countdown;
}, 1000);

// ── Render helpers ───────────────────────────────────────────────────────────
function renderProcs(procs) {
  const defs = [
    { key:'watcher',  label:'Watcher' },
    { key:'trading',  label:'Trading Bot' },
    { key:'shadow',   label:'Shadow Bot' },
    { key:'dashboard',label:'Dashboard' },
  ];
  document.getElementById('proc-grid').innerHTML = defs.map(({key, label}) => {
    const p = procs[key] || {};
    const up = p.running;
    const meta = up
      ? `PID ${p.pid} &nbsp;|&nbsp; ${p.ram_mb} MB${p.cpu_pct ? ' &nbsp;|&nbsp; CPU ' + p.cpu_pct + '%' : ''}`
      : '';
    return `
      <div class="card proc-card">
        <div class="proc-header">
          <div class="proc-indicator ${up ? 'up' : 'down'}"></div>
          <div class="proc-name">${label}</div>
        </div>
        ${up
          ? `<div class="proc-meta">${meta}</div>`
          : `<div class="proc-down-label">● STOPPED</div>`}
      </div>`;
  }).join('');
}

function renderMetrics(t) {
  const pct = t.total_capital > 0
    ? ((t.total_pnl / (t.total_capital - t.total_pnl)) * 100).toFixed(2)
    : '0.00';
  const pclass = pnlClass(t.total_pnl);
  document.getElementById('trading-metrics').innerHTML = `
    <div class="card">
      <div class="metric-label">Total PnL</div>
      <div class="metric-value ${pclass}">${fmtPnl(t.total_pnl)}</div>
      <div class="metric-sub">${pct >= 0 ? '+' : ''}${pct}% return</div>
    </div>
    <div class="card">
      <div class="metric-label">Capital (all agents)</div>
      <div class="metric-value neutral">$${t.total_capital.toLocaleString('en',{minimumFractionDigits:2,maximumFractionDigits:2})}</div>
      <div class="metric-sub">${t.count} agents × $1,000 base</div>
    </div>
    <div class="card">
      <div class="metric-label">Total Trades</div>
      <div class="metric-value neutral">${t.total_trades.toLocaleString()}</div>
      <div class="metric-sub">Win rate ${t.win_rate}%</div>
    </div>
    <div class="card">
      <div class="metric-label">Win Rate</div>
      <div class="metric-value ${t.win_rate >= 55 ? 'pos' : t.win_rate >= 45 ? 'neutral' : 'neg'}">${t.win_rate}%</div>
      <div class="metric-sub">
        <div class="progress-bar" style="margin-top:8px">
          <div class="progress-fill fill-${t.win_rate >= 55 ? 'green' : t.win_rate >= 45 ? 'blue' : 'red'}"
               style="width:${Math.min(t.win_rate,100)}%"></div>
        </div>
      </div>
    </div>`;
}

function botCard(data, label, isLive) {
  const pclass = pnlClass(data.total_pnl);
  const badge  = isLive
    ? `<span class="bot-type-badge">Live</span>`
    : `<span class="bot-type-badge shadow-badge">Shadow</span>`;
  return `
    <div class="bot-card-title">
      <div class="bot-label">${label}</div>${badge}
    </div>
    <div class="bot-stats">
      <div>
        <div class="bstat-label">PnL</div>
        <div class="bstat-value ${pclass}">${fmtPnl(data.total_pnl)}</div>
      </div>
      <div>
        <div class="bstat-label">Capital</div>
        <div class="bstat-value">$${data.total_capital.toLocaleString('en',{minimumFractionDigits:2})}</div>
      </div>
      <div>
        <div class="bstat-label">Trades</div>
        <div class="bstat-value">${data.total_trades}</div>
      </div>
      <div>
        <div class="bstat-label">Win Rate</div>
        <div class="bstat-value ${data.win_rate >= 55 ? 'pos' : ''}">${data.win_rate}%</div>
      </div>
      <div>
        <div class="bstat-label">Agents</div>
        <div class="bstat-value">${data.count}</div>
      </div>
      <div>
        <div class="bstat-label">Top Agent</div>
        <div class="bstat-value" style="font-size:13px">${data.agents.length ? data.agents[0].name.replace('Clone_','') : '—'}</div>
      </div>
    </div>`;
}

function renderBotCards(t, s) {
  document.getElementById('trading-card').innerHTML = botCard(t, 'Trading Bot', true);
  document.getElementById('shadow-card').innerHTML  = botCard(s, 'Shadow Bot',  false);
}

function renderSys(sys, procs) {
  const ramPct   = sys.used_pct;
  const fillCls  = ramPct < 60 ? 'fill-blue' : ramPct < 80 ? 'fill-yellow' : 'fill-red';
  const totalCpu = (procs.trading?.cpu_pct || 0) + (procs.shadow?.cpu_pct || 0);
  document.getElementById('sys-grid').innerHTML = `
    <div class="card">
      <div class="card-title">RAM</div>
      <div style="font-size:22px;font-weight:700;margin:6px 0">${fmtMb(sys.used_mb)}</div>
      <div style="font-size:12px;color:var(--muted)">of ${fmtMb(sys.total_mb)} (${sys.used_pct}% used)</div>
      <div class="progress-wrap">
        <div class="progress-bar"><div class="progress-fill ${fillCls}" style="width:${sys.used_pct}%"></div></div>
      </div>
    </div>
    <div class="card">
      <div class="card-title">Bot CPU Usage</div>
      <div style="font-size:22px;font-weight:700;margin:6px 0">${totalCpu.toFixed(1)}%</div>
      <div style="font-size:12px;color:var(--muted)">Trading ${procs.trading?.cpu_pct||0}% &nbsp;+&nbsp; Shadow ${procs.shadow?.cpu_pct||0}%</div>
      <div class="progress-wrap">
        <div class="progress-bar"><div class="progress-fill fill-blue" style="width:${Math.min(totalCpu,100)}%"></div></div>
      </div>
    </div>
    <div class="card">
      <div class="card-title">Memory Breakdown</div>
      <div style="font-size:12px;color:var(--muted);margin-top:8px;display:flex;flex-direction:column;gap:4px">
        <div>Trading Bot: <b style="color:var(--text)">${fmtMb(procs.trading?.ram_mb||0)}</b></div>
        <div>Shadow Bot:  <b style="color:var(--text)">${fmtMb(procs.shadow?.ram_mb||0)}</b></div>
        <div>Watcher:     <b style="color:var(--text)">${fmtMb(procs.watcher?.ram_mb||0)}</b></div>
        <div>Dashboard:   <b style="color:var(--text)">${fmtMb(procs.dashboard?.ram_mb||0)}</b></div>
      </div>
    </div>`;
}

let sortCol = 'pnl', sortAsc = false;

function agentTableHTML(agents, tableId) {
  const cols = [
    { key:'name',        label:'Agent',      fmt: a => {
        const badge = a.type==='clone' ? '<span class="clone-badge">clone</span>' : '';
        return `<span class="agent-name">${a.name}</span>${badge}`;
      }},
    { key:'pnl',         label:'PnL',        fmt: a => `<span class="td-pnl ${pnlClass(a.pnl)}">${fmtPnl(a.pnl)}</span>` },
    { key:'capital',     label:'Capital',     fmt: a => `$${a.capital.toFixed(2)}` },
    { key:'trades',      label:'Trades',      fmt: a => a.trades },
    { key:'win_rate',    label:'Win %',       fmt: a => a.trades ? a.win_rate + '%' : '—' },
    { key:'train_count', label:'Trainings',   fmt: a => a.train_count },
    { key:'val_acc',     label:'Val Acc',     fmt: a => a.val_acc ? a.val_acc + '%' : '—' },
    { key:'gross_profit',label:'Gross Profit',fmt: a => `<span style="color:var(--green)">+$${a.gross_profit.toFixed(2)}</span>` },
    { key:'gross_loss',  label:'Gross Loss',  fmt: a => `<span style="color:var(--red)">-$${Math.abs(a.gross_loss).toFixed(2)}</span>` },
  ];

  const sorted = [...agents].sort((a,b) => {
    const va = a[sortCol], vb = b[sortCol];
    if (typeof va === 'string') return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
    return sortAsc ? va - vb : vb - va;
  });

  const head = '<tr>' + cols.map(c => {
    const active = c.key === sortCol ? 'sorted' : '';
    const arrow  = c.key === sortCol ? (sortAsc ? '▲' : '▼') : '▲';
    return `<th class="${active}" onclick="setSort('${c.key}','${tableId}')">${c.label}<span class="sort-arrow">${arrow}</span></th>`;
  }).join('') + '</tr>';

  const body = sorted.map(a =>
    '<tr>' + cols.map(c => `<td>${c.fmt(a)}</td>`).join('') + '</tr>'
  ).join('');

  return { head, body };
}

function renderAgentTable(agents, tableId) {
  const tbl = document.getElementById(tableId);
  if (!tbl) return;
  const { head, body } = agentTableHTML(agents, tableId);
  tbl.querySelector('thead').innerHTML = head;
  tbl.querySelector('tbody').innerHTML = body;
}

function setSort(col, tableId) {
  if (sortCol === col) sortAsc = !sortAsc;
  else { sortCol = col; sortAsc = false; }
  if (window._lastData) renderAgentTable(
    tableId === 'agent-table' ? window._lastData.trading.agents : window._lastData.shadow.agents,
    tableId
  );
}

function renderLogs(tradingLines, shadowLines) {
  document.getElementById('trading-log').innerHTML = tradingLines.map(fmtLogLine).join('');
  document.getElementById('shadow-log').innerHTML  = shadowLines.map(fmtLogLine).join('');
  // auto-scroll to bottom
  const tl = document.getElementById('trading-log');
  const sl = document.getElementById('shadow-log');
  tl.scrollTop = tl.scrollHeight;
  sl.scrollTop = sl.scrollHeight;
}

// ── Main render ──────────────────────────────────────────────────────────────
function render(data) {
  window._lastData = data;

  // Market badge
  const mb  = document.getElementById('market-badge');
  const mn  = document.getElementById('market-next');
  if (data.market.is_open) {
    mb.className = 'market-badge market-open';
    mb.textContent = '● MARKET OPEN';
    mn.textContent = '';
  } else {
    mb.className = 'market-badge market-closed';
    mb.textContent = '● CLOSED';
    mn.textContent = data.market.next ? `Opens in ${data.market.next}` : '';
  }

  renderProcs(data.processes);
  renderMetrics(data.trading);
  renderBotCards(data.trading, data.shadow);
  renderSys(data.system, data.processes);
  renderAgentTable(data.trading.agents, 'agent-table');
  renderAgentTable(data.shadow.agents,  'shadow-agent-table');
  renderLogs(data.trading_log, data.shadow_log);

  const ts = new Date(data.timestamp);
  document.getElementById('last-updated').textContent =
    `Last updated: ${ts.toLocaleString()} UTC`;
  document.body.classList.add('updated');
  setTimeout(() => document.body.classList.remove('updated'), 500);
}

// ── Fetch ────────────────────────────────────────────────────────────────────
async function fetchData() {
  try {
    const resp = await fetch('/api/data');
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    const data = await resp.json();
    render(data);
    countdown = 60;
  } catch(e) {
    console.error('fetchData error:', e);
  }
}

// Initial load
fetchData();
</script>
</body>
</html>"""

# ── HTTP Handler ───────────────────────────────────────────────────────────────

class Handler(BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        pass  # suppress per-request stdout noise

    def do_GET(self):
        if self.path in ('/', '/index.html'):
            body = DASHBOARD_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)

        elif self.path == '/api/data':
            try:
                data = build_data()
                body = json.dumps(data, default=str).encode('utf-8')
            except Exception as e:
                body = json.dumps({"error": str(e)}).encode('utf-8')
            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.send_header('Access-Control-Allow-Origin', '*')
            self.send_header('Cache-Control', 'no-cache')
            self.send_header('Content-Length', str(len(body)))
            self.end_headers()
            self.wfile.write(body)

        else:
            self.send_response(404)
            self.end_headers()

# ── Entry point ────────────────────────────────────────────────────────────────

def main():
    server = HTTPServer(('0.0.0.0', PORT), Handler)
    ts = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{ts}] Dashboard server listening on http://0.0.0.0:{PORT}", flush=True)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass

if __name__ == '__main__':
    main()
