[object Object]

← back to Desktop Dotbar

Add RAM Sniper to the nav bar: live RAM%/CPU% chip, ON/OFF daemon toggle, top-hogs dropdown

34abb189c7fecf5e16de91665f2c68cfd298d4df · 2026-09-22 10:13:09 -0700 · Steve Abrams

- server.js: /api/ram (top -l1 + sysctl + ps, zero-dep, cached 2s) and
  POST /api/ram/toggle (start/stop ~/bin/ram-sniper.sh, pgrep-f path-qualified)
- index.html: 🧠 RAM% ⚙ CPU% chip in #meta, 🔫 ON/OFF toggle, click-to-open
  top-memory dropdown reusing the existing panel pattern

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AztR94xH5K8nBe9ssBoagG

Files touched

Diff

commit 34abb189c7fecf5e16de91665f2c68cfd298d4df
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 22 10:13:09 2026 -0700

    Add RAM Sniper to the nav bar: live RAM%/CPU% chip, ON/OFF daemon toggle, top-hogs dropdown
    
    - server.js: /api/ram (top -l1 + sysctl + ps, zero-dep, cached 2s) and
      POST /api/ram/toggle (start/stop ~/bin/ram-sniper.sh, pgrep-f path-qualified)
    - index.html: 🧠 RAM% ⚙ CPU% chip in #meta, 🔫 ON/OFF toggle, click-to-open
      top-memory dropdown reusing the existing panel pattern
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AztR94xH5K8nBe9ssBoagG
---
 public/index.html | 49 +++++++++++++++++++++++++++++++++++++++++++++++--
 server.js         | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 98 insertions(+), 3 deletions(-)

diff --git a/public/index.html b/public/index.html
index 5d8f511..7db9988 100644
--- a/public/index.html
+++ b/public/index.html
@@ -109,6 +109,8 @@ const BAR_H = 48, PANEL_H = 360;
 let openColor = null, data = null, _miss = 0, curBarH = BAR_H, curBarW = 210, curBarLen = 0, arranging = false, arrangeMsg = '';
 // Live config from the main process (orientation + auto-hide + top-bar length). Defaults match a fresh install.
 let cfg = { orientation: 'top', autohide: false, barW: 210, barLen: 0 };
+// RAM Sniper readout (RAM%/CPU% + daemon on/off + top hogs), polled from /api/ram.
+let ram = { ram_pct: '–', cpu_pct: '–', on: false, hogs: [] };
 // The four needs-Steve states pulse; green/pink stay solid (Steve 2026-09-15 dot-flash directive).
 const WAIT = new Set(['lightblue','orange','purple','yellow']);
 const ORIENT_LABEL = { top: '▲ Top', left: '◀ Left', right: '▶ Right' };
