[object Object]

← 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

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 => ({'<':'&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);
 </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 →