← 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
M public/index.htmlM server.js
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 →