Spaces:
Sleeping
Sleeping
| /** | |
| * Genesis AI β Price Elasticity Intelligence Platform | |
| * Node.js / Express server for Hugging Face Spaces deployment | |
| * | |
| * Routes: | |
| * GET / β serves the built dashboard HTML | |
| * GET /health β health check (HF uses this) | |
| * POST /api/build β triggers a fresh dashboard build from /output JSON files | |
| * GET /api/data/:key β serves individual JSON artefact files | |
| */ | |
| const express = require('express'); | |
| const path = require('path'); | |
| const fs = require('fs'); | |
| const { execSync } = require('child_process'); | |
| const app = express(); | |
| const PORT = process.env.PORT || 7860; // Hugging Face requires 7860 | |
| // ββ Paths ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const ROOT = __dirname; | |
| const PUBLIC_DIR = path.join(ROOT, 'public'); | |
| const OUTPUT_DIR = path.join(ROOT, 'output'); // JSON artefacts from Python pipeline | |
| const DASH_HTML = path.join(PUBLIC_DIR, 'dashboard.html'); | |
| // ββ Middleware βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.use(express.json()); | |
| app.use(express.static(PUBLIC_DIR)); | |
| // ββ Health check (Hugging Face pings this) βββββββββββββββββββββββββββββββββββ | |
| app.get('/health', (req, res) => { | |
| res.json({ status: 'ok', timestamp: new Date().toISOString() }); | |
| }); | |
| // ββ Serve dashboard ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get('/', (req, res) => { | |
| if (fs.existsSync(DASH_HTML)) { | |
| res.sendFile(DASH_HTML); | |
| } else { | |
| // Dashboard not built yet β show a friendly waiting page | |
| res.send(`<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Genesis AI β Building Dashboardβ¦</title> | |
| <style> | |
| body { font-family: 'Segoe UI', sans-serif; background: #f5f0e8; | |
| display: flex; align-items: center; justify-content: center; | |
| height: 100vh; margin: 0; } | |
| .box { background: #fff; border: 1px solid rgba(26,107,47,.2); | |
| border-radius: 12px; padding: 40px 48px; text-align: center; | |
| box-shadow: 0 4px 20px rgba(0,0,0,.08); max-width: 480px; } | |
| h1 { font-size: 20px; color: #1a6b2f; margin-bottom: 8px; } | |
| p { color: #6b6b6b; font-size: 14px; line-height: 1.6; } | |
| pre { background: #f0ead8; border-radius: 7px; padding: 14px 18px; | |
| font-size: 12px; text-align: left; color: #2c2c2c; margin-top: 20px; | |
| overflow-x: auto; } | |
| .badge { display:inline-block; background:rgba(26,107,47,.1); | |
| color:#1a6b2f; padding:3px 10px; border-radius:12px; | |
| font-size:11px; font-weight:600; margin-bottom:16px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="box"> | |
| <div class="badge">Genesis AI</div> | |
| <h1>Dashboard not yet built</h1> | |
| <p>The Python pipeline has not run yet, or the JSON artefacts are missing | |
| from <code>/output/</code>. Run the pipeline first, then rebuild:</p> | |
| <pre>python elasticity_model.py \\ | |
| --input Brand_Onboarding_Template.xlsx \\ | |
| --focal "DABUR SARSON AMLA" \\ | |
| --output ./output/ | |
| python dashboard_builder.py \\ | |
| --data-dir ./output/ \\ | |
| --brand "DABUR SARSON AMLA" \\ | |
| --css public/dashboard.css \\ | |
| --js public/dashboard.js \\ | |
| --out public/dashboard.html</pre> | |
| <p>Or POST to <code>/api/build</code> if artefacts already exist in <code>/output/</code>.</p> | |
| </div> | |
| </body> | |
| </html>`); | |
| } | |
| }); | |
| // ββ API: serve individual JSON artefacts ββββββββββββββββββββββββββββββββββββββ | |
| // GET /api/data/models β output/models.json | |
| // GET /api/data/trend β output/trend.json β¦ etc. | |
| const JSON_MAP = { | |
| models: 'models.json', | |
| recs: 'recs_full.json', | |
| trend: 'trend.json', | |
| ms: 'ms.json', | |
| vol_salience: 'vol_salience.json', | |
| val_share: 'val_share.json', | |
| freq_anchors: 'freq_anchors.json', | |
| ppa_mt: 'ppa_mt.json', | |
| ppa_tt: 'ppa_tt.json', | |
| comp_ms: 'comp_ms.json', | |
| vtm: 'vtm.json', | |
| interaction: 'interaction.json', | |
| growth_decomp: 'growth_decomp.json', | |
| pgi: 'pgi.json', | |
| stats: 'stats.json', | |
| }; | |
| app.get('/api/data/:key', (req, res) => { | |
| const fname = JSON_MAP[req.params.key]; | |
| if (!fname) return res.status(404).json({ error: 'Unknown data key' }); | |
| const fpath = path.join(OUTPUT_DIR, fname); | |
| if (!fs.existsSync(fpath)) { | |
| return res.status(404).json({ error: `File not found: ${fname}` }); | |
| } | |
| res.setHeader('Content-Type', 'application/json'); | |
| res.sendFile(fpath); | |
| }); | |
| // ββ API: trigger a dashboard rebuild ββββββββββββββββββββββββββββββββββββββββββ | |
| // POST /api/build body: { brand: "DABUR SARSON AMLA" } | |
| app.post('/api/build', (req, res) => { | |
| const brand = req.body?.brand || 'DABUR SARSON AMLA'; | |
| // Check JSON artefacts exist | |
| const needed = ['models.json', 'recs_full.json', 'trend.json']; | |
| const missing = needed.filter(f => !fs.existsSync(path.join(OUTPUT_DIR, f))); | |
| if (missing.length) { | |
| return res.status(400).json({ | |
| error: 'Missing JSON artefacts β run elasticity_model.py first', | |
| missing, | |
| }); | |
| } | |
| try { | |
| const cmd = [ | |
| 'python3 dashboard_builder.py', | |
| `--data-dir "${OUTPUT_DIR}"`, | |
| `--brand "${brand}"`, | |
| `--css "${path.join(PUBLIC_DIR, 'dashboard.css')}"`, | |
| `--js "${path.join(PUBLIC_DIR, 'dashboard.js')}"`, | |
| `--out "${DASH_HTML}"`, | |
| ].join(' '); | |
| execSync(cmd, { cwd: ROOT, stdio: 'pipe' }); | |
| const size = fs.statSync(DASH_HTML).size; | |
| res.json({ | |
| ok: true, | |
| message: 'Dashboard rebuilt successfully', | |
| file: DASH_HTML, | |
| size_kb: Math.round(size / 1024), | |
| }); | |
| } catch (err) { | |
| res.status(500).json({ | |
| error: 'Build failed', | |
| detail: err.stderr?.toString() || err.message, | |
| }); | |
| } | |
| }); | |
| // ββ API: list available JSON artefacts ββββββββββββββββββββββββββββββββββββββββ | |
| app.get('/api/status', (req, res) => { | |
| const files = Object.entries(JSON_MAP).map(([key, fname]) => ({ | |
| key, | |
| file: fname, | |
| exists: fs.existsSync(path.join(OUTPUT_DIR, fname)), | |
| })); | |
| res.json({ | |
| dashboard_built: fs.existsSync(DASH_HTML), | |
| artefacts: files, | |
| }); | |
| }); | |
| // ββ Start βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.listen(PORT, '0.0.0.0', () => { | |
| console.log(` | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β Genesis AI β Price Elasticity Intelligence β | |
| β Server running on http://0.0.0.0:${PORT} β | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| `); | |
| console.log(`Dashboard built: ${fs.existsSync(DASH_HTML) ? 'β YES' : 'β NO (run pipeline first)'}`); | |
| console.log(`Artefacts dir : ${OUTPUT_DIR}`); | |
| }); | |