← back to Macstudio1 Dashboard
feat: ticker-tape Ollama HTTP log feed below the gauges
78fa648b54c48fd5d37c810a784812cbc248c4c9 · 2026-05-07 23:25:19 -0700 · SteveStudio2
Live tail of /tmp/ollama.log (where the running ollama process writes
its stdout/stderr per lsof). Parses GIN-format HTTP rows + structured
events into a ticker-tape feed:
- HTTP rows: gold timestamp, color-coded status pill (green 2xx /
amber 4xx / red 5xx), turquoise method, cream path, gold duration
- Event rows: italic gray, with level prefix
- Raw rows: dim fallback
Endpoint:
GET /api/ollama/log?lines=N parsed tail of /tmp/ollama.log,
falls back to ~/.ollama/logs/server.log
Refreshes every 3s, scrolls newest-first, max-height 280px so it
fits the dash.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M public/index.htmlM server.js
Diff
commit 78fa648b54c48fd5d37c810a784812cbc248c4c9
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Thu May 7 23:25:19 2026 -0700
feat: ticker-tape Ollama HTTP log feed below the gauges
Live tail of /tmp/ollama.log (where the running ollama process writes
its stdout/stderr per lsof). Parses GIN-format HTTP rows + structured
events into a ticker-tape feed:
- HTTP rows: gold timestamp, color-coded status pill (green 2xx /
amber 4xx / red 5xx), turquoise method, cream path, gold duration
- Event rows: italic gray, with level prefix
- Raw rows: dim fallback
Endpoint:
GET /api/ollama/log?lines=N parsed tail of /tmp/ollama.log,
falls back to ~/.ollama/logs/server.log
Refreshes every 3s, scrolls newest-first, max-height 280px so it
fits the dash.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
public/index.html | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
server.js | 51 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 119 insertions(+)
diff --git a/public/index.html b/public/index.html
index fc4f4ac..f907eb3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -495,6 +495,42 @@ header.masthead {
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;
@@ -585,6 +621,15 @@ footer a { color: var(--gold); text-decoration: none; }
<span class="digit" id="odo-calls">0000</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">
@@ -820,6 +865,29 @@ async function tickProcs() {
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 => ({'<':'<','>':'>','&':'&','"':'"'}[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);
</script>
</body>
diff --git a/server.js b/server.js
index 3749c6e..a268dfb 100644
--- a/server.js
+++ b/server.js
@@ -237,6 +237,57 @@ app.get('/api/processes', async (_req, res) => {
}
});
+// Live tail of /tmp/ollama.log — actual prompt/inference activity.
+// Returns last N lines, parsed where possible into structured records.
+app.get('/api/ollama/log', async (req, res) => {
+ try {
+ const lines = Math.min(parseInt(String(req.query.lines || '40'), 10) || 40, 200);
+ const candidates = ['/tmp/ollama.log', '/private/tmp/ollama.log'];
+ let raw = '';
+ for (const p of candidates) {
+ try { raw = await fs.promises.readFile(p, 'utf8'); break; } catch {}
+ }
+ if (!raw) {
+ // Fallback: ~/.ollama/logs/server.log (older installs)
+ const home = process.env.HOME || '';
+ try { raw = await fs.promises.readFile(`${home}/.ollama/logs/server.log`, 'utf8'); } catch {}
+ }
+ const all = raw.split(/\r?\n/).filter(Boolean);
+ const tail = all.slice(-lines);
+ const parsed = tail.map(line => {
+ // [GIN] 2026/05/07 - 21:32:14 | 200 | 12.341s | 127.0.0.1 | POST "/api/chat"
+ const gin = line.match(/^\[GIN\]\s+(\S+)\s+-\s+(\S+)\s+\|\s+(\d+)\s+\|\s+([^|]+)\|\s+(\S+)\s+\|\s+(\S+)\s+"([^"]+)"/);
+ if (gin) {
+ return {
+ kind: 'http',
+ ts: gin[1] + ' ' + gin[2],
+ status: parseInt(gin[3], 10),
+ dur: gin[4].trim(),
+ ip: gin[5],
+ method: gin[6],
+ path: gin[7]
+ };
+ }
+ // time=... level=INFO source=foo.go:NN msg="..."
+ const ev = line.match(/time=(\S+)\s+level=(\w+)\s+source=(\S+)\s+msg="?([^"]+)"?/);
+ if (ev) {
+ return {
+ kind: 'event',
+ ts: ev[1],
+ level: ev[2],
+ source: ev[3],
+ msg: ev[4].slice(0, 200)
+ };
+ }
+ return { kind: 'raw', line: line.slice(0, 200) };
+ });
+ res.set('Cache-Control', 'no-store');
+ res.json({ count: parsed.length, lines: parsed });
+ } catch (e) {
+ res.status(500).json({ error: String(e.message || e) });
+ }
+});
+
app.get('/api/health', (_req, res) => res.json({ ok: true, ts: Date.now() }));
app.listen(PORT, '0.0.0.0', () => {
← 684c80b feat: rich LLM bay panel — per-model rows + ollama process s
·
back to Macstudio1 Dashboard
·
feat: tach history strip — 60s CPU + RAM sparklines above th 92ad6cb →