← back to Macstudio1 Dashboard
feat: engine bay live process table below gauges
3ef920f4ce94f24d28bd737328912a7ef3923a5f · 2026-05-07 23:16:50 -0700 · SteveStudio2
- /api/processes endpoint runs ps -Ao with explicit columns,
parses to JSON {pid,user,cpu,mem,rss_mb,start,time,command}
- table rendered below odometer w/ chrome trim + leather backing
- sortable headers (click any col, second click reverses)
default sort: CPU% desc; numeric cols default desc on first click
- hot rows (>=50% CPU or >=10% MEM) tinted red
- 2.5s poll cycle independent of gauge ticks
- Lobster Engine bay header in cream + gold counter
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M public/index.htmlM server.js
Diff
commit 3ef920f4ce94f24d28bd737328912a7ef3923a5f
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Thu May 7 23:16:50 2026 -0700
feat: engine bay live process table below gauges
- /api/processes endpoint runs ps -Ao with explicit columns,
parses to JSON {pid,user,cpu,mem,rss_mb,start,time,command}
- table rendered below odometer w/ chrome trim + leather backing
- sortable headers (click any col, second click reverses)
default sort: CPU% desc; numeric cols default desc on first click
- hot rows (>=50% CPU or >=10% MEM) tinted red
- 2.5s poll cycle independent of gauge ticks
- Lobster Engine bay header in cream + gold counter
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
public/index.html | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
server.js | 29 +++++++++
2 files changed, 203 insertions(+)
diff --git a/public/index.html b/public/index.html
index 672a746..0fa096c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -371,6 +371,92 @@ header.masthead {
letter-spacing: 0.16em;
}
+/* Engine bay process table — chrome trim, leather background, brass header */
+.proc-table {
+ 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);
+}
+.proc-head {
+ display: flex; justify-content: space-between; align-items: baseline;
+ border-bottom: 2px solid var(--chrome-2);
+ padding-bottom: 10px; margin-bottom: 10px;
+}
+.proc-head h3 {
+ margin: 0;
+ font-family: 'Lobster', cursive;
+ font-size: 26px;
+ color: var(--cream);
+ text-shadow: 0 0 10px rgba(212, 182, 131, 0.5);
+}
+.proc-count {
+ font-family: 'Special Elite', monospace;
+ font-size: 11px;
+ letter-spacing: 0.28em;
+ color: var(--gold);
+ text-transform: uppercase;
+}
+.proc-table table {
+ width: 100%;
+ border-collapse: collapse;
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 12px;
+ color: var(--cream);
+}
+.proc-table thead th {
+ position: sticky; top: 0;
+ text-align: left;
+ padding: 8px 10px;
+ font-family: 'Special Elite', monospace;
+ font-size: 10px;
+ letter-spacing: 0.22em;
+ text-transform: uppercase;
+ color: var(--gold);
+ border-bottom: 1px solid var(--chrome-3);
+ background: var(--leather);
+ cursor: pointer;
+ user-select: none;
+ white-space: nowrap;
+}
+.proc-table thead th:hover { color: var(--cream); }
+.proc-table thead th.active { color: var(--turq); }
+.proc-table thead th.active::after {
+ content: ' ▾';
+ color: var(--turq);
+}
+.proc-table thead th.active.asc::after { content: ' ▴'; }
+.proc-table tbody tr {
+ border-bottom: 1px dotted rgba(212, 182, 131, 0.18);
+ transition: background 0.15s;
+}
+.proc-table tbody tr:hover { background: rgba(74, 189, 197, 0.12); }
+.proc-table tbody td {
+ padding: 5px 10px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 280px;
+}
+.proc-table td.num {
+ text-align: right;
+ font-variant-numeric: tabular-nums;
+ color: var(--gold);
+}
+.proc-table tbody td.cmd {
+ font-family: 'Special Elite', monospace;
+ color: var(--cream);
+ max-width: 380px;
+}
+.proc-table tbody tr.hot {
+ background: rgba(200, 36, 58, 0.18);
+}
+
footer {
text-align: center;
padding: 32px 24px 64px;
@@ -459,6 +545,28 @@ footer a { color: var(--gold); text-decoration: none; }
<span class="digit" id="odo-calls">0000</span>
</div>
+ <!-- Live process list -->
+ <section class="proc-table">
+ <div class="proc-head">
+ <h3>Engine bay · live processes</h3>
+ <span class="proc-count" id="proc-count">— running</span>
+ </div>
+ <table id="proc">
+ <thead>
+ <tr>
+ <th data-sort="pid">PID</th>
+ <th data-sort="user">USER</th>
+ <th data-sort="cpu" class="active desc">CPU %</th>
+ <th data-sort="mem">MEM %</th>
+ <th data-sort="rss_mb">RSS (MB)</th>
+ <th data-sort="time">TIME</th>
+ <th data-sort="command">COMMAND</th>
+ </tr>
+ </thead>
+ <tbody></tbody>
+ </table>
+ </section>
+
</main>
<footer>
@@ -564,6 +672,72 @@ async function trigger(mode, btn) {
tick();
setInterval(tick, 1200);
+
+// ─── Live process table ────────────────────────────────────────────────
+let PROC_ROWS = [];
+let PROC_SORT = { col: 'cpu', dir: 'desc' };
+
+function fmtNum(n, dec) { return Number(n).toFixed(dec === undefined ? 1 : dec); }
+
+function renderProcs() {
+ const tbody = document.querySelector('#proc tbody');
+ if (!tbody) return;
+ const rows = [...PROC_ROWS];
+ rows.sort((a, b) => {
+ let va = a[PROC_SORT.col], vb = b[PROC_SORT.col];
+ if (typeof va === 'string' && typeof vb === 'string') {
+ return PROC_SORT.dir === 'asc' ? va.localeCompare(vb) : vb.localeCompare(va);
+ }
+ return PROC_SORT.dir === 'asc' ? (va - vb) : (vb - va);
+ });
+ tbody.innerHTML = rows.map(r => {
+ const hot = (r.cpu >= 50 || r.mem >= 10) ? ' class="hot"' : '';
+ const escCmd = String(r.command).replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'}[c]));
+ return `<tr${hot}>
+ <td class="num">${r.pid}</td>
+ <td>${r.user}</td>
+ <td class="num">${fmtNum(r.cpu)}</td>
+ <td class="num">${fmtNum(r.mem)}</td>
+ <td class="num">${fmtNum(r.rss_mb, 0)}</td>
+ <td>${r.time}</td>
+ <td class="cmd" title="${escCmd}">${escCmd}</td>
+ </tr>`;
+ }).join('');
+
+ // Sort indicators on headers
+ document.querySelectorAll('#proc thead th').forEach(th => {
+ th.classList.remove('active', 'asc', 'desc');
+ if (th.dataset.sort === PROC_SORT.col) {
+ th.classList.add('active', PROC_SORT.dir);
+ }
+ });
+}
+
+document.querySelectorAll('#proc thead th').forEach(th => {
+ th.addEventListener('click', () => {
+ const col = th.dataset.sort;
+ if (!col) return;
+ if (PROC_SORT.col === col) {
+ PROC_SORT.dir = PROC_SORT.dir === 'asc' ? 'desc' : 'asc';
+ } else {
+ PROC_SORT.col = col;
+ PROC_SORT.dir = (col === 'pid' || col === 'user' || col === 'command') ? 'asc' : 'desc';
+ }
+ renderProcs();
+ });
+});
+
+async function tickProcs() {
+ try {
+ const r = await fetch('/api/processes').then(r => r.json());
+ PROC_ROWS = r.rows || [];
+ document.getElementById('proc-count').textContent = `${PROC_ROWS.length} running`;
+ renderProcs();
+ } catch (e) { /* network blip */ }
+}
+
+tickProcs();
+setInterval(tickProcs, 2500);
</script>
</body>
diff --git a/server.js b/server.js
index 7ab074a..a466962 100644
--- a/server.js
+++ b/server.js
@@ -157,6 +157,35 @@ app.post('/api/control/idle', async (_req, res) => {
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) });
+ }
+});
+
app.get('/api/health', (_req, res) => res.json({ ok: true, ts: Date.now() }));
app.listen(PORT, '0.0.0.0', () => {
← 574b249 feat: macstudio1-dashboard v0.1 — 57 Bel Air aesthetic
·
back to Macstudio1 Dashboard
·
fix: no-store HTML so new sections appear on reload 2afd088 →