[object Object]

← back to Slack To Steve

dashboard: pipe status + inbox + transcripts viewer on :9897 (pm2 slack-to-steve-dash)

b51b8133fa56091cd6a805f437da5ed6722f6165 · 2026-07-13 01:33:33 -0700 · Steve Abrams

Files touched

Diff

commit b51b8133fa56091cd6a805f437da5ed6722f6165
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 13 01:33:33 2026 -0700

    dashboard: pipe status + inbox + transcripts viewer on :9897 (pm2 slack-to-steve-dash)
---
 server.js | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 160 insertions(+)

diff --git a/server.js b/server.js
new file mode 100644
index 0000000..e0caaaf
--- /dev/null
+++ b/server.js
@@ -0,0 +1,160 @@
+// server.js — slack-to-steve dashboard (zero-dep, local, basic-auth)
+// Shows: pipe status (armed/loop/cursors), the inbox timeline, channel transcripts.
+// Port 9897 · admin / DW2024!
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const DIR = __dirname;
+const PORT = parseInt(process.env.PORT || '9897', 10);
+const AUTH = 'Basic ' + Buffer.from('admin:DW2024!').toString('base64');
+
+const CHANNEL_NAMES = {
+  C0BGQ2QLR35: '#claude-chat',
+  C09SW7VQ0RK: '#claude-to-steve',
+};
+
+function loadEnv() {
+  const o = {};
+  try {
+    for (const l of fs.readFileSync(path.join(DIR, '.env'), 'utf8').split('\n')) {
+      const m = l.match(/^([A-Z0-9_]+)=(.*)$/);
+      if (m) o[m[1]] = m[2].replace(/^['"]|['"]$/g, '');
+    }
+  } catch {}
+  return o;
+}
+
+function loopPid() {
+  try {
+    const out = execSync("ps ax -o pid=,command= | grep 'ingest.mjs' | grep -v grep", { encoding: 'utf8' });
+    const line = out.trim().split('\n')[0];
+    return line ? parseInt(line.trim().split(/\s+/)[0], 10) : null;
+  } catch { return null; }
+}
+
+function readInbox() {
+  try {
+    return fs.readFileSync(path.join(DIR, 'data', 'inbox.jsonl'), 'utf8')
+      .trim().split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } })
+      .filter(Boolean);
+  } catch { return []; }
+}
+
+function readCursors() {
+  const out = [];
+  try {
+    for (const f of fs.readdirSync(path.join(DIR, 'data'))) {
+      const m = f.match(/^last-ts-(.+)\.txt$/);
+      if (!m) continue;
+      const ts = fs.readFileSync(path.join(DIR, 'data', f), 'utf8').trim();
+      out.push({ channel: m[1], name: CHANNEL_NAMES[m[1]] || m[1], ts, at: ts ? new Date(parseFloat(ts) * 1000).toISOString() : null });
+    }
+  } catch {}
+  return out;
+}
+
+function status() {
+  const env = loadEnv();
+  const pid = loopPid();
+  const channels = (env.SLACK_CHANNEL_IDS || '').split(',').map(s => s.trim()).filter(Boolean);
+  const armedList = (env.ARMED_CHANNELS || '').split(',').map(s => s.trim()).filter(Boolean);
+  return {
+    loop_alive: !!pid,
+    loop_pid: pid,
+    armed: (env.AUTO_EXECUTE || '0') === '1',
+    watch_interval_ms: parseInt(env.WATCH_INTERVAL_MS || '90000', 10),
+    channels: channels.map(c => ({ id: c, name: CHANNEL_NAMES[c] || c, armed: (env.AUTO_EXECUTE === '1') && (armedList.length === 0 || armedList.includes(c)) })),
+    cursors: readCursors(),
+    inbox_count: readInbox().length,
+    now: new Date().toISOString(),
+  };
+}
+
+const esc = s => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+const fmt = iso => iso ? new Date(iso).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', second: '2-digit' }) : '—';
+
+function page() {
+  const st = status();
+  const inbox = readInbox().slice().reverse();
+  const transcripts = [];
+  try {
+    for (const f of fs.readdirSync(path.join(DIR, 'exports')).filter(f => f.endsWith('.txt')).sort()) {
+      transcripts.push({ name: f, body: fs.readFileSync(path.join(DIR, 'exports', f), 'utf8').slice(-20000) });
+    }
+  } catch {}
+
+  const chanChips = st.channels.map(c =>
+    `<span class="chip ${c.armed ? 'armed' : ''}">${esc(c.name)} ${c.armed ? '⚡ armed' : 'queue-only'}</span>`).join(' ');
+
+  const rows = inbox.map(m => `
+    <div class="msg ${m.armed ? 'armed' : ''}">
+      <div class="meta">
+        <span class="chan">${esc(CHANNEL_NAMES[m.channel] || m.channel || '?')}</span>
+        <span class="when" title="${esc(m.queued_at)}">🕓 ${fmt(m.queued_at)}</span>
+        ${m.armed ? '<span class="badge">⚡ auto-executed</span>' : '<span class="badge q">📥 queued</span>'}
+      </div>
+      <div class="text">${esc(m.text)}</div>
+    </div>`).join('');
+
+  const trans = transcripts.map(t => `
+    <details><summary>${esc(t.name)}</summary><pre>${esc(t.body)}</pre></details>`).join('');
+
+  return `<!doctype html><html><head><meta charset="utf-8">
+<meta http-equiv="refresh" content="30">
+<title>slack-to-steve · pipe dashboard</title>
+<style>
+  :root { color-scheme: dark; }
+  body { font: 14px/1.5 -apple-system, Helvetica, sans-serif; background: #0d1117; color: #e6edf3; margin: 0; padding: 24px; max-width: 900px; margin-inline: auto; }
+  h1 { font-size: 20px; margin: 0 0 4px; } h2 { font-size: 15px; margin: 28px 0 10px; color: #9ab; text-transform: uppercase; letter-spacing: .08em; }
+  .sub { color: #7d8590; font-size: 12px; margin-bottom: 18px; }
+  .cards { display: flex; gap: 10px; flex-wrap: wrap; }
+  .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 10px 14px; }
+  .card b { display: block; font-size: 18px; } .card span { color: #7d8590; font-size: 11px; }
+  .ok { color: #3fb950; } .bad { color: #f85149; }
+  .chip { display: inline-block; background: #21262d; border: 1px solid #30363d; border-radius: 999px; padding: 2px 10px; font-size: 12px; margin: 2px; }
+  .chip.armed { border-color: #d29922; color: #e3b341; }
+  .msg { background: #161b22; border: 1px solid #30363d; border-left: 3px solid #388bfd; border-radius: 6px; padding: 10px 12px; margin: 8px 0; }
+  .msg.armed { border-left-color: #d29922; }
+  .meta { display: flex; gap: 10px; align-items: center; font-size: 12px; color: #7d8590; margin-bottom: 4px; }
+  .chan { color: #58a6ff; } .badge { color: #e3b341; } .badge.q { color: #7d8590; }
+  .text { white-space: pre-wrap; }
+  details { background: #161b22; border: 1px solid #30363d; border-radius: 6px; margin: 8px 0; padding: 6px 12px; }
+  summary { cursor: pointer; color: #58a6ff; }
+  pre { white-space: pre-wrap; font-size: 12px; color: #9ab; max-height: 420px; overflow: auto; }
+  table { border-collapse: collapse; font-size: 12px; } td, th { border: 1px solid #30363d; padding: 4px 10px; text-align: left; }
+</style></head><body>
+  <h1>slack-to-steve <span class="${st.loop_alive ? 'ok' : 'bad'}">●</span></h1>
+  <div class="sub">Two-way Slack↔Claude pipe · auto-refreshes every 30s · ${esc(fmt(st.now))}</div>
+  <div class="cards">
+    <div class="card"><b class="${st.loop_alive ? 'ok' : 'bad'}">${st.loop_alive ? 'LIVE' : 'DOWN'}</b><span>watch loop ${st.loop_pid ? 'pid ' + st.loop_pid : ''}</span></div>
+    <div class="card"><b class="${st.armed ? '' : 'bad'}">${st.armed ? '⚡ ARMED' : 'queue-only'}</b><span>auto-execute</span></div>
+    <div class="card"><b>${st.inbox_count}</b><span>messages processed</span></div>
+    <div class="card"><b>${Math.round(st.watch_interval_ms / 1000)}s</b><span>poll interval</span></div>
+  </div>
+  <h2>Channels</h2>${chanChips}
+  <h2>Cursors</h2>
+  <table><tr><th>channel</th><th>last message actioned</th></tr>
+  ${st.cursors.map(c => `<tr><td>${esc(c.name)}</td><td title="${esc(c.ts)}">${esc(fmt(c.at))}</td></tr>`).join('')}</table>
+  <h2>Inbox — newest first</h2>${rows || '<div class="sub">empty</div>'}
+  <h2>Channel transcripts (exports/)</h2>${trans || '<div class="sub">none exported</div>'}
+</body></html>`;
+}
+
+http.createServer((req, res) => {
+  if (req.headers.authorization !== AUTH) {
+    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="slack-to-steve"' });
+    return res.end('auth required');
+  }
+  if (req.url === '/api/status') {
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify(status(), null, 2));
+  }
+  if (req.url === '/api/inbox') {
+    res.writeHead(200, { 'Content-Type': 'application/json' });
+    return res.end(JSON.stringify(readInbox(), null, 2));
+  }
+  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+  res.end(page());
+}).listen(PORT, () => console.log(`slack-to-steve dashboard on http://127.0.0.1:${PORT}`));

← bef02c4 arm.sh: arm BOTH channels (Steve's call)  ·  back to Slack To Steve  ·  ingest: re-entrancy guard on tick() — prevents overlapping a 641bc93 →