← back to Macstudio2 Dashboard

public/index.html

1044 lines

<!doctype html>
<html lang="en">
<head>
    <link rel="icon" type="image/svg+xml" href="/favicon.svg">

<meta charset="utf-8">
<title>Mac Studio 2 · '57 Eldorado</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
@import url('https://fonts.googleapis.com/css2?family=Bowlby+One+SC&family=Bungee+Shade&family=Lobster&family=Special+Elite&family=JetBrains+Mono:wght@400;600&display=swap');

:root {
  --turq: #b22234;
  --turq-dark: #6b0e1f;
  --cream: #f4ecd8;
  --cream-shadow: #d8c9a0;
  --chrome-1: #f8f8f8;
  --chrome-2: #b6b6b6;
  --chrome-3: #686868;
  --red: #c8243a;
  --red-glow: #ff5060;
  --leather: #4d2c1d;
  --leather-light: #7d4a30;
  --black: #15110d;
  --gold: #d4b683;
}

* { box-sizing: border-box; }
html, body {
  margin: 0; min-height: 100vh;
  background:
    radial-gradient(ellipse at center top, rgba(74, 189, 197, 0.4), rgba(15, 17, 13, 0.85) 60%),
    linear-gradient(180deg, var(--cream) 0%, var(--turq) 60%, var(--turq-dark) 100%);
  font-family: 'JetBrains Mono', monospace;
  color: var(--black);
  overflow-x: hidden;
}

/* Era stripe — turquoise/cream two-tone divider that runs across the top */
.tail-fin {
  position: absolute; top: 0; left: 0; right: 0; height: 8px;
  background: linear-gradient(90deg, var(--turq) 0%, var(--cream) 50%, var(--turq) 100%);
  border-bottom: 2px solid var(--chrome-2);
  box-shadow: 0 0 12px rgba(212, 182, 131, 0.6);
}

header.masthead {
  text-align: center;
  padding: 36px 24px 18px;
  position: relative;
}

.brand {
  font-family: 'Lobster', cursive;
  font-size: 78px;
  color: var(--cream);
  text-shadow:
    -2px 0 0 var(--chrome-2),
    2px 0 0 var(--chrome-2),
    0 -2px 0 var(--chrome-2),
    0 2px 0 var(--chrome-2),
    0 0 18px rgba(212, 182, 131, 0.65),
    4px 6px 0 rgba(20, 17, 13, 0.4);
  letter-spacing: 0.02em;
  line-height: 0.95;
  margin: 0;
}
.brand .sub {
  display: block;
  font-family: 'Bowlby One SC', sans-serif;
  font-size: 18px;
  letter-spacing: 0.4em;
  color: var(--gold);
  text-shadow: 1px 1px 0 var(--black);
  margin-top: 6px;
}
.year-badge {
  display: inline-block;
  font-family: 'Bungee Shade', sans-serif;
  font-size: 22px;
  background: var(--cream);
  color: var(--red);
  padding: 4px 14px;
  border: 3px solid var(--chrome-2);
  border-radius: 24px;
  margin-top: 14px;
  box-shadow: inset 0 0 8px rgba(0,0,0,0.2), 0 4px 8px rgba(0,0,0,0.3);
}

/* The dashboard — looks like the chrome panel of a '57 Bel Air */
.dash {
  max-width: 1200px;
  margin: 32px auto 0;
  padding: 38px 32px 40px;
  background:
    linear-gradient(180deg, var(--cream) 0%, var(--cream-shadow) 100%);
  border: 5px solid var(--chrome-2);
  border-radius: 28px 28px 12px 12px;
  box-shadow:
    inset 0 0 0 2px var(--chrome-1),
    inset 0 0 28px rgba(120, 90, 50, 0.35),
    0 18px 40px rgba(0, 0, 0, 0.55),
    0 0 0 8px var(--turq) /* the turquoise paint surrounding the chrome */;
  position: relative;
}

/* Chrome trim line down the middle of the dash */
.dash::before {
  content: '';
  position: absolute;
  top: 8px; bottom: 8px;
  left: 50%;
  width: 4px;
  margin-left: -2px;
  background: linear-gradient(180deg, var(--chrome-1) 0%, var(--chrome-3) 50%, var(--chrome-1) 100%);
  border-radius: 2px;
  opacity: 0.55;
}

/* Gauge cluster */
.gauges {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 24px;
  align-items: center;
  margin-bottom: 36px;
}

