← back to Factory Floor
server.js
218 lines
// factory-floor — single-page live coordinator. Lean variant: event-loop-safe,
// concurrency-capped, lazy first poll, no shelling out during request handling.
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { execFile } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = +(process.env.PORT || 9936);
const POLL_MS = +(process.env.POLL_MS || 15_000);
const GIT_LIMIT = +(process.env.GIT_LIMIT || 40); // cap repos polled per cycle
const CONCURRENCY = +(process.env.CONCURRENCY || 4);
const PROJECTS_DIR = path.join(os.homedir(), 'Projects');
const SERVICES = JSON.parse(fs.readFileSync(path.join(__dirname, 'services.json'), 'utf8')).services;
function run(cmd, args, opts = {}) {
return new Promise((res) => {
execFile(cmd, args, { timeout: 15000, maxBuffer: 8 * 1024 * 1024, ...opts }, (err, stdout) =>
res({ err, stdout: stdout?.toString() || '' })
);
});
}
// concurrency-capped parallel map
async function pmap(items, fn, limit = CONCURRENCY) {
const out = new Array(items.length);
let i = 0;
async function worker() {
while (true) {
const idx = i++;
if (idx >= items.length) return;
try { out[idx] = await fn(items[idx], idx); } catch (e) { out[idx] = { error: e.message }; }
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return out;
}
async function pollPm2() {
const r = await run('pm2', ['jlist'], { timeout: 20000 });
if (r.err) return { error: r.err.code || r.err.message, items: [] };
try {
const j = JSON.parse(r.stdout);
return {
items: j.map(p => ({
name: p.name,
status: p.pm2_env?.status,
cpu: p.monit?.cpu ?? 0,
mem_mb: Math.round((p.monit?.memory ?? 0) / 1024 / 1024),
uptime_s: p.pm2_env?.pm_uptime ? Math.round((Date.now() - p.pm2_env.pm_uptime) / 1000) : 0,
restarts: p.pm2_env?.restart_time ?? 0,
port: p.pm2_env?.PORT ?? p.pm2_env?.env?.PORT ?? null,
})).sort((a, b) => a.name.localeCompare(b.name)),
};
} catch (e) { return { error: e.message, items: [] }; }
}
async function pollOllama() {
try {
const ctrl = AbortSignal.timeout(4000);
const [tagsR, psR] = await Promise.all([
fetch('http://127.0.0.1:11434/api/tags', { signal: ctrl }).then(r => r.json()).catch(() => null),
fetch('http://127.0.0.1:11434/api/ps', { signal: ctrl }).then(r => r.json()).catch(() => null),
]);
return {
installed: (tagsR?.models || []).map(m => ({
name: m.name,
size_gb: +(m.size / 1024 ** 3).toFixed(2),
q: m.details?.quantization_level,
params: m.details?.parameter_size,
})),
loaded: (psR?.models || []).map(m => ({ name: m.name })),
};
} catch (e) { return { error: e.message, installed: [], loaded: [] }; }
}
async function pollServices() {
return pmap(SERVICES, async (s) => {
const t0 = Date.now();
try {
const r = await fetch(s.url, { signal: AbortSignal.timeout(3000), redirect: 'manual' });
return { ...s, ok: r.status < 500, status: r.status, ms: Date.now() - t0 };
} catch (e) {
return { ...s, ok: false, status: 0, ms: Date.now() - t0, error: e.code || (e.name === 'TimeoutError' ? 'timeout' : 'err') };
}
}, 6);
}
async function pollGit() {
const dirs = fs.readdirSync(PROJECTS_DIR, { withFileTypes: true })
.filter(d => d.isDirectory() && !d.name.startsWith('.') && !d.name.startsWith('_'))
.map(d => d.name);
// cheap pre-filter: only dirs with a .git
const repos = dirs.filter(name => {
try { return fs.statSync(path.join(PROJECTS_DIR, name, '.git')).isDirectory(); } catch { return false; }
});
// sample most-recent-mtime first for relevance
const dated = repos.map(name => {
let mt = 0;
try { mt = fs.statSync(path.join(PROJECTS_DIR, name, '.git')).mtimeMs; } catch {}
return { name, mt };
}).sort((a, b) => b.mt - a.mt).slice(0, GIT_LIMIT);
const rows = await pmap(dated, async ({ name }) => {
const repo = path.join(PROJECTS_DIR, name);
const r = await run('git', ['-C', repo, 'log', '-1', '--format=%cI%x09%h%x09%s'], { timeout: 4000 });
if (r.err || !r.stdout) return null;
const [iso, sha, ...msg] = r.stdout.trim().split('\t');
return { project: name, iso, sha, msg: msg.join('\t'), age_s: Math.round((Date.now() - new Date(iso).getTime()) / 1000) };
}, 4);
return rows.filter(Boolean).sort((a, b) => b.iso.localeCompare(a.iso)).slice(0, 15);
}
function pollHost() {
const load = os.loadavg();
const total = os.totalmem(), free = os.freemem();
return {
host: os.hostname(),
cores: os.cpus().length,
load: load.map(n => +n.toFixed(2)),
mem_gb_total: +(total / 1024 ** 3).toFixed(1),
mem_gb_free: +(free / 1024 ** 3).toFixed(1),
uptime_h: +(os.uptime() / 3600).toFixed(1),
};
}
// "Big Red is thinking" — detect active claude CLI child processes
async function pollBigRed() {
const r = await run('pgrep', ['-fl', 'claude'], { timeout: 3000 });
const lines = (r.stdout || '').split('\n').filter(Boolean);
const claudeProcs = lines.filter(l => /\sclaude\s/.test(l) || /\/claude\b/.test(l));
// Heuristic: big-red invokes claude with --print + sandbox cwd. Count those specifically.
const bigRedActive = lines.filter(l => /big-red|--print/.test(l)).length;
return {
thinking: bigRedActive > 0,
claude_procs: claudeProcs.length,
big_red_calls: bigRedActive,
};
}
let STATE = { generated_at: null, booting: true };
let inFlight = false;
async function refresh() {
if (inFlight) return;
inFlight = true;
const t0 = Date.now();
try {
const [pm2, ollama, services, host, bigRed] = await Promise.all([
pollPm2(), pollOllama(), pollServices(), pollHost(), pollBigRed(),
]);
// git last; it's the heaviest
const git = await pollGit().catch(() => []);
STATE = { generated_at: new Date().toISOString(), poll_ms: Date.now() - t0, pm2, ollama, services, git, host, bigRed };
} finally {
inFlight = false;
}
}
// Schedule first refresh AFTER server starts listening, so the server is reachable immediately
const server = http.createServer((req, res) => {
try {
if (req.url === '/api/state') {
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
return res.end(JSON.stringify(STATE));
}
if (req.url === '/api/refresh' && req.method === 'POST') {
refresh().catch(() => {});
res.writeHead(202, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ scheduled: true }));
}
if (req.url === '/api/bigred/model' && req.method === 'POST') {
let body = '';
req.on('data', c => body += c);
req.on('end', () => {
try {
const { model } = JSON.parse(body || '{}');
if (!model || typeof model !== 'string') { res.writeHead(400); return res.end('model required'); }
const target = path.join(os.homedir(), 'Projects', 'big-red', '.selected-model');
fs.writeFileSync(target, model + '\n');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, model, written_to: target }));
} catch (e) { res.writeHead(500); res.end(e.message); }
});
return;
}
if (req.url === '/api/bigred/model' && req.method === 'GET') {
const target = path.join(os.homedir(), 'Projects', 'big-red', '.selected-model');
let model = null;
try { model = fs.readFileSync(target, 'utf8').trim(); } catch {}
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ model }));
}
let urlPath = req.url === '/' ? '/index.html' : req.url.split('?')[0];
const filePath = path.join(__dirname, 'public', urlPath);
if (!filePath.startsWith(path.join(__dirname, 'public'))) { res.writeHead(403); return res.end(); }
if (!fs.existsSync(filePath)) { res.writeHead(404); return res.end('not found'); }
const ext = path.extname(filePath).slice(1);
const types = { html: 'text/html; charset=utf-8', js: 'text/javascript', css: 'text/css', svg: 'image/svg+xml', json: 'application/json' };
res.writeHead(200, { 'Content-Type': types[ext] || 'application/octet-stream', 'Cache-Control': 'no-store' });
fs.createReadStream(filePath).pipe(res);
} catch (e) {
res.writeHead(500); res.end(e.message);
}
});
server.listen(PORT, () => {
console.log(`factory-floor listening on http://127.0.0.1:${PORT} (poll every ${POLL_MS}ms, git_limit=${GIT_LIMIT})`);
// Defer first poll so the server is responsive even while the first poll is in flight
setTimeout(() => {
refresh().catch(e => console.error('initial refresh err', e.message));
setInterval(() => refresh().catch(() => {}), POLL_MS);
}, 250);
});