← back to Fm Wallpaper Sync
viewer/server.js
87 lines
#!/usr/bin/env node
// Local approval viewer for new WALLPAPER fmpro SKUs (fmpro_wallpaper_review).
// Basic-auth admin/DW2024!. Zero deps — queries via psql. Read/write dw_unified.
const http = require('http');
const { execFile } = require('child_process');
const fs = require('fs');
const path = require('path');
const PORT = 9796, USER = 'admin', PASS = 'DW2024!';
const PG = ['-h', '127.0.0.1', '-U', 'dw_admin', '-d', 'dw_unified', '-tAqX'];
const HTML = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8');
const q1 = s => "'" + String(s).replace(/'/g, "''") + "'"; // sql string literal
const clean = s => String(s || '').replace(/[^\x20-\x7E]/g, '').slice(0, 120);
const skuOk = s => /^[A-Za-z0-9_.\- ]{1,60}$/.test(s);
function sql(query) {
return new Promise((res, rej) => {
execFile('psql', [...PG, '-c', query], { maxBuffer: 64 * 1024 * 1024 }, (e, out) => e ? rej(e) : res(out));
});
}
const jsonQuery = async (inner) =>
JSON.parse((await sql(`SELECT coalesce(json_agg(t),'[]') FROM (${inner}) t`)).trim() || '[]');
function whereClause({ status, vendor, q }) {
const w = [];
w.push(`review_status = ${q1((status && ['pending','approved','rejected'].includes(status)) ? status : 'pending')}`);
if (vendor) w.push(vendor === '(none)' ? `nullif(vendor_name,'') IS NULL` : `vendor_name = ${q1(clean(vendor))}`);
if (q) { const t = q1('%' + clean(q).toLowerCase() + '%'); w.push(`(lower(sku) LIKE ${t} OR lower(coalesce(mfr_sku,'')) LIKE ${t} OR lower(coalesce(title,'')) LIKE ${t})`); }
return w.join(' AND ');
}
const send = (r, code, body, type = 'application/json') => { r.writeHead(code, { 'Content-Type': type }); r.end(body); };
const server = http.createServer(async (req, res) => {
const auth = (req.headers.authorization || '').split(' ')[1] || '';
const [u, p] = Buffer.from(auth, 'base64').toString().split(':');
if (u !== USER || p !== PASS) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="fmpro-review"' }); return res.end('auth'); }
const url = new URL(req.url, 'http://x');
try {
if (url.pathname === '/') return send(res, 200, HTML, 'text/html');
if (url.pathname === '/api/summary') {
const counts = await jsonQuery(`SELECT review_status status, count(*)::int n FROM fmpro_wallpaper_review GROUP BY 1`);
const vendors = await jsonQuery(`SELECT coalesce(nullif(vendor_name,''),'(none)') vendor, count(*)::int n
FROM fmpro_wallpaper_review WHERE review_status='pending' GROUP BY 1 ORDER BY 2 DESC`);
const batches = await jsonQuery(`SELECT batch_date::text batch, count(*)::int n, count(*) FILTER (WHERE review_status='pending')::int pending
FROM fmpro_wallpaper_review GROUP BY 1 ORDER BY 1 DESC`);
return send(res, 200, JSON.stringify({ counts, vendors, batches }));
}
if (url.pathname === '/api/list') {
const g = Object.fromEntries(url.searchParams);
const limit = Math.min(500, Math.max(1, parseInt(g.limit) || 60));
const offset = Math.max(0, parseInt(g.offset) || 0);
const sort = ({ sku: 'sku', vendor: 'vendor_name NULLS LAST, sku', newest: 'first_seen DESC', img: '(image_url IS NOT NULL) DESC, sku' })[g.sort] || 'sku';
const where = whereClause(g);
const total = (await jsonQuery(`SELECT count(*)::int n FROM fmpro_wallpaper_review WHERE ${where}`))[0]?.n || 0;
const rows = await jsonQuery(`SELECT sku, review_status, vendor_name, mfr_sku, sku_prefix, image_filename,
image_url, title, to_char(first_seen,'Mon DD, YYYY, HH12:MI AM') seen, batch_date::text batch
FROM fmpro_wallpaper_review WHERE ${where} ORDER BY ${sort} LIMIT ${limit} OFFSET ${offset}`);
return send(res, 200, JSON.stringify({ total, rows, limit, offset }));
}
if (url.pathname === '/api/decide' && req.method === 'POST') {
let body = ''; for await (const c of req) body += c;
const b = JSON.parse(body || '{}');
const decision = b.decision === 'approved' ? 'approved' : b.decision === 'rejected' ? 'rejected' : b.decision === 'pending' ? 'pending' : null;
if (!decision) return send(res, 400, JSON.stringify({ error: 'bad decision' }));
let where;
if (b.scope === 'selected' && Array.isArray(b.skus) && b.skus.length) {
const list = b.skus.filter(skuOk).map(q1).join(',');
if (!list) return send(res, 400, JSON.stringify({ error: 'no valid skus' }));
where = `sku IN (${list})`;
} else if (b.scope === 'filter') {
where = whereClause(b); // acts on everything matching the current filter (status defaults to pending)
} else return send(res, 400, JSON.stringify({ error: 'bad scope' }));
const out = await sql(`WITH u AS (UPDATE fmpro_wallpaper_review
SET review_status=${q1(decision)}, decided_at=now(), decided_by='local-viewer'
WHERE ${where} RETURNING 1) SELECT count(*)::int FROM u`);
return send(res, 200, JSON.stringify({ updated: parseInt(out.trim()) || 0, decision }));
}
return send(res, 404, JSON.stringify({ error: 'not found' }));
} catch (e) { return send(res, 500, JSON.stringify({ error: String(e.message || e) })); }
});
server.listen(PORT, '127.0.0.1', () => console.log(`fmpro-review viewer on http://127.0.0.1:${PORT} (admin/DW2024!)`));