.gauge {
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  max-width: 280px;
  margin: 0 auto;
  border-radius: 50%;
  background:
    radial-gradient(circle at 30% 30%, var(--chrome-1) 0%, var(--chrome-2) 30%, var(--chrome-3) 75%, var(--black) 100%);
  box-shadow:
    inset 0 0 0 8px var(--chrome-2),
    inset 0 0 0 12px var(--black),
    inset 0 0 30px rgba(0, 0, 0, 0.7),
    0 6px 16px rgba(0, 0, 0, 0.45);
}
.gauge .face {
  position: absolute;
  inset: 18px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 50% 30%, #2a3a3f 0%, #0a0e10 80%);
  border: 2px solid var(--chrome-3);
  overflow: hidden;
}
.gauge .face::before {
  /* tick marks */
  content: '';
  position: absolute;
  inset: 0;
  background:
    repeating-conic-gradient(
      from -135deg at 50% 50%,
      var(--chrome-1) 0deg 1deg, transparent 1deg 27deg
    );
  mask: radial-gradient(circle at 50% 50%, transparent 0 38%, black 38% 48%, transparent 48% 100%);
  -webkit-mask: radial-gradient(circle at 50% 50%, transparent 0 38%, black 38% 48%, transparent 48% 100%);
  opacity: 0.7;
}
.gauge .label {
  position: absolute;
  top: 28%;
  left: 0; right: 0;
  text-align: center;
  font-family: 'Special Elite', monospace;
  font-size: 11px;
  letter-spacing: 0.32em;
  color: var(--cream);
  text-transform: uppercase;
  z-index: 2;
}
.gauge .needle {
  position: absolute;
  bottom: 50%;
  left: 50%;
  width: 4px;
  height: 38%;
  margin-left: -2px;
  background: linear-gradient(180deg, var(--red-glow) 0%, var(--red) 70%, #6b0010 100%);
  transform-origin: 50% 100%;
  transform: rotate(-135deg);
  transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
  box-shadow: 0 0 6px var(--red), 0 0 14px rgba(255, 80, 96, 0.4);
  z-index: 3;
  border-radius: 2px;
}
.gauge .pin {
  position: absolute;
  width: 24px; height: 24px;
  border-radius: 50%;
  background: radial-gradient(circle at 30% 30%, var(--chrome-1), var(--chrome-3));
  top: 50%; left: 50%;
  margin: -12px 0 0 -12px;
  z-index: 4;
  box-shadow: 0 0 0 2px var(--black), 0 2px 4px rgba(0,0,0,0.6);
}
.gauge .value {
  position: absolute;
  bottom: 22%;
  left: 0; right: 0;
  text-align: center;
  font-family: 'Bowlby One SC', sans-serif;
  font-size: 36px;
  color: var(--cream);
  z-index: 5;
  text-shadow: 0 0 8px rgba(74, 189, 197, 0.6);
}
.gauge .units {
  position: absolute;
  bottom: 14%;
  left: 0; right: 0;
  text-align: center;
  font-family: 'Special Elite', monospace;
  font-size: 10px;
  letter-spacing: 0.3em;
  color: var(--cream);
  z-index: 5;
  opacity: 0.65;
}

/* LLM status panel — the radio dial */
.radio {
  margin-top: 8px;
  background:
    repeating-linear-gradient(180deg, rgba(0,0,0,0.04) 0px, transparent 1px, transparent 4px),
    linear-gradient(180deg, var(--leather-light) 0%, var(--leather) 100%);
  border: 4px solid var(--chrome-2);
  border-radius: 6px;
  padding: 18px 22px;
  box-shadow:
    inset 0 0 0 2px var(--chrome-1),
    inset 0 0 16px rgba(0, 0, 0, 0.45),
    0 4px 10px rgba(0, 0, 0, 0.4);
  position: relative;
  margin-bottom: 36px;
}
.radio::before, .radio::after {
  /* speaker grilles */
  content: '';
  position: absolute;
  top: 50%; transform: translateY(-50%);
  width: 36px; height: 60px;
  background:
    repeating-linear-gradient(0deg, var(--chrome-3) 0px, var(--chrome-3) 2px, var(--chrome-1) 2px, var(--chrome-1) 4px);
  border: 2px solid var(--chrome-1);
  border-radius: 4px;
}
.radio::before { left: 8px; }
.radio::after  { right: 8px; }
.radio-band {
  text-align: center;
  font-family: 'Special Elite', monospace;
  font-size: 11px;
  letter-spacing: 0.4em;
  color: var(--cream);
  margin-bottom: 8px;
  text-transform: uppercase;
}
.station {
  text-align: center;
  font-family: 'Bowlby One SC', sans-serif;
  font-size: 28px;
  color: var(--gold);
  text-shadow: 0 0 14px rgba(212, 182, 131, 0.7);
  letter-spacing: 0.04em;
  margin: 4px 0;
  min-height: 36px;
}
.station.idle { color: var(--chrome-3); text-shadow: none; opacity: 0.65; }
.subtitle {
  text-align: center;
  font-family: 'Special Elite', monospace;
  font-size: 11px;
  letter-spacing: 0.18em;
  color: var(--cream);
  text-transform: uppercase;
  opacity: 0.85;
}
.llm-rows {
  margin: 14px 32px 0;
  border-top: 1px dotted rgba(212, 182, 131, 0.4);
  padding-top: 10px;
}
.llm-row {
  display: grid;
  grid-template-columns: 1.4fr 0.6fr 0.7fr 0.9fr 1fr;
  gap: 10px;
  padding: 6px 0;
  border-bottom: 1px dotted rgba(212, 182, 131, 0.18);
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  color: var(--cream);
  align-items: center;
}
.llm-row:last-child { border-bottom: none; }
.llm-row .nm { color: var(--gold); font-weight: 600; }
.llm-row .num { text-align: right; font-variant-numeric: tabular-nums; }
.llm-row .pinned { color: var(--turq); }
.llm-row .pinned::before { content: '⚓ '; }
.llm-row .countdown { color: var(--red-glow); }
.llm-foot {
  margin: 8px 32px 0;
  padding-top: 8px;
  border-top: 1px dotted rgba(212, 182, 131, 0.4);
  font-family: 'Special Elite', monospace;
  font-size: 10px;
  letter-spacing: 0.18em;
  color: var(--gold);
  text-transform: uppercase;
  text-align: center;
  display: flex;
  gap: 18px;
  justify-content: center;
  flex-wrap: wrap;
}
.llm-foot span b { color: var(--cream); font-family: 'JetBrains Mono', monospace; font-weight: 600; }

/* Push buttons — chrome cigarette-lighter style */
.controls {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 18px;
  margin-top: 8px;
}
.btn {
  background:
    radial-gradient(circle at 30% 25%, var(--chrome-1) 0%, var(--chrome-2) 50%, var(--chrome-3) 100%);
  border: none;
  border-radius: 14px;
  padding: 22px 14px 18px;
  cursor: pointer;
  font-family: 'Bowlby One SC', sans-serif;
  font-size: 16px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--leather);
  box-shadow:
    inset 0 -3px 6px rgba(0, 0, 0, 0.35),
    inset 0 3px 6px rgba(255, 255, 255, 0.6),
    0 6px 12px rgba(0, 0, 0, 0.45);
  position: relative;
  transition: transform 0.1s ease, box-shadow 0.15s ease;
  overflow: hidden;
}
.btn:active {
  transform: translateY(2px);
  box-shadow:
    inset 0 -1px 3px rgba(0, 0, 0, 0.35),
    inset 0 1px 3px rgba(255, 255, 255, 0.6),
    0 2px 4px rgba(0, 0, 0, 0.4);
}
.btn .light {
  display: block;
  width: 14px; height: 14px;
  border-radius: 50%;
  background: radial-gradient(circle at 30% 30%, #6b0010 0%, #3a000a 80%);
  margin: 0 auto 10px;
  box-shadow: inset 0 0 4px rgba(0,0,0,0.6);
  transition: background 0.2s ease, box-shadow 0.2s ease;
}
.btn.active .light {
  background: radial-gradient(circle at 30% 30%, var(--red-glow) 0%, var(--red) 70%);
  box-shadow: 0 0 12px var(--red-glow), inset 0 0 6px var(--red-glow);
  animation: pulse 1.2s infinite alternate;
}
@keyframes pulse {
  from { opacity: 0.85; }
  to   { opacity: 1; }
}
.btn .sub-label {
  display: block;
  font-family: 'Special Elite', monospace;
  font-size: 9px;
  letter-spacing: 0.18em;
  margin-top: 4px;
  color: var(--leather);
  opacity: 0.75;
  text-transform: uppercase;
}
.btn[data-mode="hyperdrive"] { border: 3px solid var(--red); }
.btn[data-mode="max-ram"]    { border: 3px solid var(--turq-dark); }
.btn[data-mode="idle"]       { border: 3px solid var(--chrome-3); }

/* Recent activity strip — like an odometer */
.odometer {
  margin-top: 32px;
  padding: 12px 18px;
  background: var(--black);
  border: 3px solid var(--chrome-2);
  border-radius: 6px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px;
  color: #d4ffd4;
  box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.7);
  letter-spacing: 0.08em;
}
.odometer .digit {
  font-family: 'Bowlby One SC', sans-serif;
  font-size: 20px;
  color: var(--gold);
  letter-spacing: 0.16em;
}