@@ -150,7 +152,13 @@ function renderBar(){
   const sp = document.createElement('div'); sp.id='spacer'; bar.appendChild(sp);
   const meta = document.createElement('div'); meta.id='meta';
   const nextO = cfg.orientation==='top'?'left':cfg.orientation==='left'?'right':'top';
-  meta.innerHTML = `<button class="arrbtn${arranging?' busy':''}" ${arranging?'disabled':''} title="run the master dot-screen arrangement now" onclick="arrange()">${arranging?'⧉ Arranging…':(arrangeMsg?('⧉ '+arrangeMsg):'⧉ Arrange master')}</button>`
+  // RAM Sniper: 🧠 RAM% ⚙ CPU% chip (click -> top-hogs dropdown) + 🔫 ON/OFF daemon toggle.
+  meta.innerHTML = `<div class="chip ramchip${openColor==='ram'?' active':''}" title="RAM / CPU — click for top memory hogs" onclick="toggle('ram')">`
+      + `<span class="dot" style="background:#59d0ff"></span>`
+      + `<span class="cnt">🧠 ${ram.ram_pct}%</span>`
+      + `<span class="nm">⚙ ${ram.cpu_pct}%</span></div>`
+    + `<button class="cfgbtn${ram.on?' on':''}" title="RAM Sniper daemon — throttles background CPU/RAM hogs (never kills). Click to turn ${ram.on?'off':'on'}." onclick="toggleSniper()">${ram.on?'🔫 ON':'🔫 OFF'}</button>`
+    + `<button class="arrbtn${arranging?' busy':''}" ${arranging?'disabled':''} title="run the master dot-screen arrangement now" onclick="arrange()">${arranging?'⧉ Arranging…':(arrangeMsg?('⧉ '+arrangeMsg):'⧉ Arrange master')}</button>`
     + `<button class="cfgbtn" title="dock the bar on another edge (revert to Top from a side)" onclick="cycleOrient()">${ORIENT_LABEL[cfg.orientation]||'▲ Top'} ▸ ${ORIENT_LABEL[nextO].replace(/^[^ ]+ /,'')}</button>`
     + `<button class="cfgbtn${cfg.autohide?' on':''}" title="edge auto-hide: slide the bar off its edge; hover the edge to reveal" onclick="toggleAutohide()">${cfg.autohide?'👁 Auto-hide':'📌 Pinned'}</button>`
     + `<span><b>${data.total}</b> live</span>`
@@ -162,6 +170,22 @@ function renderBar(){
 function renderPanel(){
   const panel = document.getElementById('panel');
   if (!openColor){ panel.className=''; panel.innerHTML=''; return; }
+  // RAM Sniper panel: top memory hogs (not a live colour group).
+  if (openColor==='ram'){
+    panel.className = 'open';
+    const hogs = (ram && ram.hogs) || [];
+    let html = `<div class="phead">🧠 RAM ${ram.ram_pct}% · ⚙ CPU ${ram.cpu_pct}% · sniper ${ram.on?'🔫 ON (throttling)':'OFF'} — top memory</div>`;
+    if (!hogs.length){ panel.innerHTML = html + `<div class="empty">No data yet.</div>`; return; }
+    for (const h of hogs){
+      html += `<div class="row">`
+        + `<span class="rdot" style="background:#59d0ff"></span>`
+        + `<span class="tk">${h.pid}</span>`
+        + `<span class="doing" title="${(h.cmd||'').replace(/"/g,'&quot;')}">${h.name}</span>`
+        + `<span class="tty">${h.gb} GB · ${h.pct}%</span>`
+        + `</div>`;
+    }
+    panel.innerHTML = html; return;
+  }
   // Durable PARKED panel (TK-11946): registry entries, not a live colour group.
   if (openColor==='parked'){
     panel.className = 'open';
@@ -224,11 +248,30 @@ async function arrange(){
   tick();
 }
 
+// Turn the ram-sniper.sh daemon on/off from the bar (reversible — it only reniced).
+async function toggleSniper(){
+  try {
+    const r = await fetch('/api/ram/toggle', { method:'POST' });
+    const j = await r.json().catch(()=>null);
+    if (j) { ram = j; renderBar(); if (openColor==='ram') renderPanel(); }
+  } catch(e){}
+}
+// Poll live RAM%/CPU% + hogs on its own faster cadence than the (slow) dot scan.
+async function ramTick(){
+  try {
+    const r = await fetch('/api/ram');
+    const j = await r.json();
+    if (j && j.updated){ ram = j; renderBar(); if (openColor==='ram') renderPanel(); }
+  } catch(e){}
+}
+
 async function tick(){
   const d = await fetchDots();
   if (!d) return;
   data = d;
-  const openStillHasItems = openColor==='parked'
+  const openStillHasItems = openColor==='ram'
+    ? true                                   // RAM panel is always valid; never auto-close it
+    : openColor==='parked'
     ? !!(data.parked && data.parked.count)
     : data.groups.some(g=>g.color===openColor && g.count>0);
   if (openColor && !openStillHasItems) {
@@ -309,6 +352,8 @@ const _pre = new URLSearchParams(location.search).get('open');
 resize(false);
 tick().then(() => { if (_pre) toggle(_pre); });
 setInterval(tick, 3000);
+ramTick();
+setInterval(ramTick, 2000);
 </script>
 </body>
 </html>
diff --git a/server.js b/server.js
index 51345fb..c67ab1c 100755
--- a/server.js
+++ b/server.js
@@ -5,13 +5,14 @@
 'use strict';
 
 const http = require('http');
-const { execFile } = require('child_process');
+const { execFile, spawn } = require('child_process');
 const fs = require('fs');
 const path = require('path');
 
 const ALLCOLORDOTS = `${process.env.HOME}/.claude/skills/allcolordots/allcolordots.sh`;
 const ROUTER = `${process.env.HOME}/.claude/skills/dot-screen-router/router.sh`;
 const PARKED_MJS = `${process.env.HOME}/.claude/skills/parked/parked.mjs`;  // durable PARKED registry (TK-11946)
+const SNIPER = `${process.env.HOME}/bin/ram-sniper.sh`;  // priority-throttle daemon (renice only, never kills)
 
 // Urgency order (matches allcolordots): needs-Steve first, working/parked last.
 const ORDER = ['lightblue', 'orange', 'purple', 'yellow', 'green', 'pink', 'none'];
@@ -127,6 +128,47 @@ async function arrange() {
     master: true, busy, err: busy ? null : (err ? String(err).slice(0, 120) : null) };
 }
 
+// ---- RAM Sniper panel: live RAM%/CPU%, top memory hogs, daemon on/off ----
+// Is the priority-throttle daemon running? Path-qualified pattern so pgrep can't
+// self-match the node server (see pgrep-f-matches-own-watcher-shell).
+async function sniperOn() {
+  const { stdout } = await run('pgrep', ['-f', 'bin/ram-sniper.sh']);
+  return stdout.trim().split(/\s+/).filter(Boolean).length > 0;
+}
+// Zero-dep stats: system RAM%/CPU% from `top`, top hogs from `ps` (no psutil).
+async function getRam() {
+  const [{ stdout: topOut }, { stdout: memBytes }, { stdout: psOut }] = await Promise.all([
+    run('top', ['-l', '1', '-n', '0']),
+    run('sysctl', ['-n', 'hw.memsize']),
+    run('ps', ['-axo', 'pid=,rss=,comm=']),
+  ]);
+  // CPU: "CPU usage: 4.1% user, 5.2% sys, 90.6% idle" -> used = 100 - idle.
+  let cpu = 0;
+  const idle = /CPU usage:.*?([\d.]+)%\s*idle/i.exec(topOut);
+  if (idle) cpu = Math.max(0, Math.min(100, 100 - parseFloat(idle[1])));
+  // RAM: "PhysMem: 21G used (2.0G wired), 11G unused." -> used/(used+unused).
+  const toGB = (n, u) => parseFloat(n) * ({ K: 1 / 1048576, M: 1 / 1024, G: 1, T: 1024 }[u] || 1);
+  let ramPct = 0;
+  const pm = /PhysMem:\s*([\d.]+)([KMGT])\s*used.*?([\d.]+)([KMGT])\s*unused/i.exec(topOut);
+  if (pm) { const used = toGB(pm[1], pm[2]), unused = toGB(pm[3], pm[4]); if (used + unused) ramPct = used / (used + unused) * 100; }
+  const totalBytes = parseInt(memBytes, 10) || 0;
+  const hogs = psOut.trim().split('\n').map(l => {
+    const m = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(l);
+    if (!m) return null;
+    const rssKB = parseInt(m[2], 10);
+    return { pid: +m[1], name: (m[3].split('/').pop() || m[3]).slice(0, 40), cmd: m[3].slice(0, 120),
+      gb: (rssKB / 1048576).toFixed(1),
+      pct: totalBytes ? ((rssKB * 1024 / totalBytes) * 100).toFixed(1) : '0' };
+  }).filter(Boolean).sort((a, b) => +b.gb - +a.gb).slice(0, 12);
+  return { updated: Date.now(), ram_pct: Math.round(ramPct), cpu_pct: Math.round(cpu), on: await sniperOn(), hogs };
+}
+// Toggle the daemon: kill if running (reversible — it only reniced), else spawn detached.
+async function toggleSniper() {
+  if (await sniperOn()) { await run('pkill', ['-f', 'bin/ram-sniper.sh']); return { on: false }; }
+  try { spawn(SNIPER, [], { detached: true, stdio: 'ignore' }).unref(); } catch (e) { return { on: false, error: String(e) }; }
+  return { on: true };
+}
+
 function send(res, code, body, type = 'application/json') {
   res.writeHead(code, { 'Content-Type': type, 'Cache-Control': 'no-store' });
   res.end(typeof body === 'string' ? body : JSON.stringify(body));
@@ -135,7 +177,9 @@ function send(res, code, body, type = 'application/json') {
 // allcolordots is slow (scans every terminal), so refresh in the background and
 // serve a cached snapshot — /api/dots must return instantly for a smooth bar.
 let snapshot = { updated: 0, total: 0, groups: [], stale: true };
+let ramSnapshot = { updated: 0, ram_pct: 0, cpu_pct: 0, on: false, hogs: [] };
 let refreshing = false;
+async function refreshRam() { try { ramSnapshot = await getRam(); } catch (e) { /* keep last */ } }
 async function refresh() {
   if (refreshing) return;
   refreshing = true;
@@ -156,6 +200,10 @@ const server = http.createServer(async (req, res) => {
     if (url.pathname === '/api/arrange' && req.method === 'POST') {
       return send(res, 200, await arrange());
     }
+    if (url.pathname === '/api/ram') { if (!ramSnapshot.updated) await refreshRam(); return send(res, 200, ramSnapshot); }
+    if (url.pathname === '/api/ram/toggle' && req.method === 'POST') {
+      const r = await toggleSniper(); await refreshRam(); return send(res, 200, { ...ramSnapshot, ...r });
+    }
     if (url.pathname === '/api/reveal' && req.method === 'POST') {
       let raw = '';
       req.on('data', c => (raw += c));
@@ -187,6 +235,8 @@ function listen(port, tries = 20) {
     console.log(`desktop-dotbar on http://127.0.0.1:${p}`);
     refresh();                       // warm the cache
     setInterval(refresh, 2500);      // keep it fresh in the background
+    refreshRam();                    // warm RAM/CPU stats
+    setInterval(refreshRam, 2000);   // keep RAM/CPU fresh
   });
 }
 listen(parseInt(process.env.PORT || '9787', 10));

← b102cca desktop-dotbar: collapsed PARKED chip/panel from the durable  ·  back to Desktop Dotbar  ·  Fix RAM missing on the bar: move RAM/CPU readout into the al 60eea92 →