// Static server for the built SPA (replaces `pm2 serve dist 8443 --spa`). // Exists for ONE reason pm2-serve can't do: correct cache headers. // - index.html (and any SPA-fallback response): Cache-Control: no-store // -> every page load fetches fresh HTML, so deploys are visible on a // normal reload. This ends the recurring "deployed but users still see // the old UI" stale-tab problem (bit us three times on 2026-08-28). // - /assets/* (Vite content-hashed filenames): immutable, cached 1 year // -> repeat visits stay fast; a new build = new hash = new URL. // Zero dependencies; run under PM2 as "audit-portal-fe". const http = require("http"); const fs = require("fs"); const path = require("path"); const PORT = 8443; const ROOT = path.join(__dirname, "dist"); const MIME = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json", ".map": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".woff": "font/woff", ".woff2": "font/woff2", ".webmanifest": "application/manifest+json", }; const sendFile = (res, filePath, cacheControl) => { const ext = path.extname(filePath).toLowerCase(); res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream", "Cache-Control": cacheControl, }); fs.createReadStream(filePath).pipe(res); }; http .createServer((req, res) => { // Resolve inside ROOT only - refuse path traversal. const urlPath = decodeURIComponent((req.url || "/").split("?")[0]); const resolved = path.normalize(path.join(ROOT, urlPath)); if (!resolved.startsWith(ROOT)) { res.writeHead(403).end(); return; } if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) { // Hashed build assets are immutable; everything else served as a real // file (favicon, images) gets a short cache. const immutable = urlPath.startsWith("/assets/"); const cache = resolved.endsWith(".html") ? "no-store" : immutable ? "public, max-age=31536000, immutable" : "public, max-age=300"; sendFile(res, resolved, cache); return; } // SPA fallback: every unknown path is the app (React Router owns routing). sendFile(res, path.join(ROOT, "index.html"), "no-store"); }) .listen(PORT, "0.0.0.0", () => { console.log(`audit-portal-fe static server on :${PORT} (root: ${ROOT})`); });