/* Tach history strip — under chrome bezel, tachometer aesthetic */
.hist {
  margin-bottom: 24px;
  background: linear-gradient(180deg, #0e1314 0%, #181f20 100%);
  border: 4px solid var(--chrome-2);
  border-radius: 6px;
  padding: 12px 18px;
  box-shadow: inset 0 0 0 2px var(--chrome-1), inset 0 0 14px rgba(0,0,0,0.6), 0 4px 8px rgba(0,0,0,0.35);
}
.hist-title {
  text-align: center;
  font-family: 'Special Elite', monospace;
  font-size: 10px;
  letter-spacing: 0.32em;
  color: var(--gold);
  margin-bottom: 8px;
  text-transform: uppercase;
}
.hist-row {
  display: grid;
  grid-template-columns: 60px 1fr 60px;
  align-items: center;
  gap: 12px;
  padding: 4px 0;
}
.hist-label {
  font-family: 'Bowlby One SC', sans-serif;
  font-size: 13px;
  letter-spacing: 0.18em;
  color: var(--cream);
  text-transform: uppercase;
}
.hist-now {
  font-family: 'JetBrains Mono', monospace;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--metal-glow);
  text-align: right;
  font-size: 14px;
}
.hist-svg {
  width: 100%;
  height: 38px;
  background:
    repeating-linear-gradient(0deg, rgba(212,182,131,0.08) 0px, rgba(212,182,131,0.08) 1px, transparent 1px, transparent 19px),
    repeating-linear-gradient(90deg, rgba(212,182,131,0.06) 0px, rgba(212,182,131,0.06) 1px, transparent 1px, transparent 30px);
  border: 1px solid var(--chrome-3);
  border-radius: 2px;
}

