← 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}`));