← back to Macstudio1 Dashboard

server.js

379 lines

// Mac Studio 1 dashboard — '57 Chevy convertible aesthetic.
// - GET /api/status      live Ollama state + CPU/RAM
// - POST /api/control/X  hyperdrive controls (RAM-pin models / power mode)
// - GET /                static dashboard

const express = require('express');
const { exec } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');

const PORT = parseInt(process.env.PORT || '9930', 10);
const OLLAMA = process.env.OLLAMA || 'http://localhost:11434';

const app = express();
app.use(express.json({ limit: '8kb' }));
// no caching of HTML — dashboard iterates fast, stale HTML hides new sections
app.use(express.static(path.join(__dirname, 'public'), {
  maxAge: 0,
  setHeaders: (res, filePath) => {
    if (filePath.endsWith('.html')) {
      res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
    }
  }
}));

// Viewer tracking — every /api/status hit from the dashboard front-end
// counts toward "is anyone watching". 30s window decides "active".
const viewerHits = new Map(); // ip -> last_at
function recordViewer(ip) {
  viewerHits.set(ip || 'unknown', Date.now());
  // GC entries older than 5 min so map stays bounded
  const cutoff = Date.now() - 5 * 60 * 1000;
  for (const [k, v] of viewerHits) if (v < cutoff) viewerHits.delete(k);
}
function activeViewers() {
  const cutoff = Date.now() - 30 * 1000;
  let n = 0;
  for (const v of viewerHits.values()) if (v >= cutoff) n++;
  return n;
}

// Recent activity log — last 30 inference triggers seen via /api/ps polling.
// Each tick of the front-end's fetch indirectly produces a record when ps changes.
const recentCalls = []; // { at, model, queue }

function pushCall(model, queue) {
  recentCalls.unshift({ at: Date.now(), model, queue });
  while (recentCalls.length > 30) recentCalls.pop();
}

function execCmd(cmd, timeoutMs = 4000) {
  return new Promise((resolve) => {
    exec(cmd, { timeout: timeoutMs, encoding: 'utf8' }, (err, stdout, stderr) => {
      resolve({ ok: !err, stdout: stdout || '', stderr: stderr || '' });
    });
  });
}

async function readCpu() {
  // top -l 1 -n 0 -F: one-shot, no per-process detail. Parse the "CPU usage" line.
  const r = await execCmd('top -l 1 -n 0 -F');
  const line = r.stdout.split('\n').find(l => /CPU usage:/i.test(l)) || '';
  const m = line.match(/(\d+\.\d+)%\s+user.*?(\d+\.\d+)%\s+sys.*?(\d+\.\d+)%\s+idle/i);
  if (!m) return { user: 0, sys: 0, idle: 100, total: 0 };
  const user = parseFloat(m[1]), sys = parseFloat(m[2]), idle = parseFloat(m[3]);
  return { user, sys, idle, total: +(user + sys).toFixed(1) };
}

async function readMemory() {
  // sysctl hw.memsize for total, vm_stat for active/wired/free pages.
  const total = await execCmd('sysctl -n hw.memsize');
  const totalBytes = parseInt(total.stdout.trim(), 10) || 0;
  const vm = await execCmd('vm_stat');
  const pageSize = 16384; // ARM page size
  let used = 0, free = 0;
  for (const line of vm.stdout.split('\n')) {
    const m = line.match(/Pages\s+(active|wired down|compressed):\s+(\d+)/i);
    if (m) used += parseInt(m[2], 10) * pageSize;
    const f = line.match(/Pages\s+free:\s+(\d+)/i);
    if (f) free = parseInt(f[1], 10) * pageSize;
  }
  return {
    total_bytes: totalBytes,
    used_bytes: used,
    free_bytes: free,
    used_pct: totalBytes ? +(used / totalBytes * 100).toFixed(1) : 0
  };
}

