← back to Majilite Onboard
server.js
98 lines
#!/usr/bin/env node
/**
* Majilite Metallic Specialties I — live onboarding viewer (zero-dependency).
* Basic-auth admin/DW2024!. Serves the staged catalog + an SSE "import" stream
* so you watch all 160 SKUs flow through the pipeline stages as they onboard.
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
const ROOT = __dirname;
const PORT = process.env.PORT ? Number(process.env.PORT) : 0; // 0 = OS-assigned free port
const USER = process.env.BASIC_AUTH_USER || 'admin';
const PASS = process.env.BASIC_AUTH_PASS || 'DW2024!';
const products = () => JSON.parse(fs.readFileSync(path.join(ROOT, 'data/products.json'), 'utf8'));
const collection = () => JSON.parse(fs.readFileSync(path.join(ROOT, 'data/collection.json'), 'utf8'));
const MIME = { '.html':'text/html', '.js':'application/javascript', '.css':'text/css',
'.json':'application/json', '.png':'image/png', '.jpg':'image/jpeg', '.svg':'image/svg+xml' };
function unauth(res){ res.writeHead(401,{'WWW-Authenticate':'Basic realm="majilite"'}); res.end('Auth required'); }
function ok(res,body,type='application/json'){ res.writeHead(200,{'Content-Type':type}); res.end(body); }
const server = http.createServer((req,res)=>{
// Basic auth gate (Steve's standing viewer rule)
const h = req.headers.authorization || '';
const [u,p] = Buffer.from(h.split(' ')[1]||'', 'base64').toString().split(':');
if (u!==USER || p!==PASS) return unauth(res);
const url = new URL(req.url, 'http://x');
const p0 = url.pathname;
if (p0==='/api/collection') return ok(res, JSON.stringify(collection()));
if (p0==='/api/products') return ok(res, JSON.stringify(products()));
if (p0==='/api/facets'){
const all = products();
const tally = (key)=>{ const m={}; for(const x of all){ const v=x[key]; if(v) m[v]=(m[v]||0)+1; } return m; };
const patTally={}; for(const x of all){ patTally[x.pattern]=(patTally[x.pattern]||0)+1; }
return ok(res, JSON.stringify({
total: all.length,
color_family: tally('color_family'),
pattern: patTally,
product_type: tally('product_type'),
onboard_status: tally('onboard_status')
}));
}
// SSE live-import stream: emits start, one 'item' per SKU (staged through 5 stages), then done.
if (p0==='/api/import/stream'){
const speed = Math.max(8, Math.min(400, Number(url.searchParams.get('speed'))||45)); // ms/item
res.writeHead(200,{'Content-Type':'text/event-stream','Cache-Control':'no-cache','Connection':'keep-alive'});
const all = products();
const STAGES = ['parsed','sku_assigned','spec_attached','image_linked','staged'];
res.write(`event: start\ndata: ${JSON.stringify({total:all.length, stages:STAGES, collection:collection().collection})}\n\n`);
let i=0;
const timer = setInterval(()=>{
if (i>=all.length){
res.write(`event: done\ndata: ${JSON.stringify({total:all.length})}\n\n`);
clearInterval(timer); res.end(); return;
}
const pr = all[i++];
const hasImage = fs.existsSync(path.join(ROOT,'public',pr.image));
res.write(`event: item\ndata: ${JSON.stringify({
i, dw_sku:pr.dw_sku, mfr_sku:pr.mfr_sku, title:pr.title, pattern:pr.pattern,
color:pr.color, color_family:pr.color_family, color_hex:pr.color_hex,
image: hasImage ? pr.image : null, has_image: hasImage, tags:pr.tags,
pricing:pr.pricing, created_at:pr.created_at, spec_ok:true
})}\n\n`);
}, speed);
req.on('close', ()=>clearInterval(timer));
return;
}
// index.html — inject bootstrap payload so first paint needs no second authed fetch
if (p0==='/' || p0==='/index.html'){
let html = fs.readFileSync(path.join(ROOT,'public','index.html'),'utf8');
const boot = `<script>window.__BOOT__=${JSON.stringify({collection:collection(),products:products()})};</script>`;
html = html.replace('</head>', boot+'\n</head>');
return ok(res, html, 'text/html');
}
// other static files
let file = path.join(ROOT, 'public', p0);
if (!file.startsWith(path.join(ROOT,'public'))) { res.writeHead(403); return res.end('no'); }
fs.readFile(file,(e,buf)=>{
if (e){ res.writeHead(404); return res.end('not found'); }
ok(res, buf, MIME[path.extname(file)]||'application/octet-stream');
});
});
server.listen(PORT, ()=>{
const port = server.address().port;
fs.writeFileSync(path.join(ROOT,'.port'), String(port));
console.log(`Majilite onboarding viewer → http://127.0.0.1:${port} (admin/DW2024!)`);
});