← back to Ventura Claw

viewer/server.js

84 lines

#!/usr/bin/env node
// VenturaClaw — Live Build Viewer
// Streams build-events.jsonl via SSE; serves liquid-glass UI on :9785

const http = require('http');
const fs = require('fs');
const path = require('path');

const PORT = process.env.PORT || 9785;
const ROOT = path.resolve(__dirname, '..');
const EVENTS_FILE = path.join(ROOT, 'logs', 'build-events.jsonl');
const PUBLIC_DIR = path.join(__dirname, 'public');

if (!fs.existsSync(path.dirname(EVENTS_FILE))) fs.mkdirSync(path.dirname(EVENTS_FILE), { recursive: true });
if (!fs.existsSync(EVENTS_FILE)) fs.writeFileSync(EVENTS_FILE, '');

function readAllEvents() {
  try {
    return fs.readFileSync(EVENTS_FILE, 'utf8')
      .split('\n').filter(Boolean)
      .map(l => { try { return JSON.parse(l); } catch { return null; } })
      .filter(Boolean);
  } catch { return []; }
}

function sendSSE(res) {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Access-Control-Allow-Origin': '*'
  });

  let lastSize = 0;
  const push = () => {
    try {
      const stat = fs.statSync(EVENTS_FILE);
      if (stat.size === lastSize) return;
      const fd = fs.openSync(EVENTS_FILE, 'r');
      const buf = Buffer.alloc(stat.size - lastSize);
      fs.readSync(fd, buf, 0, buf.length, lastSize);
      fs.closeSync(fd);
      lastSize = stat.size;
      buf.toString('utf8').split('\n').filter(Boolean).forEach(line => {
        res.write(`data: ${line}\n\n`);
      });
    } catch (e) { /* ignore */ }
  };

  // initial backfill
  readAllEvents().forEach(ev => res.write(`data: ${JSON.stringify(ev)}\n\n`));
  lastSize = fs.statSync(EVENTS_FILE).size;

  const iv = setInterval(push, 500);
  const hb = setInterval(() => res.write(': hb\n\n'), 15000);
  res.on('close', () => { clearInterval(iv); clearInterval(hb); });
}

const server = http.createServer((req, res) => {
  if (req.url === '/events') return sendSSE(res);
  if (req.url === '/api/snapshot') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    return res.end(JSON.stringify({ events: readAllEvents() }));
  }
  if (req.url === '/healthz') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    return res.end('ok');
  }

  let urlPath = req.url === '/' ? '/index.html' : req.url.split('?')[0];
  const file = path.join(PUBLIC_DIR, urlPath);
  if (!file.startsWith(PUBLIC_DIR) || !fs.existsSync(file)) {
    res.writeHead(404); return res.end('Not found');
  }
  const ext = path.extname(file).slice(1);
  const types = { html: 'text/html', css: 'text/css', js: 'application/javascript', svg: 'image/svg+xml', png: 'image/png', json: 'application/json' };
  res.writeHead(200, { 'Content-Type': types[ext] || 'application/octet-stream' });
  fs.createReadStream(file).pipe(res);
});

server.listen(PORT, '127.0.0.1', () => {
  console.log(`[ventura-claw-viewer] listening on http://127.0.0.1:${PORT}`);
});