async function readOllama() {
  try {
    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,
      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: 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: [], 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() {
  const r = await execCmd('pmset -g | grep -E "powermode|lowpowermode"');
  const text = r.stdout.toLowerCase();
  if (/powermode\s+2/.test(text) || /high power/.test(text)) return 'high';
  if (/powermode\s+1/.test(text) || /low power/.test(text)) return 'low';
  return 'auto';
}

app.get('/api/status', async (req, res) => {
  try {
    recordViewer(req.ip);
    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);
    }
    res.set('Cache-Control', 'no-store');
    res.json({
      ts: Date.now(),
      hostname: require('os').hostname(),
      cpu, memory: mem, ollama: oll, ollama_procs: ollProcs, power_mode: power,
      recent_calls: recentCalls.slice(0, 10),
      uptime_s: Math.round(require('os').uptime()),
      viewers: activeViewers(),
      viewer_hits_30s: Array.from(viewerHits.values()).filter(t => Date.now() - t < 30000).length,
      total_known_viewers: viewerHits.size
    });
  } catch (e) {
    res.status(500).json({ error: String(e.message || e) });
  }
});

// Hyperdrive — high-power mode + pin all loaded models in VRAM forever.
// pmset requires sudo; if denied, returns clear failure (UI shows it).
app.post('/api/control/hyperdrive', async (_req, res) => {
  const out = {};
  // Try power mode (sudo). Will fail without NOPASSWD; that's fine, surface it.
  out.power = await execCmd('sudo -n pmset -a powermode 2 2>&1');
  // Pin loaded models — re-call each with keep_alive -1 (Ollama re-applies).
  const oll = await readOllama();
  out.pinned = [];
  for (const m of oll.models) {
    const r = await execCmd(`curl -sS --max-time 4 -X POST ${OLLAMA}/api/generate -H "Content-Type: application/json" -d '{"model":"${m.name}","prompt":"","keep_alive":-1,"options":{"num_predict":1}}'`);
    out.pinned.push({ model: m.name, ok: r.ok });
  }
  res.json({ ok: true, mode: 'hyperdrive', detail: out });
});

// Max RAM — load every installed model into memory + pin (-1 keep_alive)
app.post('/api/control/max-ram', async (_req, res) => {
  // List all installed models
  const tagsR = await execCmd(`curl -sS --max-time 4 ${OLLAMA}/api/tags`);
  let names = [];
  try {
    const j = JSON.parse(tagsR.stdout);
    names = (j.models || []).map(m => m.name);
  } catch {}
  const out = [];
  for (const name of names) {
    // Fire-and-forget; load with no actual generation
    const r = await execCmd(`curl -sS --max-time 8 -X POST ${OLLAMA}/api/generate -H "Content-Type: application/json" -d '{"model":"${name}","prompt":"","keep_alive":-1,"options":{"num_predict":1}}'`);
    out.push({ model: name, ok: r.ok });
  }
  res.json({ ok: true, mode: 'max-ram', loaded: out });
});

