129 lines
4.0 KiB
JavaScript
129 lines
4.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Forecast Pipeline Orchestrator
|
|
*
|
|
* Spawns the Python forecast engine with database credentials from the
|
|
* environment. Can be run manually, via cron, or integrated into the
|
|
* existing metrics pipeline.
|
|
*
|
|
* Usage:
|
|
* node run_forecast.js
|
|
*
|
|
* Environment:
|
|
* Reads DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT from
|
|
* /var/www/html/inventory/.env (or current process env).
|
|
*/
|
|
|
|
const { spawn } = require('child_process');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
// Load .env file if it exists (production path)
|
|
const envPaths = [
|
|
'/var/www/html/inventory/.env',
|
|
path.join(__dirname, '../../.env'),
|
|
];
|
|
|
|
for (const envPath of envPaths) {
|
|
if (fs.existsSync(envPath)) {
|
|
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
for (const line of envContent.split('\n')) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
const eqIndex = trimmed.indexOf('=');
|
|
if (eqIndex === -1) continue;
|
|
const key = trimmed.slice(0, eqIndex);
|
|
const value = trimmed.slice(eqIndex + 1);
|
|
if (!process.env[key]) {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
console.log(`Loaded env from ${envPath}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Verify required env vars
|
|
const required = ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'];
|
|
const missing = required.filter(k => !process.env[k]);
|
|
if (missing.length > 0) {
|
|
console.error(`Missing required environment variables: ${missing.join(', ')}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const SCRIPT_DIR = __dirname;
|
|
const PYTHON_SCRIPT = path.join(SCRIPT_DIR, 'forecast_engine.py');
|
|
const VENV_DIR = path.join(SCRIPT_DIR, 'venv');
|
|
const REQUIREMENTS = path.join(SCRIPT_DIR, 'requirements.txt');
|
|
|
|
// Determine python binary (prefer venv if it exists)
|
|
function getPythonBin() {
|
|
const venvPython = path.join(VENV_DIR, 'bin', 'python');
|
|
if (fs.existsSync(venvPython)) return venvPython;
|
|
|
|
// Fall back to system python
|
|
return 'python3';
|
|
}
|
|
|
|
// Ensure venv and dependencies are installed
|
|
async function ensureDependencies() {
|
|
if (!fs.existsSync(path.join(VENV_DIR, 'bin', 'python'))) {
|
|
console.log('Creating virtual environment...');
|
|
await runCommand('python3', ['-m', 'venv', VENV_DIR]);
|
|
}
|
|
|
|
// Always run pip install — idempotent, fast when packages already present
|
|
console.log('Checking dependencies...');
|
|
const python = path.join(VENV_DIR, 'bin', 'python');
|
|
await runCommand(python, ['-m', 'pip', 'install', '--quiet', '-r', REQUIREMENTS]);
|
|
}
|
|
|
|
function runCommand(cmd, args, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const proc = spawn(cmd, args, {
|
|
stdio: 'inherit',
|
|
...options,
|
|
});
|
|
proc.on('close', code => {
|
|
if (code === 0) resolve();
|
|
else reject(new Error(`${cmd} exited with code ${code}`));
|
|
});
|
|
proc.on('error', reject);
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const startTime = Date.now();
|
|
console.log('='.repeat(60));
|
|
console.log(`Forecast Pipeline - ${new Date().toISOString()}`);
|
|
console.log('='.repeat(60));
|
|
|
|
try {
|
|
await ensureDependencies();
|
|
|
|
const pythonBin = getPythonBin();
|
|
console.log(`Using Python: ${pythonBin}`);
|
|
console.log(`Running: ${PYTHON_SCRIPT}`);
|
|
console.log('');
|
|
|
|
await runCommand(pythonBin, [PYTHON_SCRIPT], {
|
|
env: {
|
|
...process.env,
|
|
PYTHONUNBUFFERED: '1', // Real-time output
|
|
},
|
|
});
|
|
|
|
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
console.log('');
|
|
console.log('='.repeat(60));
|
|
console.log(`Forecast pipeline completed in ${duration}s`);
|
|
console.log('='.repeat(60));
|
|
} catch (err) {
|
|
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
console.error(`Forecast pipeline FAILED after ${duration}s:`, err.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|