spirit-tracker/viz/serve.js
Brennan Wilkes (Text Groove) 7a33d51c90 UX Improvements
2026-02-10 16:45:22 -08:00

159 lines
4.7 KiB
JavaScript
Executable file

#!/usr/bin/env node
"use strict";
const http = require("http");
const fs = require("fs");
const path = require("path");
const root = path.resolve(__dirname); // viz/
const projectRoot = path.resolve(__dirname, ".."); // repo root
const MIME = {
".html": "text/html; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".svg": "image/svg+xml",
};
function safePath(urlPath) {
const p = decodeURIComponent(urlPath.split("?")[0]).replace(/\\/g, "/");
const joined = path.join(root, p);
const norm = path.normalize(joined);
if (!norm.startsWith(root)) return null;
return norm;
}
// Project-level file (shared by viz + report tooling)
const LINKS_FILE = path.join(projectRoot, "data", "sku_links.json");
function readMeta() {
try {
const raw = fs.readFileSync(LINKS_FILE, "utf8");
const obj = JSON.parse(raw);
const links = obj && Array.isArray(obj.links) ? obj.links : [];
const ignores = obj && Array.isArray(obj.ignores) ? obj.ignores : [];
return { generatedAt: obj?.generatedAt || new Date().toISOString(), links, ignores };
} catch {}
return { generatedAt: new Date().toISOString(), links: [], ignores: [] };
}
function writeMeta(obj) {
obj.generatedAt = new Date().toISOString();
fs.mkdirSync(path.dirname(LINKS_FILE), { recursive: true });
fs.writeFileSync(LINKS_FILE, JSON.stringify(obj, null, 2) + "\n", "utf8");
}
function send(res, code, body, headers) {
res.writeHead(code, { "Content-Type": "text/plain; charset=utf-8", ...(headers || {}) });
res.end(body);
}
function sendJson(res, code, obj) {
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(obj));
}
const server = http.createServer((req, res) => {
const u = req.url || "/";
const url = new URL(u, "http://127.0.0.1");
// Local API: read/write sku links + ignore pairs on disk (only exists when using this local server)
if (url.pathname === "/__stviz/sku-links") {
if (req.method === "GET") {
const obj = readMeta();
return sendJson(res, 200, { ok: true, count: obj.links.length, links: obj.links, ignores: obj.ignores });
}
if (req.method === "POST") {
let body = "";
req.on("data", (c) => (body += c));
req.on("end", () => {
try {
const inp = JSON.parse(body || "{}");
const fromSku = String(inp.fromSku || "").trim();
const toSku = String(inp.toSku || "").trim();
if (!fromSku || !toSku) return sendJson(res, 400, { ok: false, error: "fromSku/toSku required" });
const obj = readMeta();
obj.links.push({ fromSku, toSku, createdAt: new Date().toISOString() });
writeMeta(obj);
return sendJson(res, 200, { ok: true, count: obj.links.length, file: "data/sku_links.json" });
} catch (e) {
return sendJson(res, 400, { ok: false, error: String(e && e.message ? e.message : e) });
}
});
return;
}
return send(res, 405, "Method Not Allowed");
}
if (url.pathname === "/__stviz/sku-ignores") {
if (req.method === "GET") {
const obj = readMeta();
return sendJson(res, 200, { ok: true, count: obj.ignores.length, ignores: obj.ignores });
}
if (req.method === "POST") {
let body = "";
req.on("data", (c) => (body += c));
req.on("end", () => {
try {
const inp = JSON.parse(body || "{}");
const skuA = String(inp.skuA || "").trim();
const skuB = String(inp.skuB || "").trim();
if (!skuA || !skuB) return sendJson(res, 400, { ok: false, error: "skuA/skuB required" });
if (skuA === skuB) return sendJson(res, 400, { ok: false, error: "skuA and skuB must differ" });
const obj = readMeta();
obj.ignores.push({ skuA, skuB, createdAt: new Date().toISOString() });
writeMeta(obj);
return sendJson(res, 200, { ok: true, count: obj.ignores.length, file: "data/sku_links.json" });
} catch (e) {
return sendJson(res, 400, { ok: false, error: String(e && e.message ? e.message : e) });
}
});
return;
}
return send(res, 405, "Method Not Allowed");
}
// Static
let file = safePath(u === "/" ? "/index.html" : u);
if (!file) {
res.writeHead(400);
res.end("Bad path");
return;
}
if (fs.existsSync(file) && fs.statSync(file).isDirectory()) {
file = path.join(file, "index.html");
}
fs.readFile(file, (err, buf) => {
if (err) {
res.writeHead(404);
res.end("Not found");
return;
}
const ext = path.extname(file);
res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream" });
res.end(buf);
});
});
const port = Number(process.env.PORT || 8080);
server.listen(port, "127.0.0.1", () => {
process.stdout.write(`Serving ${root} on http://127.0.0.1:${port}\n`);
process.stdout.write(`SKU links file: ${LINKS_FILE}\n`);
});