feat(server): add production mode with static file serving and SPA fallback.

This commit is contained in:
K
2026-05-03 22:42:52 +05:30
parent fd73b8bde5
commit e8b5beca5e
2 changed files with 39 additions and 4 deletions
+9
View File
@@ -7,6 +7,15 @@ GROQ_API_KEY=your_groq_api_key_here
# Server port (optional, defaults to 5000) # Server port (optional, defaults to 5000)
PORT=5000 PORT=5000
# Set to "production" in production deployments.
# Enables static file serving of the built React app (web/client/dist).
# Run "npm run build" in web/client before starting the server.
NODE_ENV=production
# Allowed CORS origins (comma-separated). Defaults to localhost dev ports.
# Set this to your production domain(s) in production.
# CORS_ORIGIN=https://yourdomain.com
# Python interpreter for the retrieval daemon (optional, defaults to "python"). # Python interpreter for the retrieval daemon (optional, defaults to "python").
# Must be "python", "python3", or an absolute path to a Python 3 executable. # Must be "python", "python3", or an absolute path to a Python 3 executable.
# Do not set this to an arbitrary binary -- the value is validated on startup. # Do not set this to an arbitrary binary -- the value is validated on startup.
+30 -4
View File
@@ -77,9 +77,13 @@ let chunks = [];
try { try {
standards = JSON.parse(fs.readFileSync(path.join(DATA_DIR, "standards.json"), "utf-8")); standards = JSON.parse(fs.readFileSync(path.join(DATA_DIR, "standards.json"), "utf-8"));
chunks = JSON.parse(fs.readFileSync(path.join(DATA_DIR, "standards_chunks.json"), "utf-8")); chunks = JSON.parse(fs.readFileSync(path.join(DATA_DIR, "standards_chunks.json"), "utf-8"));
if (!Array.isArray(standards) || !Array.isArray(chunks)) {
throw new Error("standards.json or standards_chunks.json is not a JSON array");
}
console.log(`[init] Loaded ${standards.length} standards, ${chunks.length} chunks`); console.log(`[init] Loaded ${standards.length} standards, ${chunks.length} chunks`);
} catch (e) { } catch (e) {
console.error("[init] Failed to load data:", e.message); console.error("[init] Failed to load data:", e.message);
console.error("[init] API will return empty results. Run the data pipeline first.");
} }
// Pre-build lookups // Pre-build lookups
@@ -198,6 +202,12 @@ function bestChunk(standardId, question) {
return best; return best;
} }
// Serve React production build when NODE_ENV=production
const CLIENT_DIST = path.join(__dirname, "../client/dist");
if (process.env.NODE_ENV === "production" && fs.existsSync(CLIENT_DIST)) {
app.use(express.static(CLIENT_DIST));
}
// Routes // Routes
/** /**
@@ -208,8 +218,8 @@ function bestChunk(standardId, question) {
app.get("/api/standards", (req, res) => { app.get("/api/standards", (req, res) => {
const q = sanitizeText(req.query.q || "", 200); const q = sanitizeText(req.query.q || "", 200);
const category = sanitizeText(req.query.category || "", 100); const category = sanitizeText(req.query.category || "", 100);
const pageNum = Math.max(1, parseInt(req.query.page) || 1); const pageNum = Math.max(1, parseInt(req.query.page, 10) || 1);
const limitNum = Math.min(100, Math.max(1, parseInt(req.query.limit) || 20)); const limitNum = Math.min(100, Math.max(1, parseInt(req.query.limit, 10) || 20));
let results = standards; let results = standards;
if (category) results = results.filter((s) => s.category === category); if (category) results = results.filter((s) => s.category === category);
@@ -234,7 +244,7 @@ app.get("/api/standards", (req, res) => {
* Returns a single standard by its IS identifier; 404 if not found, 400 if the id is malformed. * Returns a single standard by its IS identifier; 404 if not found, 400 if the id is malformed.
*/ */
app.get("/api/standards/:id", (req, res) => { app.get("/api/standards/:id", (req, res) => {
const raw = decodeURIComponent(req.params.id); const raw = req.params.id;
if (!isValidStandardId(raw)) { if (!isValidStandardId(raw)) {
return res.status(400).json({ error: "Invalid standard ID format." }); return res.status(400).json({ error: "Invalid standard ID format." });
} }
@@ -277,7 +287,7 @@ app.get("/api/stats", (req, res) => {
*/ */
app.post("/api/recommend", async (req, res) => { app.post("/api/recommend", async (req, res) => {
const rawQuery = req.body?.query; const rawQuery = req.body?.query;
const top_n = Math.min(10, Math.max(1, parseInt(req.body?.top_n) || 5)); const top_n = Math.min(10, Math.max(1, parseInt(req.body?.top_n, 10) || 5));
const rewrite = req.body?.rewrite === true; const rewrite = req.body?.rewrite === true;
const query = sanitizeText(rawQuery, 500); const query = sanitizeText(rawQuery, 500);
@@ -445,6 +455,22 @@ app.post("/api/chat", async (req, res) => {
res.json({ answer }); res.json({ answer });
}); });
// SPA fallback: serve index.html for all non-API routes in production
if (process.env.NODE_ENV === "production" && fs.existsSync(CLIENT_DIST)) {
app.get("*", (req, res) => {
res.sendFile(path.join(CLIENT_DIST, "index.html"));
});
}
process.on("unhandledRejection", (reason) => {
console.error("[ERROR] Unhandled promise rejection:", reason);
});
process.on("uncaughtException", (err) => {
console.error("[ERROR] Uncaught exception:", err.message);
process.exit(1);
});
const server = app.listen(PORT, () => { const server = app.listen(PORT, () => {
console.log(`[init] BIS API running on http://localhost:${PORT}`); console.log(`[init] BIS API running on http://localhost:${PORT}`);
}); });