[object Object]

← back to Ticket System

auto-save: 2026-07-27T11:50:21 (2 files) — office.html server.js

4d5b85d99b14e4c6ab15cdc33992171ab56c1247 · 2026-07-27 11:50:21 -0700 · Steve Abrams

Files touched

Diff

commit 4d5b85d99b14e4c6ab15cdc33992171ab56c1247
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 27 11:50:21 2026 -0700

    auto-save: 2026-07-27T11:50:21 (2 files) — office.html server.js
---
 office.html | 12 ++++++++++++
 server.js   | 25 +++++++++++++++++++++++++
 2 files changed, 37 insertions(+)

diff --git a/office.html b/office.html
index 879c215..bdb5257 100644
--- a/office.html
+++ b/office.html
@@ -31,6 +31,9 @@
         text-transform:uppercase;letter-spacing:.08em}
   .pill i.d{width:7px;height:7px;border-radius:2px;box-shadow:0 0 8px currentColor}
   .pill b{font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:700;color:#fff;letter-spacing:0}
+  .pill.run{border-color:rgba(52,217,160,0.4);background:rgba(52,217,160,0.07)}
+  .pill.run i.d{animation:runblink 1.6s ease-in-out infinite}
+  @keyframes runblink{0%,100%{opacity:.45}50%{opacity:1}}
   #top .age{margin-left:auto;font-size:10px;color:var(--faint);letter-spacing:.14em;text-transform:uppercase}
   #top .age b{font-family:'JetBrains Mono',monospace;color:var(--dim);font-weight:600}
   /* ── legend / workload ── */
@@ -54,6 +57,8 @@
   .ctl button:hover{border-color:rgba(255,255,255,0.22);color:#fff;background:rgba(255,255,255,0.06)}
   .ctl button.on{color:#fff;border-color:var(--blue);background:rgba(90,169,240,0.12)}
   .ctl button.on::before{background:var(--blue);box-shadow:0 0 9px var(--blue)}
+  #b-run.on,#b-procs.on{color:#fff;border-color:var(--green);background:rgba(52,217,160,0.12)}
+  #b-run.on::before,#b-procs.on::before{background:var(--green);box-shadow:0 0 9px var(--green)}
   #b-ping{color:var(--purple);border-color:rgba(201,164,242,0.4)}
   #b-ping::before{background:var(--purple);box-shadow:0 0 9px var(--purple)}
   #b-reset::before,#b-ping::before{display:none}
@@ -98,6 +103,9 @@
     <span class="pill"><i class="d" style="background:var(--open);color:var(--open)"></i>open <b id="s-open">0</b></span>
     <span class="pill"><i class="d" style="background:var(--green);color:var(--green)"></i>done <b id="s-done">0</b></span>
     <span class="pill"><i class="d" style="background:var(--purple);color:var(--purple)"></i>agents <b id="s-agents">0</b></span>
+    <span class="pill run"><i class="d" style="background:var(--green);color:var(--green)"></i>live <b id="s-live">0</b></span>
+    <span class="pill run"><i class="d" style="background:var(--green);color:var(--green)"></i>pm2 <b id="s-running">0</b></span>
+    <span class="pill run"><i class="d" style="background:var(--green);color:var(--green)"></i>sessions <b id="s-sessions">0</b></span>
     <span class="pill"><i class="d" style="background:var(--purple);color:var(--purple)"></i>DMs <b id="s-dms">0</b></span>
   </div>
   <span class="age">SYNC <b id="s-age">–</b></span>
@@ -115,6 +123,8 @@
   <button id="b-follow">Follow DMs</button>
   <div class="csec">Scene</div>
   <button id="b-labels" class="on">Labels</button>
+  <button id="b-run" class="on">Running edges</button>
+  <button id="b-procs" class="on">Process wall</button>
   <button id="b-done" class="on">Done rooms</button>
   <button id="b-ping" title="Preview the DM packet animation (visual only — writes nothing)">Ping ✦</button>
   <button id="b-reset">Reset camera</button>
@@ -144,6 +154,8 @@ import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
 const LANES   = ['doing', 'blocked', 'open', 'done'];
 const COLOR   = { doing:0x6db3f2, blocked:0xff6b7a, open:0x9aa4b8, done:0x3ee6a0 };
 const ROOM=9, PITCH=12, LANE_W=42, DONE_CAP=14, POLL_MS=4000;
+const RUN=0x34d9a0;                 // "running" green — the through-line for live things
+const LIVE_MS=8*60*1000;            // an agent that acted within 8 min counts as live/running
 
 // ───────────────────────── renderer ─────────────────────────
 const canvas=document.getElementById('c');
diff --git a/server.js b/server.js
index 347ec6b..cd77964 100644
--- a/server.js
+++ b/server.js
@@ -2,8 +2,32 @@
 const http = require('http');
 const fs = require('fs');
 const path = require('path');
+const { exec } = require('child_process');
 const { tickets, STATUSES, messages } = require('./lib.js');
 
+// ── live "running" signal: online pm2 processes + live claude CLI sessions ──
+// Cached 5s so N polling browsers don't each spawn a `pm2 jlist` on a busy box.
+let runCache = { ts: 0, data: { pm2: [], sessions: 0, at: null } };
+function getRunning(cb) {
+  if (Date.now() - runCache.ts < 5000) return cb(runCache.data);
+  exec('pm2 jlist', { maxBuffer: 16 * 1024 * 1024, timeout: 8000 }, (e, out) => {
+    let pm2 = [];
+    if (!e) { try {
+      pm2 = JSON.parse(out).filter(p => p.pm2_env && p.pm2_env.status === 'online')
+        .map(p => ({ name: p.name, cpu: (p.monit && p.monit.cpu) || 0,
+          mem: Math.round(((p.monit && p.monit.memory) || 0) / 1048576),
+          up: p.pm2_env.pm_uptime || 0, restarts: p.pm2_env.restart_time || 0 }))
+        .sort((a, b) => a.name < b.name ? -1 : 1);
+    } catch (_) {} }
+    // count live `claude` CLI sessions (exclude skills-dir helpers), best-effort
+    exec("ps -Ao command | grep '[c]laude' | grep -v 'skills/' | wc -l", { timeout: 4000 }, (e2, out2) => {
+      const sessions = e2 ? 0 : (parseInt(String(out2).trim(), 10) || 0);
+      runCache = { ts: Date.now(), data: { pm2, sessions, at: new Date().toISOString() } };
+      cb(runCache.data);
+    });
+  });
+}
+
 const OFFICE_HTML = path.join(__dirname, 'office.html');
 
 const PORT = process.env.PORT || 9794;
@@ -82,6 +106,7 @@ http.createServer((req, res) => {
       res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(buf);
     });
   }
+  if (req.url === '/api/running') { return getRunning(d => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(d)); }); }
   if (req.url === '/api/tickets') { res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify([...tickets().values()])); }
   if (req.url === '/api/messages') { res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify([...messages().values()])); }
   res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(page());

← 338a454 office: harden agent placement — clear stale atWork glow on  ·  back to Ticket System  ·  Fleet Office: green 'running' edge for live agents, pm2 proc bb20804 →