// 🏁 TEST — fires a tiny prompt at the loaded model and times it. Useful to
// directly compare before/after a BOOST or to see if Mac1 is responsive.
app.post('/api/control/test', async (_req, res) => {
  try {
    const ps = await fetch(`${OLLAMA}/api/ps`, { signal: AbortSignal.timeout(2500) });
    const psJ = ps.ok ? await ps.json() : { models: [] };
    const model = (psJ.models || [])[0]?.name || 'qwen3:14b';
    const t0 = Date.now();
    const r = await fetch(`${OLLAMA}/api/chat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        model, stream: false,
        messages: [{ role: 'user', content: 'Reply with exactly the phrase "BOOST OK" and nothing else, no thinking.' }],
        options: { num_predict: 8, num_ctx: 512 }
      }),
      signal: AbortSignal.timeout(60000)
    });
    const ms = Date.now() - t0;
    if (!r.ok) {
      const txt = await r.text();
      return res.json({ ok: false, mode: 'test', model, ms, status: r.status, error: txt.slice(0, 240) });
    }
    const j = await r.json();
    res.json({
      ok: true, mode: 'test', model, ms,
      reply: (j?.message?.content || '').slice(0, 120),
      tokens_per_s: j.eval_count && j.eval_duration ? +(j.eval_count / (j.eval_duration / 1e9)).toFixed(1) : null
    });
  } catch (e) {
    res.status(500).json({ ok: false, error: String(e?.message || e) });
  }
});

// 🏎️ BOOST — runs ~/bin/ollama-boost.sh which kills the manual ollama serve
// and relaunches via launchd com.steve.ollama-boost with these env vars:
//   OLLAMA_NUM_PARALLEL=4         (4× concurrent inference per model)
//   OLLAMA_MAX_LOADED_MODELS=2    (qwen3:14b + qwen3:8b hot side-by-side)
//   OLLAMA_FLASH_ATTENTION=1      (~30% less KV cache memory)
//   OLLAMA_KV_CACHE_TYPE=q4_0     (4× longer context fits)
//   OLLAMA_KEEP_ALIVE=-1          (never unload)
// Followed by pre-loading every installed model into VRAM with keep_alive=-1.
// Big-deal restart — kills in-flight magazine-gen calls; pm2 worker retries.
app.post('/api/control/boost', async (_req, res) => {
  const r = await execCmd('/Users/steveabramsdesignsgmail.com/bin/ollama-boost.sh 2>&1', 90000);
  res.json({ ok: r.ok, mode: 'boost', stdout: r.stdout.slice(-2000), stderr: r.stderr.slice(-1000) });
});

// Idle — restore default power mode + unload models
app.post('/api/control/idle', async (_req, res) => {
  const out = {};
  out.power = await execCmd('sudo -n pmset -a powermode 0 2>&1');
  const oll = await readOllama();
  out.unloaded = [];
  for (const m of oll.models) {
    const r = await execCmd(`curl -sS --max-time 4 -X POST ${OLLAMA}/api/generate -H "Content-Type: application/json" -d '{"model":"${m.name}","prompt":"","keep_alive":0}'`);
    out.unloaded.push({ model: m.name, ok: r.ok });
  }
  res.json({ ok: true, mode: 'idle', detail: out });
});

// Live process list — parsed ps output. Top 60 by CPU desc by default.
// Columns: pid, user, cpu, mem, rss_mb, start, time, command
app.get('/api/processes', async (_req, res) => {
  try {
    // -A all, -o explicit columns, sort by -%cpu (descending)
    // comm = full command (truncated by ps to ~256 chars)
    const r = await execCmd("ps -Ao pid=,user=,%cpu=,%mem=,rss=,etime=,time=,comm= -r 2>/dev/null | head -200");
    const rows = [];
    for (const line of r.stdout.split('\n')) {
      const m = line.match(/^\s*(\d+)\s+(\S+)\s+(\d+\.\d+)\s+(\d+\.\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.+?)\s*$/);
      if (!m) continue;
      const [, pid, user, cpu, mem, rss, start, time, command] = m;
      rows.push({
        pid: parseInt(pid, 10),
        user,
        cpu: parseFloat(cpu),
        mem: parseFloat(mem),
        rss_mb: +(parseInt(rss, 10) / 1024).toFixed(1),
        start, time,
        command: command.slice(0, 80)
      });
    }
    res.set('Cache-Control', 'no-store');
    res.json({ count: rows.length, rows: rows.slice(0, 60) });
  } catch (e) {
    res.status(500).json({ error: String(e.message || e) });
  }
});

// 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) });
  }
});

// Server-side build version — hash of public/index.html. Polling clients
// compare to their loaded copy and force-reload when it changes.
const crypto = require('node:crypto');
let _buildVersion = '';
function computeBuildVersion() {
  try {
    const html = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
    _buildVersion = crypto.createHash('sha1').update(html).digest('hex').slice(0, 10);
  } catch { _buildVersion = 'unknown'; }
  return _buildVersion;
}
computeBuildVersion();
fs.watch(path.join(__dirname, 'public'), { recursive: true }, () => computeBuildVersion());
app.get('/api/version', (_req, res) => res.json({ version: _buildVersion, ts: Date.now() }));

app.get('/api/health', (_req, res) => res.json({ ok: true, ts: Date.now() }));

app.listen(PORT, '0.0.0.0', () => {
  console.log(`[macstudio1-dashboard] serving on http://0.0.0.0:${PORT}`);
});