/* Engine bay process table — chrome trim, leather background, brass header */
.proc-table {
  margin-top: 32px;
  background: linear-gradient(180deg, var(--leather-light) 0%, var(--leather) 100%);
  border: 4px solid var(--chrome-2);
  border-radius: 6px;
  padding: 16px 18px 18px;
  box-shadow:
    inset 0 0 0 2px var(--chrome-1),
    inset 0 0 16px rgba(0, 0, 0, 0.5),
    0 4px 10px rgba(0, 0, 0, 0.4);
}
.proc-head {
  display: flex; justify-content: space-between; align-items: baseline;
  border-bottom: 2px solid var(--chrome-2);
  padding-bottom: 10px; margin-bottom: 10px;
}
.proc-head h3 {
  margin: 0;
  font-family: 'Lobster', cursive;
  font-size: 26px;
  color: var(--cream);
  text-shadow: 0 0 10px rgba(212, 182, 131, 0.5);
}
.proc-count {
  font-family: 'Special Elite', monospace;
  font-size: 11px;
  letter-spacing: 0.28em;
  color: var(--gold);
  text-transform: uppercase;
}
.proc-table table {
  width: 100%;
  border-collapse: collapse;
  font-family: 'JetBrains Mono', monospace;
  font-size: 12px;
  color: var(--cream);
}
.proc-table thead th {
  position: sticky; top: 0;
  text-align: left;
  padding: 8px 10px;
  font-family: 'Special Elite', monospace;
  font-size: 10px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--gold);
  border-bottom: 1px solid var(--chrome-3);
  background: var(--leather);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
.proc-table thead th:hover { color: var(--cream); }
.proc-table thead th.active { color: var(--turq); }
.proc-table thead th.active::after {
  content: ' ▾';
  color: var(--turq);
}
.proc-table thead th.active.asc::after { content: ' ▴'; }
.proc-table tbody tr {
  border-bottom: 1px dotted rgba(212, 182, 131, 0.18);
  transition: background 0.15s;
}
.proc-table tbody tr:hover { background: rgba(74, 189, 197, 0.12); }
.proc-table tbody td {
  padding: 5px 10px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 280px;
}
.proc-table td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--gold);
}
.proc-table tbody td.cmd {
  font-family: 'Special Elite', monospace;
  color: var(--cream);
  max-width: 380px;
}
.proc-table tbody tr.hot {
  background: rgba(200, 36, 58, 0.18);
}

