← back to Doing Viewer

server-mirror.js

56 lines

#!/usr/bin/env node
// server-mirror.js  (runs on KAMATERA — always-on public face)
// Serves the board purely from data/snapshot.json pushed by Mac2. No ticket log,
// no Ollama, no lib.js. If Mac2 stops syncing, it keeps serving the last snapshot
// with a staleness flag the UI surfaces. Same front-end as the primary.

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

const PORT      = process.env.PORT || 9802;
const SNAP      = path.join(__dirname, 'data', 'snapshot.json');
const INDEX     = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
const AUTH_USER = process.env.BASIC_USER || 'admin';
const AUTH_PASS = process.env.BASIC_PASS || 'DW2024!';

function authed(req) {
  const h = req.headers['authorization'] || '';
  if (!h.startsWith('Basic ')) return false;
  const [u, p] = Buffer.from(h.slice(6), 'base64').toString().split(':');
  return u === AUTH_USER && p === AUTH_PASS;
}
function readSnap() {
  try { return JSON.parse(fs.readFileSync(SNAP, 'utf8')); }
  catch { return { syncedAt: null, source: 'mac2', count: 0, items: [] }; }
}

http.createServer((req, res) => {
  // health check is open (no auth) so deploy.sh smoke test + monitors work
  if (req.url === '/api/healthz') {
    const s = readSnap();
    const ageMin = s.syncedAt ? (Date.now() - new Date(s.syncedAt).getTime()) / 60000 : null;
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ ok: true, mirror: true, count: s.count, syncedAt: s.syncedAt, syncAgeMin: ageMin }));
    return;
  }
  if (!authed(req)) {
    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="doing-viewer (mirror)"' });
    res.end('auth required'); return;
  }
  if (req.url === '/api/doing') {
    const s = readSnap();
    res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
    res.end(JSON.stringify({
      now: new Date().toISOString(), count: s.count, pending: 0,
      mirror: true, canAct: false, syncedAt: s.syncedAt, items: s.items,
    }));
    return;
  }
  if (req.url === '/' || req.url === '/index.html') {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.end(INDEX); return;
  }
  res.writeHead(404); res.end('not found');
}).listen(PORT, () => console.log(`doing-viewer MIRROR on :${PORT}`));