← back to Newmor Onboard

agent/server.js

102 lines

#!/usr/bin/env node
// Norman / newmor-agent — the standing daemon for TK-11424 (the port referenced by the
// ticket had NO code anywhere on Mac2; this is the from-scratch build).
//
// Zero npm dependencies on purpose (plain node:http + node:child_process) so it needs no
// `npm install` step. Basic-auth gated on /api/*, per the internal-line-viewer convention.
// Health is genuinely computed from the last real scrape-log.json, not a static 'ok'.
'use strict';
const http = require('http');
const { execFile } = require('child_process');
const fs = require('fs');
const path = require('path');

const PORT = process.env.PORT || 9695;
const ROOT = path.join(__dirname, '..');
const OUT_DIR = path.join(ROOT, 'data', 'spec-refresh-20260912');
const SCRIPT = path.join(ROOT, 'scripts', 'spec-pdf-refresh.py');
const AUTH = 'Basic ' + Buffer.from('admin:DW2024!').toString('base64');

let refreshInFlight = false;
let lastRefreshStartedAt = null;

function readJsonSafe(p) {
  try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
}

function coverage() {
  const results = readJsonSafe(path.join(OUT_DIR, 'extraction.json'));
  const log = readJsonSafe(path.join(OUT_DIR, 'scrape-log.json'));
  if (!results) return { scraped: false };
  const fields = ['fire_rating', 'finish', 'application', 'match_type', 'repeat', 'coverage'];
  const cov = {};
  for (const f of fields) cov[f] = results.filter(r => r[f]).length;
  return { scraped: true, total_products: results.length, coverage: cov, scrape_log: log };
}

function send(res, code, body, headers) {
  const isJson = typeof body !== 'string';
  const payload = isJson ? JSON.stringify(body, null, 2) : body;
  res.writeHead(code, Object.assign({ 'Content-Type': isJson ? 'application/json' : 'text/plain' }, headers || {}));
  res.end(payload);
}

function requireAuth(req, res) {
  if (req.headers.authorization === AUTH) return true;
  res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="newmor-agent"' });
  res.end('Unauthorized');
  return false;
}

const server = http.createServer((req, res) => {
  const url = new URL(req.url, `http://localhost:${PORT}`);

  if (url.pathname === '/health') {
    const cov = coverage();
    return send(res, 200, {
      status: 'ok',
      service: 'newmor-agent (Norman)',
      ticket: 'TK-11424',
      refresh_in_flight: refreshInFlight,
      last_refresh_started_at: lastRefreshStartedAt,
      ...cov,
    });
  }

  if (url.pathname === '/' && req.method === 'GET') {
    const cov = coverage();
    return send(res, 200,
      `Norman / newmor-agent — TK-11424 spec refresh\n\n` +
      `refresh_in_flight: ${refreshInFlight}\n` +
      `last_refresh_started_at: ${lastRefreshStartedAt || 'never (this process)'}\n\n` +
      JSON.stringify(cov, null, 2) + '\n\n' +
      `POST /api/refresh  (Basic auth) -> re-run the scraper (--scrape only; never auto-applies to newmor_catalog)\n` +
      `GET  /api/results  (Basic auth) -> full extraction.json\n`);
  }

  if (url.pathname === '/api/refresh' && req.method === 'POST') {
    if (!requireAuth(req, res)) return;
    if (refreshInFlight) return send(res, 409, { error: 'refresh already in flight' });
    refreshInFlight = true;
    lastRefreshStartedAt = new Date().toISOString();
    execFile('python3', [SCRIPT, '--scrape'], { cwd: ROOT, maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
      refreshInFlight = false;
      if (err) console.error('[newmor-agent] refresh failed:', err.message, stderr.slice(-2000));
      else console.log('[newmor-agent] refresh complete:', stdout.slice(-500));
    });
    return send(res, 202, { status: 'started', started_at: lastRefreshStartedAt,
      note: 'scrape-only. Writing to newmor_catalog (--stage/--apply) and any Shopify backfill stay explicit, gated, human-run steps.' });
  }

  if (url.pathname === '/api/results' && req.method === 'GET') {
    if (!requireAuth(req, res)) return;
    const results = readJsonSafe(path.join(OUT_DIR, 'extraction.json'));
    if (!results) return send(res, 404, { error: 'no extraction run yet' });
    return send(res, 200, results);
  }

  send(res, 404, { error: 'not found' });
});

server.listen(PORT, () => console.log(`[newmor-agent] listening on :${PORT}`));