← 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
M public/index.htmlM server.js
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,'"')}">${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 →