/* Ollama live log — ticker-tape style under the gauges */
.ollama-log {
  margin-top: 32px;
  background: linear-gradient(180deg, var(--leather-light) 0%, var(--leather) 100%);
  border: 4px solid var(--chrome-2);
  border-radius: 6px;
  padding: 16px 18px 18px;
  box-shadow:
    inset 0 0 0 2px var(--chrome-1),
    inset 0 0 16px rgba(0, 0, 0, 0.5),
    0 4px 10px rgba(0, 0, 0, 0.4);
}
.log-feed {
  max-height: 280px;
  overflow-y: auto;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  line-height: 1.55;
  background: var(--black);
  border: 1px solid var(--chrome-3);
  padding: 10px 14px;
  color: #d4ffd4;
}
.log-feed .row { padding: 2px 0; border-bottom: 1px dotted rgba(212, 182, 131, 0.08); }
.log-feed .row .ts { color: var(--gold); margin-right: 8px; }
.log-feed .row .status { display: inline-block; padding: 0 6px; border-radius: 2px; font-weight: 600; margin-right: 6px; }
.log-feed .row .status.s2xx { background: #2a7c4a; color: #fff; }
.log-feed .row .status.s4xx { background: #b8860b; color: #fff; }
.log-feed .row .status.s5xx { background: #c8243a; color: #fff; }
.log-feed .row .method { color: var(--turq); margin-right: 6px; }
.log-feed .row .path { color: var(--cream); }
.log-feed .row .dur { color: var(--metal-glow); margin-left: 8px; }
.log-feed .row.event { color: #b8b8b8; font-style: italic; }
.log-feed .row.event .lvl { color: var(--turq); margin-right: 6px; }
.log-feed .row.raw { color: var(--ink-mute); }

footer {
  text-align: center;
  padding: 32px 24px 64px;
  font-family: 'Special Elite', monospace;
  font-size: 11px;
  letter-spacing: 0.28em;
  color: var(--cream);
  text-transform: uppercase;
  opacity: 0.7;
}
footer a { color: var(--gold); text-decoration: none; }
</style>
</head>
<body>

<div class="tail-fin"></div>

<header class="masthead">
  <h1 class="brand">Mac Studio<br><span class="sub">— TWO —</span></h1>
  <span class="year-badge">'57 Eldorado</span>
  <div style="margin-top:8px;font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:.32em;color:rgba(212,182,131,0.7);text-transform:uppercase">build v0.4 · scroll for engine bay + ticker tape</div>
</header>

<main class="dash">

  <!-- Tachometer-style history strip — last 60 ticks of CPU% + RAM% -->
  <div class="hist">
    <div class="hist-title">— TACH · last 60 seconds —</div>
    <div class="hist-row">
      <span class="hist-label">CPU</span>
      <svg id="hist-cpu" class="hist-svg" viewBox="0 0 360 60" preserveAspectRatio="none"></svg>
      <span class="hist-now" id="hist-cpu-now">0%</span>
    </div>
    <div class="hist-row">
      <span class="hist-label">RAM</span>
      <svg id="hist-ram" class="hist-svg" viewBox="0 0 360 60" preserveAspectRatio="none"></svg>
      <span class="hist-now" id="hist-ram-now">0%</span>
    </div>
  </div>

  <!-- LLM panel — Now Broadcasting + per-model rows + ollama proc stats -->
  <div class="radio">
    <div class="radio-band">— LLM bay · now broadcasting —</div>
    <div class="station idle" id="station">engine off</div>
    <div class="subtitle" id="station-sub">no model loaded · standing by</div>
    <div id="llm-rows" class="llm-rows"></div>
    <div id="llm-foot" class="llm-foot"></div>
  </div>

  <!-- Three round gauges -->
  <div class="gauges">
    <div class="gauge" data-key="cpu">
      <div class="face"></div>
      <div class="label">CPU</div>
      <div class="needle" data-needle="cpu"></div>
      <div class="value" data-value="cpu">0</div>
      <div class="units">%</div>
      <div class="pin"></div>
    </div>
    <div class="gauge" data-key="ram">
      <div class="face"></div>
      <div class="label">MEMORY</div>
      <div class="needle" data-needle="ram"></div>
      <div class="value" data-value="ram">0</div>
      <div class="units">%</div>
      <div class="pin"></div>
    </div>
    <div class="gauge" data-key="vram">
      <div class="face"></div>
      <div class="label">V·RAM</div>
      <div class="needle" data-needle="vram"></div>
      <div class="value" data-value="vram">0</div>
      <div class="units">GB</div>
      <div class="pin"></div>
    </div>
  </div>

  <!-- last test result lozenge -->
  <div id="test-result" style="margin-bottom:10px;text-align:center;font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--gold);min-height:18px"></div>

  <!-- Push-button shifter / hyperdrive controls (5 columns) -->
  <div class="controls" style="grid-template-columns: repeat(5, 1fr)">
    <button class="btn" data-mode="test" onclick="trigger('test', this)" style="border:3px solid var(--turq-dark)" title="Fires a tiny prompt at the loaded model, times the round-trip. Use to compare before/after BOOST.">
      <span class="light"></span>
      🏁 Test
      <span class="sub-label">prompt + latency check</span>
    </button>
    <button class="btn" data-mode="boost" onclick="trigger('boost', this)" style="border:3px solid var(--metal-glow)" title="Restart ollama with NUM_PARALLEL=4, MAX_LOADED=2, FLASH_ATTENTION, q4_0 KV cache, KEEP_ALIVE=-1. Big-deal — kills in-flight calls.">
      <span class="light"></span>
      🏎️ Boost
      <span class="sub-label">restart w/ 4x parallel · q4 cache</span>
    </button>
    <button class="btn" data-mode="hyperdrive" onclick="trigger('hyperdrive', this)">
      <span class="light"></span>
      🚀 Hyperdrive
      <span class="sub-label">high power · pin models</span>
    </button>
    <button class="btn" data-mode="max-ram" onclick="trigger('max-ram', this)">
      <span class="light"></span>
      💪 Max RAM
      <span class="sub-label">load all · keep alive</span>
    </button>
    <button class="btn" data-mode="idle" onclick="trigger('idle', this)">
      <span class="light"></span>
      🛑 Idle
      <span class="sub-label">unload · default power</span>
    </button>
  </div>

  <!-- Odometer-style activity counter -->
  <div class="odometer">
    <span>UPTIME</span>
    <span class="digit" id="odo-uptime">000000</span>
    <span>POWER MODE</span>
    <span class="digit" id="odo-power">AUTO</span>
    <span>CALLS</span>
    <span class="digit" id="odo-calls">0000</span>
    <span>👀 VIEWERS</span>
    <span class="digit" id="odo-viewers">0</span>
    <span>LAST POLL</span>
    <span class="digit" id="odo-poll">—</span>
  </div>

  <!-- Ollama live log tail — what's actually hitting the server -->
  <section class="ollama-log">
    <div class="proc-head">
      <h3>Ticker tape · Ollama HTTP traffic</h3>
      <span class="proc-count" id="log-count">— lines</span>
    </div>
    <div id="log-feed" class="log-feed"></div>
  </section>

  <!-- Live process list -->
  <section class="proc-table">
    <div class="proc-head">
      <h3>Engine bay · live processes</h3>
      <span class="proc-count" id="proc-count">— running</span>
    </div>
    <table id="proc">
      <thead>
        <tr>
          <th data-sort="pid">PID</th>
          <th data-sort="user">USER</th>
          <th data-sort="cpu" class="active desc">CPU %</th>
          <th data-sort="mem">MEM %</th>
          <th data-sort="rss_mb">RSS (MB)</th>
          <th data-sort="time">TIME</th>
          <th data-sort="command">COMMAND</th>
        </tr>
      </thead>
      <tbody></tbody>
    </table>
  </section>

</main>

<footer>
  Mac Studio 1 · Sherman Oaks · &lt;a href="http://localhost:9930"&gt;localhost:9930&lt;/a&gt; ·
  polling /api/status every 1.2 s
</footer>

<script>
const $ = (s, root = document) => root.querySelector(s);

const GAUGE_RANGE = {
  // -135deg = full counter-clockwise (0), +135deg = full clockwise (max)
  cpu: { min: 0, max: 100 },
  ram: { min: 0, max: 100 },
  vram: { min: 0, max: 64 } // M3 Ultra hits 192GB but most LLMs cap ~30-60GB VRAM
};

function setGauge(key, value, displayValue, displayUnits) {
  const range = GAUGE_RANGE[key];
  const clamped = Math.max(range.min, Math.min(range.max, value));
  const t = (clamped - range.min) / (range.max - range.min);
  const angle = -135 + t * 270;
  const needle = document.querySelector(`[data-needle="${key}"]`);
  if (needle) needle.style.transform = `rotate(${angle}deg)`;
  const valueEl = document.querySelector(`[data-value="${key}"]`);
  if (valueEl) valueEl.textContent = displayValue;
  if (displayUnits) {
    const u = valueEl?.parentElement?.querySelector('.units');
    if (u) u.textContent = displayUnits;
  }
}

function fmtUptime(secs) {
  const d = Math.floor(secs / 86400);
  const h = Math.floor((secs % 86400) / 3600);
  const m = Math.floor((secs % 3600) / 60);
  if (d > 0) return `${d}d ${h}h`;
  if (h > 0) return `${h}h ${String(m).padStart(2,'0')}m`;
  return `${m}m ${String(secs % 60).padStart(2,'0')}s`;
}

let CALL_COUNT = 0;
let LAST_MODEL = null;
const HIST_LEN = 60;
const histCpu = new Array(HIST_LEN).fill(0);
const histRam = new Array(HIST_LEN).fill(0);

function paintHist(svgId, data, color) {
  const svg = document.getElementById(svgId);
  if (!svg) return;
  const W = 360, H = 60;
  const max = 100;
  const stepX = W / (HIST_LEN - 1);
  const points = data.map((v, i) => `${(i * stepX).toFixed(1)},${(H - (v / max) * H).toFixed(1)}`).join(' ');
  const polyArea = `M0,${H} L${points.split(' ').join(' L')} L${W},${H} Z`;
  svg.innerHTML = `
    <path d="${polyArea}" fill="${color}" fill-opacity="0.22"/>
    <polyline points="${points}" fill="none" stroke="${color}" stroke-width="1.6"/>
    <circle cx="${(HIST_LEN - 1) * stepX}" cy="${H - (data[data.length-1] / max) * H}" r="2.5" fill="${color}"/>
  `;
}

async function tick() {
  try {
    const r = await fetch('/api/status').then(r => r.json());

    // CPU + RAM gauges
    setGauge('cpu', r.cpu.total || 0, Math.round(r.cpu.total || 0));
    setGauge('ram', r.memory.used_pct || 0, Math.round(r.memory.used_pct || 0));

    // Push to history ring buffers + paint
    histCpu.shift(); histCpu.push(r.cpu.total || 0);
    histRam.shift(); histRam.push(r.memory.used_pct || 0);
    paintHist('hist-cpu', histCpu, '#c8243a');
    paintHist('hist-ram', histRam, '#4abdc5');
    document.getElementById('hist-cpu-now').textContent = (r.cpu.total || 0).toFixed(1) + '%';
    document.getElementById('hist-ram-now').textContent = (r.memory.used_pct || 0).toFixed(1) + '%';

    // VRAM = sum of size_vram_bytes across loaded models
    const vramBytes = (r.ollama.models || []).reduce((s, m) => s + (m.size_vram_bytes || 0), 0);
    const vramGB = vramBytes / (1024 ** 3);
    setGauge('vram', vramGB, vramGB.toFixed(1));

    // Radio panel — current LLM
    const station = document.getElementById('station');
    const sub = document.getElementById('station-sub');
    const llmRows = document.getElementById('llm-rows');
    const llmFoot = document.getElementById('llm-foot');
    const fmtBytes = (b) => {
      if (!b) return '0 GB';
      const gb = b / (1024 ** 3);
      return gb >= 1 ? gb.toFixed(2) + ' GB' : (b / (1024 ** 2)).toFixed(0) + ' MB';
    };
    const fmtCountdown = (iso) => {
      if (!iso) return '<span class="pinned">pinned</span>';
      const sec = Math.round((new Date(iso).getTime() - Date.now()) / 1000);
      if (sec < 0) return '<span class="countdown">expired</span>';
      // Ollama uses year-2318 for keep_alive=-1 (effectively forever).
      // Anything >180 days out is "pinned" not a countdown.
      if (sec > 180 * 86400) return '<span class="pinned">pinned</span>';
      if (sec < 60) return '<span class="countdown">' + sec + 's</span>';
      if (sec < 3600) return Math.floor(sec / 60) + 'm ' + (sec % 60) + 's';
      return Math.floor(sec / 3600) + 'h ' + Math.floor((sec % 3600) / 60) + 'm';
    };
    if (r.ollama.models && r.ollama.models.length > 0) {
      const m = r.ollama.models[0];
      station.textContent = m.name;
      station.classList.remove('idle');
      const more = r.ollama.models.length > 1 ? ` · +${r.ollama.models.length - 1} more loaded` : '';
      const expSec = m.expires_at ? Math.max(0, Math.round((new Date(m.expires_at).getTime() - Date.now()) / 1000)) : -1;
      sub.textContent = `${(m.details && m.details.parameter_size) || ''} · ${(vramGB).toFixed(1)} GB VRAM${expSec >= 0 ? ' · expires in ' + expSec + 's' : ' · pinned'}${more}`;
      if (m.name !== LAST_MODEL) {
        CALL_COUNT++;
        LAST_MODEL = m.name;
      }

      // Per-model rows
      llmRows.innerHTML = r.ollama.models.map(mm => {
        const det = mm.details || {};
        return `<div class="llm-row">
          <span class="nm">${mm.name}</span>
          <span>${det.parameter_size || ''}</span>
          <span>${det.quantization_level || ''}</span>
          <span class="num">${fmtBytes(mm.size_vram_bytes)}</span>
          <span class="num">${fmtCountdown(mm.expires_at)}</span>
        </div>`;
      }).join('');
    } else {
      station.textContent = 'engine off';
      station.classList.add('idle');
      sub.textContent = 'no model loaded · standing by';
      llmRows.innerHTML = '';
      LAST_MODEL = null;
    }

    // Foot stats — ollama process CPU/RAM + installed model count + total disk
    const ollProc = (r.ollama_procs || []).find(p => /ollama/i.test(p.command)) || (r.ollama_procs || [])[0];
    const installedCount = r.ollama.installed_count || 0;
    const installedDisk = r.ollama.total_installed_disk_bytes || 0;
    const ollPid = ollProc ? `<span>PID <b>${ollProc.pid}</b></span>` : '';
    const ollCpu = ollProc ? `<span>OLLAMA CPU <b>${ollProc.cpu.toFixed(1)}%</b></span>` : '';
    const ollMem = ollProc ? `<span>OLLAMA RAM <b>${ollProc.rss_mb.toFixed(0)} MB</b></span>` : '';
    const ollUp  = ollProc ? `<span>UP <b>${ollProc.etime}</b></span>` : '';
    const inst   = `<span>INSTALLED <b>${installedCount}</b> models · <b>${(installedDisk / (1024 ** 3)).toFixed(1)} GB</b></span>`;
    llmFoot.innerHTML = [ollPid, ollCpu, ollMem, ollUp, inst].filter(Boolean).join('');

    // Odometer
    document.getElementById('odo-uptime').textContent = fmtUptime(r.uptime_s || 0);
    document.getElementById('odo-power').textContent = (r.power_mode || 'auto').toUpperCase();
    document.getElementById('odo-calls').textContent = String(CALL_COUNT).padStart(4, '0');

  } catch (e) {
    console.warn('status fetch failed', e);
  }
}

async function trigger(mode, btn) {
  document.querySelectorAll('.btn').forEach(b => b.classList.remove('active'));
  btn.classList.add('active');
  const resEl = document.getElementById('test-result');
  if (resEl && mode === 'test') resEl.textContent = '… firing test prompt …';
  try {
    const r = await fetch('/api/control/' + mode, { method: 'POST' });
    const j = await r.json();
    console.log('[control]', mode, j);
    if (mode === 'test' && resEl) {
      if (j.ok) {
        resEl.style.color = 'var(--gold)';
        resEl.textContent = `🏁 ${j.model} · ${j.ms}ms wall · ${j.tokens_per_s ? j.tokens_per_s + ' tok/s · ' : ''}reply: "${(j.reply||'').slice(0,40)}"`;
      } else {
        resEl.style.color = '#c47b7b';
        resEl.textContent = `× test failed · ${j.status||''} · ${(j.error||j.reply||'unknown').slice(0,80)}`;
      }
    }
    setTimeout(() => tick(), 400);
  } catch (e) {
    console.warn('control failed', e);
    btn.classList.remove('active');
    if (resEl && mode === 'test') {
      resEl.style.color = '#c47b7b';
      resEl.textContent = '× network error: ' + (e.message || e);
    }
  }
}

tick();
setInterval(tick, 1200);

// ─── Live process table ────────────────────────────────────────────────
let PROC_ROWS = [];
let PROC_SORT = { col: 'cpu', dir: 'desc' };

function fmtNum(n, dec) { return Number(n).toFixed(dec === undefined ? 1 : dec); }

function renderProcs() {
  const tbody = document.querySelector('#proc tbody');
  if (!tbody) return;
  const rows = [...PROC_ROWS];
  rows.sort((a, b) => {
    let va = a[PROC_SORT.col], vb = b[PROC_SORT.col];
    if (typeof va === 'string' && typeof vb === 'string') {
      return PROC_SORT.dir === 'asc' ? va.localeCompare(vb) : vb.localeCompare(va);
    }
    return PROC_SORT.dir === 'asc' ? (va - vb) : (vb - va);
  });
  tbody.innerHTML = rows.map(r => {
    const hot = (r.cpu >= 50 || r.mem >= 10) ? ' class="hot"' : '';
    const escCmd = String(r.command).replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
    return `<tr${hot}>
      <td class="num">${r.pid}</td>
      <td>${r.user}</td>
      <td class="num">${fmtNum(r.cpu)}</td>
      <td class="num">${fmtNum(r.mem)}</td>
      <td class="num">${fmtNum(r.rss_mb, 0)}</td>
      <td>${r.time}</td>
      <td class="cmd" title="${escCmd}">${escCmd}</td>
    </tr>`;
  }).join('');

  // Sort indicators on headers
  document.querySelectorAll('#proc thead th').forEach(th => {
    th.classList.remove('active', 'asc', 'desc');
    if (th.dataset.sort === PROC_SORT.col) {
      th.classList.add('active', PROC_SORT.dir);
    }
  });
}

document.querySelectorAll('#proc thead th').forEach(th => {
  th.addEventListener('click', () => {
    const col = th.dataset.sort;
    if (!col) return;
    if (PROC_SORT.col === col) {
      PROC_SORT.dir = PROC_SORT.dir === 'asc' ? 'desc' : 'asc';
    } else {
      PROC_SORT.col = col;
      PROC_SORT.dir = (col === 'pid' || col === 'user' || col === 'command') ? 'asc' : 'desc';
    }
    renderProcs();
  });
});

async function tickProcs() {
  try {
    const r = await fetch('/api/processes').then(r => r.json());
    PROC_ROWS = r.rows || [];
    document.getElementById('proc-count').textContent = `${PROC_ROWS.length} running`;
    renderProcs();
  } catch (e) { /* network blip */ }
}

tickProcs();
setInterval(tickProcs, 2500);

// ─── Live Ollama log feed ──────────────────────────────────────────────
async function tickLog() {
  try {
    const r = await fetch('/api/ollama/log?lines=30').then(r => r.json());
    const feed = document.getElementById('log-feed');
    document.getElementById('log-count').textContent = `${r.count} lines`;
    if (!feed) return;
    const escTxt = s => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
    feed.innerHTML = r.lines.map(L => {
      if (L.kind === 'http') {
        const sClass = L.status >= 500 ? 's5xx' : L.status >= 400 ? 's4xx' : 's2xx';
        return `<div class="row"><span class="ts">${escTxt(L.ts)}</span><span class="status ${sClass}">${L.status}</span><span class="method">${escTxt(L.method)}</span><span class="path">${escTxt(L.path)}</span><span class="dur">${escTxt(L.dur)}</span></div>`;
      }
      if (L.kind === 'event') {
        return `<div class="row event"><span class="ts">${escTxt(L.ts)}</span><span class="lvl">${L.level}</span>${escTxt(L.msg)}</div>`;
      }
      return `<div class="row raw">${escTxt(L.line)}</div>`;
    }).reverse().join('');
  } catch (e) { /* network blip */ }
}
tickLog();
setInterval(tickLog, 3000);

// ─── Self-healing version watch — auto-reload if server has new HTML ────
let LOADED_VERSION = null;
async function checkVersion() {
  try {
    const r = await fetch('/api/version').then(r => r.json());
    if (LOADED_VERSION === null) {
      LOADED_VERSION = r.version;
      console.log('[dashboard] loaded version', LOADED_VERSION);
      return;
    }
    if (r.version && r.version !== LOADED_VERSION) {
      console.log('[dashboard] new version', r.version, '— reloading in 2s');
      // Brief flash so Steve sees a deploy happened
      const banner = document.createElement('div');
      banner.textContent = 'New build deployed · reloading…';
      banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:9999;background:#d4b683;color:#0a0a0c;font-family:JetBrains Mono,monospace;font-weight:600;font-size:13px;letter-spacing:.18em;text-align:center;padding:14px;box-shadow:0 4px 12px rgba(0,0,0,0.6);text-transform:uppercase';
      document.body.appendChild(banner);
      setTimeout(() => { location.reload(true); }, 2000);
    }
  } catch (e) { /* network blip */ }
}
checkVersion();
setInterval(checkVersion, 5000);
</script>

</body>
</html>