[object Object]

← back to Macstudio1 Dashboard

feat: rich LLM bay panel — per-model rows + ollama process stats + installed list

684c80b8f7372d6ef8bae3cef71fbcf6e7e4d0ef · 2026-05-07 23:23:26 -0700 · SteveStudio2

Now-broadcasting panel expanded into a proper "LLM bay":
- Per-model rows showing name (gold) / param size / quantization /
  VRAM bytes / expires-in countdown (or "pinned" anchor symbol)
- Foot strip with ollama PID / OLLAMA CPU% / OLLAMA RAM MB / process
  uptime / installed model count + total disk
- Detects pinned models (Ollama uses year-2318 sentinel for
  keep_alive=-1) and shows them as "pinned" not a 3M-hour countdown

API additions:
- /api/status now includes ollama.installed[] from /api/tags merge
- ollama_procs[] from ps grep ollama with cpu/rss/etime

Currently shows: qwen3:14b 14.8B Q4_K_M 10.09 GB pinned
+ 6 installed models / 50.5 GB on disk
+ ollama PID, CPU%, RAM MB, uptime

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 684c80b8f7372d6ef8bae3cef71fbcf6e7e4d0ef
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Thu May 7 23:23:26 2026 -0700

    feat: rich LLM bay panel — per-model rows + ollama process stats + installed list
    
    Now-broadcasting panel expanded into a proper "LLM bay":
    - Per-model rows showing name (gold) / param size / quantization /
      VRAM bytes / expires-in countdown (or "pinned" anchor symbol)
    - Foot strip with ollama PID / OLLAMA CPU% / OLLAMA RAM MB / process
      uptime / installed model count + total disk
    - Detects pinned models (Ollama uses year-2318 sentinel for
      keep_alive=-1) and shows them as "pinned" not a 3M-hour countdown
    
    API additions:
    - /api/status now includes ollama.installed[] from /api/tags merge
    - ollama_procs[] from ps grep ollama with cpu/rss/etime
    
    Currently shows: qwen3:14b 14.8B Q4_K_M 10.09 GB pinned
    + 6 installed models / 50.5 GB on disk
    + ollama PID, CPU%, RAM MB, uptime
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 public/index.html | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
 server.js         | 63 +++++++++++++++++++++++++++++++++-------
 2 files changed, 137 insertions(+), 12 deletions(-)

diff --git a/public/index.html b/public/index.html
index 0fa096c..fc4f4ac 100644
--- a/public/index.html
+++ b/public/index.html
@@ -281,6 +281,44 @@ header.masthead {
   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 {
@@ -481,11 +519,13 @@ footer a { color: var(--gold); text-decoration: none; }
 
 <main class="dash">
 
-  <!-- LLM radio panel -->
+  <!-- LLM panel — Now Broadcasting + per-model rows + ollama proc stats -->
   <div class="radio">
-    <div class="radio-band">— now broadcasting —</div>
+    <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 -->
@@ -627,6 +667,24 @@ async function tick() {
     // 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;
@@ -638,13 +696,37 @@ async function tick() {
         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();
diff --git a/server.js b/server.js
index d7ca33d..3749c6e 100644
--- a/server.js
+++ b/server.js
@@ -73,20 +73,63 @@ async function readMemory() {
 
 async function readOllama() {
   try {
-    const res = await fetch(`${OLLAMA}/api/ps`, { signal: AbortSignal.timeout(2500) });
-    if (!res.ok) return { ok: false, status: res.status, models: [] };
-    const j = await res.json();
-    const models = (j.models || []).map(m => ({
+    const [psRes, tagsRes] = await Promise.all([
+      fetch(`${OLLAMA}/api/ps`,   { signal: AbortSignal.timeout(2500) }),
+      fetch(`${OLLAMA}/api/tags`, { signal: AbortSignal.timeout(2500) })
+    ]);
+    const ps = psRes.ok ? await psRes.json() : { models: [] };
+    const tags = tagsRes.ok ? await tagsRes.json() : { models: [] };
+
+    const models = (ps.models || []).map(m => ({
       name: m.name,
       size_bytes: m.size,
       size_vram_bytes: m.size_vram,
       expires_at: m.expires_at,
-      details: m.details
+      details: m.details,
+      digest: m.digest
+    }));
+
+    const installed = (tags.models || []).map(m => ({
+      name: m.name,
+      size_bytes: m.size,
+      modified_at: m.modified_at,
+      param_size: m.details?.parameter_size || '',
+      quantization: m.details?.quantization_level || '',
+      family: m.details?.family || ''
     }));
-    return { ok: true, models };
+
+    return {
+      ok: psRes.ok,
+      models,
+      installed,
+      total_loaded_vram_bytes: models.reduce((s, m) => s + (m.size_vram_bytes || 0), 0),
+      total_installed_disk_bytes: installed.reduce((s, m) => s + (m.size_bytes || 0), 0),
+      installed_count: installed.length,
+      loaded_count: models.length
+    };
   } catch (e) {
-    return { ok: false, error: String(e?.message || e), models: [] };
+    return { ok: false, error: String(e?.message || e), models: [], installed: [] };
+  }
+}
+
+// Ollama process resource usage — cross-ref with ps so we can answer
+// "the LLM is using X% CPU and Y MB RAM right now"
+async function readOllamaProc() {
+  const r = await execCmd("ps -Ao pid=,%cpu=,%mem=,rss=,etime=,comm= | grep -i 'ollama' | grep -v grep | head -5");
+  const procs = [];
+  for (const line of r.stdout.split('\n')) {
+    const m = line.match(/^\s*(\d+)\s+(\d+\.\d+)\s+(\d+\.\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/);
+    if (!m) continue;
+    procs.push({
+      pid: parseInt(m[1], 10),
+      cpu: parseFloat(m[2]),
+      mem: parseFloat(m[3]),
+      rss_mb: +(parseInt(m[4], 10) / 1024).toFixed(1),
+      etime: m[5],
+      command: m[6]
+    });
   }
+  return procs;
 }
 
 async function readPowerMode() {
@@ -99,8 +142,8 @@ async function readPowerMode() {
 
 app.get('/api/status', async (_req, res) => {
   try {
-    const [cpu, mem, oll, power] = await Promise.all([
-      readCpu(), readMemory(), readOllama(), readPowerMode()
+    const [cpu, mem, oll, power, ollProcs] = await Promise.all([
+      readCpu(), readMemory(), readOllama(), readPowerMode(), readOllamaProc()
     ]);
     if (oll.ok && oll.models.length > 0) {
       pushCall(oll.models[0].name, 0);
@@ -109,7 +152,7 @@ app.get('/api/status', async (_req, res) => {
     res.json({
       ts: Date.now(),
       hostname: require('os').hostname(),
-      cpu, memory: mem, ollama: oll, power_mode: power,
+      cpu, memory: mem, ollama: oll, ollama_procs: ollProcs, power_mode: power,
       recent_calls: recentCalls.slice(0, 10),
       uptime_s: Math.round(require('os').uptime())
     });

← 2afd088 fix: no-store HTML so new sections appear on reload  ·  back to Macstudio1 Dashboard  ·  feat: ticker-tape Ollama HTTP log feed below the gauges 78fa648 →