From 25a6dd05a50c2b13081fffda2958c112467bd84f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:57:59 +0000 Subject: [PATCH 1/2] feat: add unicorn-shield, health-daemon, main-orchestrator; wire innovation-agent to PM2 Agent-Logs-Url: https://github.com/ruffy80/ZeusAI/sessions/1019f149-ab44-47c8-b914-678062505c41 Co-authored-by: ruffy80 <29306714+ruffy80@users.noreply.github.com> --- UNICORN_FINAL/ecosystem.config.js | 119 +++++- .../scripts/unicorn-health-daemon.js | 391 ++++++++++++++++++ .../scripts/unicorn-main-orchestrator.js | 328 +++++++++++++++ UNICORN_FINAL/scripts/unicorn-shield.js | 356 ++++++++++++++++ 4 files changed, 1192 insertions(+), 2 deletions(-) create mode 100644 UNICORN_FINAL/scripts/unicorn-health-daemon.js create mode 100644 UNICORN_FINAL/scripts/unicorn-main-orchestrator.js create mode 100644 UNICORN_FINAL/scripts/unicorn-shield.js diff --git a/UNICORN_FINAL/ecosystem.config.js b/UNICORN_FINAL/ecosystem.config.js index 8d18877b..8d9f9c82 100644 --- a/UNICORN_FINAL/ecosystem.config.js +++ b/UNICORN_FINAL/ecosystem.config.js @@ -148,7 +148,122 @@ module.exports = { log_date_format: 'YYYY-MM-DD HH:mm:ss' }, - // ── 5. Universal AI Connector — UAIC (model discovery & routing daemon) ── + // ── 5. Main Orchestrator (checkRepo / validateCode / deploy / rollback / notify) ─ + { + name: 'unicorn-main-orchestrator', + script: 'scripts/unicorn-main-orchestrator.js', + cwd: __dirname, + instances: 1, + autorestart: true, + watch: false, + max_restarts: 15, + min_uptime: '15s', + restart_delay: 10000, + exp_backoff_restart_delay: 3000, + env: { + NODE_ENV: 'production', + ORCH_POLL_MS: '120000', + ORCH_HEALTH_URL: 'http://127.0.0.1:3000/api/health', + ORCH_SHIELD_URL: 'http://127.0.0.1:3000/api/quantum-integrity/status', + ORCH_INNOVATION_URL: 'http://127.0.0.1:3000/api/innovation-loop/status', + ORCH_DEPLOY_CMD: 'bash scripts/deploy-hetzner.js 2>&1 || bash deploy.sh 2>&1', + ORCH_ROLLBACK_CMD: 'bash scripts/rollback-last-backup.sh', + ORCH_LINT_CMD: 'npm run lint --if-present', + ORCH_TEST_CMD: 'npm test --if-present', + DOMAIN: SITE_DOMAIN, + PUBLIC_APP_URL, + }, + error_file: 'logs/main-orchestrator-error.log', + out_file: 'logs/main-orchestrator-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss' + }, + + // ── 6. Shield (watchFiles / watchProcesses / autoRepair / emergencyRollback) ─ + { + name: 'unicorn-shield', + script: 'scripts/unicorn-shield.js', + cwd: __dirname, + instances: 1, + autorestart: true, + watch: false, + max_restarts: 20, + min_uptime: '10s', + restart_delay: 5000, + exp_backoff_restart_delay: 2000, + env: { + NODE_ENV: 'production', + SHIELD_INTERVAL_MS: '30000', + SHIELD_PROC_MS: '20000', + SHIELD_FAIL_THRESHOLD: '3', + SHIELD_HEALTH_URL: 'http://127.0.0.1:3000/api/health', + SHIELD_ORCH_URL: 'http://127.0.0.1:3000/api/orchestrator/notify', + SHIELD_ROLLBACK_CMD: 'bash scripts/rollback-last-backup.sh', + SHIELD_REPAIR_CMD: `pm2 startOrRestart "${ECOSYSTEM_PATH}" --only unicorn,unicorn-orchestrator,unicorn-health-guardian,unicorn-quantum-watchdog`, + }, + error_file: 'logs/shield-error.log', + out_file: 'logs/shield-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss' + }, + + // ── 7. Health Daemon (checkBackend / checkFrontend / checkSSL / checkNginx / checkResources) ─ + { + name: 'unicorn-health-daemon', + script: 'scripts/unicorn-health-daemon.js', + cwd: __dirname, + instances: 1, + autorestart: true, + watch: false, + max_restarts: 15, + min_uptime: '10s', + restart_delay: 5000, + env: { + NODE_ENV: 'production', + HEALTH_DAEMON_INTERVAL_MS: '60000', + HEALTH_BACKEND_URL: 'http://127.0.0.1:3000/api/health', + HEALTH_FRONTEND_URL: PUBLIC_APP_URL, + SITE_DOMAIN, + PUBLIC_APP_URL, + HEALTH_REPORT_URL: 'http://127.0.0.1:3000/api/health-daemon/report', + HEALTH_ORCH_URL: 'http://127.0.0.1:3000/api/orchestrator/notify', + HEALTH_SSL_WARN_DAYS: '14', + HEALTH_CPU_WARN: '85', + HEALTH_RAM_WARN: '90', + HEALTH_DISK_WARN: '85', + }, + error_file: 'logs/health-daemon-error.log', + out_file: 'logs/health-daemon-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss' + }, + + // ── 8. Auto-Innovation Agent (analyzeCodebase / proposeImprovements / createPR / innovationLoop) ─ + { + name: 'unicorn-innovation-agent', + script: 'backend/modules/auto-innovation-loop.js', + cwd: __dirname, + instances: 1, + autorestart: true, + watch: false, + max_restarts: 10, + min_uptime: '15s', + restart_delay: 15000, + exp_backoff_restart_delay: 5000, + env: { + NODE_ENV: 'production', + INNOV_CYCLE_MS: '3600000', + INNOV_PR_POLL_MS: '300000', + INNOV_MAX_PENDING: '3', + INNOV_BASE_BRANCH: 'main', + DOMAIN: SITE_DOMAIN, + PUBLIC_APP_URL, + GITHUB_REPO_OWNER: GH_OWNER, + GITHUB_REPO_NAME: GH_REPO, + }, + error_file: 'logs/innovation-agent-error.log', + out_file: 'logs/innovation-agent-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss' + }, + + // ── 9. Universal AI Connector — UAIC (model discovery & routing daemon) ── // Runs independently to keep the AI model registry fresh. // The backend also loads UAIC inline; this process handles cron discovery. { @@ -170,7 +285,7 @@ module.exports = { log_date_format: 'YYYY-MM-DD HH:mm:ss' }, - // ── 6. Llama Bridge (starts Ollama serve at P4 / nice +10) ──────────────── + // ── 10. Llama Bridge (starts Ollama serve at P4 / nice +10) ─────────────── // Prerequisite: `ollama` must be installed on the server. // Install: curl -fsSL https://ollama.ai/install.sh | sh // Pull model: ollama pull llama3.1:8b-instruct-q4_K_M diff --git a/UNICORN_FINAL/scripts/unicorn-health-daemon.js b/UNICORN_FINAL/scripts/unicorn-health-daemon.js new file mode 100644 index 00000000..d3d7d5a6 --- /dev/null +++ b/UNICORN_FINAL/scripts/unicorn-health-daemon.js @@ -0,0 +1,391 @@ +#!/usr/bin/env node +// ===================================================================== +// OWNERSHIP: Acest fișier este proprietatea exclusivă a lui Vladoi Ionut +// Email: vladoi_ionut@yahoo.com +// BTC Address: bc1q4f7e66z87mdfj56kz0dj5hvcnpmh0qh4wuv22e +// ===================================================================== + +/** + * UNICORN HEALTH-CHECK DAEMON — MONITORIZARE CONTINUĂ + * + * Verifică că TOTUL funcționează: + * 1. checkBackend() — /api/health, latență, erori + * 2. checkFrontend() — https://zeusai.pro, resurse, rute + * 3. checkSSL() — validitate certificat, reînnoire automată + * 4. checkNginx() — config, procese, loguri + * 5. checkResources() — CPU, RAM, spațiu, load + * 6. report() — trimite status către orchestrator și shield + * 7. healthLoop() — rulează non-stop, detectează probleme devreme + */ + +'use strict'; + +const http = require('http'); +const https = require('https'); +const os = require('os'); +const fs = require('fs'); +const tls = require('tls'); +const { exec } = require('child_process'); + +// ─── Config ────────────────────────────────────────────────────────────────── +const HEALTH_INTERVAL = parseInt(process.env.HEALTH_DAEMON_INTERVAL_MS || '60000', 10); // 1 min +const BACKEND_URL = process.env.HEALTH_BACKEND_URL || 'http://127.0.0.1:3000/api/health'; +const FRONTEND_URL = process.env.HEALTH_FRONTEND_URL || process.env.PUBLIC_APP_URL || 'https://zeusai.pro'; +const SITE_DOMAIN = process.env.SITE_DOMAIN || 'zeusai.pro'; +const REPORT_URL = process.env.HEALTH_REPORT_URL || 'http://127.0.0.1:3000/api/health-daemon/report'; +const ORCH_URL = process.env.HEALTH_ORCH_URL || 'http://127.0.0.1:3000/api/orchestrator/notify'; +const SSL_WARN_DAYS = parseInt(process.env.HEALTH_SSL_WARN_DAYS || '14', 10); +const CPU_WARN = parseFloat(process.env.HEALTH_CPU_WARN || '85'); +const RAM_WARN = parseFloat(process.env.HEALTH_RAM_WARN || '90'); +const DISK_WARN = parseFloat(process.env.HEALTH_DISK_WARN || '85'); +const MAX_LOG = 300; +const MAX_REPORTS = 100; + +// ─── State ─────────────────────────────────────────────────────────────────── +const state = { + startedAt: Date.now(), + cycles: 0, + reports: [], + log: [], + last: { + backend: null, + frontend: null, + ssl: null, + nginx: null, + resources: null, + }, +}; + +// ─── Helpers ────────────────────────────────────────────────────────────────── +function ts() { return new Date().toISOString(); } + +function log(level, msg, extra) { + const entry = { ts: ts(), level, msg, ...(extra ? { extra } : {}) }; + state.log.push(entry); + if (state.log.length > MAX_LOG) state.log.shift(); + const icon = level === 'ERROR' ? '❌' : level === 'WARN' ? '⚠️ ' : level === 'OK' ? '✅' : 'ℹ️ '; + if (extra) console.log(`[HealthDaemon] ${entry.ts} ${icon} ${msg}`, extra); + else console.log(`[HealthDaemon] ${entry.ts} ${icon} ${msg}`); +} + +function httpRequest(url, { method = 'GET', timeout = 10000, body = null, headers = {} } = {}) { + return new Promise((resolve) => { + const start = Date.now(); + const lib = url.startsWith('https') ? https : http; + const opts = { method, headers, timeout }; + if (body) opts.headers['Content-Length'] = Buffer.byteLength(body); + const req = lib.request(url, opts, (res) => { + let data = ''; + res.on('data', c => { data += c; }); + res.on('end', () => { + const latencyMs = Date.now() - start; + try { resolve({ ok: res.statusCode < 400, status: res.statusCode, latencyMs, body: JSON.parse(data) }); } + catch { resolve({ ok: res.statusCode < 400, status: res.statusCode, latencyMs, body: data.slice(0, 200) }); } + }); + }); + req.on('error', err => resolve({ ok: false, error: err.message, latencyMs: Date.now() - start })); + req.setTimeout(timeout, () => { req.destroy(); resolve({ ok: false, error: 'timeout', latencyMs: timeout }); }); + if (body) req.write(body); + req.end(); + }); +} + +function run(cmd, timeoutMs = 10000) { + return new Promise((resolve) => { + exec(cmd, { timeout: timeoutMs }, (err, stdout, stderr) => { + resolve({ ok: !err, stdout: (stdout || '').trim(), stderr: (stderr || '').trim() }); + }); + }); +} + +// ─── 1. checkBackend() ──────────────────────────────────────────────────────── +async function checkBackend() { + const res = await httpRequest(BACKEND_URL, { timeout: 8000 }); + const result = { + ok: res.ok, + status: res.status, + latencyMs: res.latencyMs, + body: res.body, + error: res.error, + ts: ts(), + }; + + state.last.backend = result; + + if (!result.ok) { + log('ERROR', `checkBackend — EȘEC (${result.status || result.error}, ${result.latencyMs}ms)`); + } else if (result.latencyMs > 3000) { + log('WARN', `checkBackend — latență ridicată: ${result.latencyMs}ms`); + } else { + log('OK', `checkBackend — OK (${result.status}, ${result.latencyMs}ms)`); + } + + return result; +} + +// ─── 2. checkFrontend() ─────────────────────────────────────────────────────── +async function checkFrontend() { + const result = { ok: false, checks: {}, ts: ts() }; + + // Check main page + const main = await httpRequest(FRONTEND_URL, { timeout: 15000 }); + result.checks.mainPage = { ok: main.ok, status: main.status, latencyMs: main.latencyMs }; + + // Check /health endpoint (Vercel / edge) + const healthUrl = FRONTEND_URL.replace(/\/$/, '') + '/health'; + const healthCheck = await httpRequest(healthUrl, { timeout: 10000 }); + result.checks.healthEndpoint = { ok: healthCheck.ok, status: healthCheck.status, latencyMs: healthCheck.latencyMs }; + + result.ok = main.ok; + + state.last.frontend = result; + + if (!result.ok) { + log('ERROR', `checkFrontend — ${FRONTEND_URL} inaccesibil (${main.status || main.error})`); + } else { + log('OK', `checkFrontend — OK (${main.status}, ${main.latencyMs}ms)`); + } + + return result; +} + +// ─── 3. checkSSL() ──────────────────────────────────────────────────────────── +async function checkSSL() { + const result = { ok: false, domain: SITE_DOMAIN, daysRemaining: null, ts: ts() }; + + // Skip for non-https or missing domain + if (!SITE_DOMAIN || SITE_DOMAIN === 'localhost') { + result.ok = true; + result.note = 'local env — SSL check skipped'; + log('INFO', 'checkSSL — skip (env local)'); + state.last.ssl = result; + return result; + } + + return new Promise((resolve) => { + try { + const socket = tls.connect({ host: SITE_DOMAIN, port: 443, servername: SITE_DOMAIN }, () => { + try { + const cert = socket.getPeerCertificate(); + if (!cert || !cert.valid_to) { + result.ok = false; + result.error = 'no_cert'; + } else { + const expiry = new Date(cert.valid_to); + const daysRemaining = Math.floor((expiry - Date.now()) / 86400000); + result.daysRemaining = daysRemaining; + result.expiry = cert.valid_to; + result.subject = cert.subject && cert.subject.CN; + result.ok = daysRemaining > 0; + + if (daysRemaining <= 0) { + log('ERROR', `checkSSL — certificat EXPIRAT pentru ${SITE_DOMAIN}`); + } else if (daysRemaining < SSL_WARN_DAYS) { + log('WARN', `checkSSL — certificat expiră în ${daysRemaining} zile pentru ${SITE_DOMAIN}`); + } else { + log('OK', `checkSSL — OK, expiră în ${daysRemaining} zile (${cert.valid_to})`); + } + } + } catch (e) { + result.error = e.message; + } + socket.destroy(); + state.last.ssl = result; + resolve(result); + }); + socket.setTimeout(10000, () => { + socket.destroy(); + result.ok = false; + result.error = 'timeout'; + log('WARN', `checkSSL — timeout la conectare ${SITE_DOMAIN}:443`); + state.last.ssl = result; + resolve(result); + }); + socket.on('error', (err) => { + result.ok = false; + result.error = err.message; + log('WARN', `checkSSL — eroare TLS: ${err.message}`); + state.last.ssl = result; + resolve(result); + }); + } catch (e) { + result.ok = false; + result.error = e.message; + state.last.ssl = result; + resolve(result); + } + }); +} + +// ─── 4. checkNginx() ────────────────────────────────────────────────────────── +async function checkNginx() { + const result = { ok: false, ts: ts() }; + + // Check nginx process + const procRes = await run('pgrep -x nginx > /dev/null 2>&1 && echo running || echo stopped', 5000); + result.process = procRes.stdout.includes('running') ? 'running' : 'stopped'; + + // Test nginx config (if nginx is available) + const testRes = await run('nginx -t 2>&1', 5000); + result.configTest = testRes.ok || testRes.stderr.includes('test is successful') || testRes.stdout.includes('test is successful'); + result.configOutput = (testRes.stdout + testRes.stderr).slice(0, 300); + + // nginx is optional on dev/CI, only warn if process not found + result.ok = result.process === 'running' ? result.configTest : true; + + state.last.nginx = result; + + if (result.process === 'stopped') { + log('WARN', 'checkNginx — procesul nginx nu rulează (poate fi normal în dev/CI)'); + } else if (!result.configTest) { + log('ERROR', 'checkNginx — configurație Nginx invalidă!', { output: result.configOutput }); + } else { + log('OK', `checkNginx — OK (process: ${result.process}, config: valid)`); + } + + return result; +} + +// ─── 5. checkResources() ────────────────────────────────────────────────────── +async function checkResources() { + const result = { ok: true, ts: ts() }; + const warnings = []; + + // CPU load (1min average / number of CPUs) + const cpuCount = os.cpus().length || 1; + const loadavg = os.loadavg(); + const cpuLoad = (loadavg[0] / cpuCount) * 100; + result.cpu = { loadavg: loadavg[0].toFixed(2), percentUsed: cpuLoad.toFixed(1), cores: cpuCount }; + if (cpuLoad > CPU_WARN) warnings.push(`CPU: ${cpuLoad.toFixed(0)}% (prag: ${CPU_WARN}%)`); + + // RAM + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + const usedMem = totalMem - freeMem; + const ramPct = (usedMem / totalMem) * 100; + result.ram = { + totalMB: (totalMem / 1048576).toFixed(0), + usedMB: (usedMem / 1048576).toFixed(0), + freeMB: (freeMem / 1048576).toFixed(0), + percentUsed: ramPct.toFixed(1), + }; + if (ramPct > RAM_WARN) warnings.push(`RAM: ${ramPct.toFixed(0)}% (prag: ${RAM_WARN}%)`); + + // Disk (df -h /) + const diskRes = await run("df -k / 2>/dev/null | awk 'NR==2{print $3,$4,$5}'", 5000); + if (diskRes.ok && diskRes.stdout) { + const parts = diskRes.stdout.trim().split(/\s+/); + const used = parseInt(parts[0], 10) * 1024; + const avail = parseInt(parts[1], 10) * 1024; + const pct = parseInt((parts[2] || '0').replace('%', ''), 10); + result.disk = { + usedGB: (used / 1073741824).toFixed(1), + availableGB: (avail / 1073741824).toFixed(1), + percentUsed: pct, + }; + if (pct > DISK_WARN) warnings.push(`Disk: ${pct}% (prag: ${DISK_WARN}%)`); + } else { + result.disk = { note: 'unavailable' }; + } + + // Uptime + result.uptimeHours = (os.uptime() / 3600).toFixed(1); + + result.warnings = warnings; + result.ok = warnings.length === 0; + state.last.resources = result; + + if (warnings.length > 0) { + log('WARN', `checkResources — ${warnings.length} avertisment(e)`, { warnings }); + } else { + log('OK', `checkResources — OK (CPU: ${cpuLoad.toFixed(0)}%, RAM: ${ramPct.toFixed(0)}%, Disk: ${result.disk.percentUsed || '?'}%)`); + } + + return result; +} + +// ─── 6. report() ────────────────────────────────────────────────────────────── +async function report(status) { + const payload = { + ts: ts(), + source: 'health-daemon', + uptime: Date.now() - state.startedAt, + cycles: state.cycles, + ...status, + }; + + state.reports.push(payload); + if (state.reports.length > MAX_REPORTS) state.reports.shift(); + + // Send to backend health-daemon endpoint and orchestrator (fire-and-forget) + for (const url of [REPORT_URL, ORCH_URL]) { + try { + const body = JSON.stringify({ event: 'health_report', payload }); + const req = http.request(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, + timeout: 3000, + }); + req.on('error', () => {}); + req.write(body); + req.end(); + } catch { /* ignore — endpoints may not exist yet */ } + } +} + +// ─── 7. healthLoop() — buclă principală ─────────────────────────────────────── +async function healthLoop() { + state.cycles++; + + try { + const [backend, resources, nginx] = await Promise.all([ + checkBackend(), + checkResources(), + checkNginx(), + ]); + + // Frontend and SSL checks are slower/external — run every 5 cycles to reduce noise + let frontend = state.last.frontend; + let ssl = state.last.ssl; + + if (state.cycles % 5 === 1) { + [frontend, ssl] = await Promise.all([checkFrontend(), checkSSL()]); + } + + const allOk = backend.ok && resources.ok; + + const summary = { + overall: allOk ? 'healthy' : 'degraded', + backend: { ok: backend.ok, latencyMs: backend.latencyMs }, + resources: { ok: resources.ok, cpu: resources.cpu, ram: resources.ram }, + nginx: { ok: nginx.ok, process: nginx.process }, + frontend: frontend ? { ok: frontend.ok } : null, + ssl: ssl ? { ok: ssl.ok, daysRemaining: ssl.daysRemaining } : null, + }; + + if (state.cycles % 5 === 0) { + log('INFO', `healthLoop — ciclu #${state.cycles}: ${summary.overall}`); + } + + await report(summary); + + } catch (err) { + log('ERROR', `healthLoop — eroare în ciclu #${state.cycles}`, { error: err.message }); + } +} + +// ─── Startup ────────────────────────────────────────────────────────────────── +function main() { + log('INFO', '💓 Unicorn Health Daemon pornit'); + + // Stagger first check + setTimeout(() => healthLoop().catch(e => log('ERROR', 'initial healthLoop error', { error: e.message })), 8000); + + // Main health loop + setInterval(() => healthLoop().catch(e => log('ERROR', 'healthLoop error', { error: e.message })), HEALTH_INTERVAL); + + process.on('uncaughtException', err => log('ERROR', 'uncaughtException', { error: err.message })); + process.on('unhandledRejection', err => log('ERROR', 'unhandledRejection', { error: String(err) })); +} + +main(); diff --git a/UNICORN_FINAL/scripts/unicorn-main-orchestrator.js b/UNICORN_FINAL/scripts/unicorn-main-orchestrator.js new file mode 100644 index 00000000..0cc2ea18 --- /dev/null +++ b/UNICORN_FINAL/scripts/unicorn-main-orchestrator.js @@ -0,0 +1,328 @@ +#!/usr/bin/env node +// ===================================================================== +// OWNERSHIP: Acest fișier este proprietatea exclusivă a lui Vladoi Ionut +// Email: vladoi_ionut@yahoo.com +// BTC Address: bc1q4f7e66z87mdfj56kz0dj5hvcnpmh0qh4wuv22e +// ===================================================================== + +/** + * UNICORN MAIN ORCHESTRATOR + * + * Controlează TOT fluxul Unicorn: + * 1. checkRepo() — verifică branch main, detectează commit-uri noi / conflicte + * 2. validateCode() — rulează lint + teste + * 3. deploy() — execută deploy.sh pe Hetzner, reîncarcă Nginx, repornește PM2 + * 4. rollback() — revine la commit-ul anterior, reconstruiește, validează + * 5. notify() — trimite rapoarte către shield și agentul de inovare + * 6. orchestrate() — buclă continuă de decizie + */ + +'use strict'; + +const { exec } = require('child_process'); +const path = require('path'); +const http = require('http'); +const https = require('https'); + +// ─── Config ────────────────────────────────────────────────────────────────── +const ROOT = path.join(__dirname, '..'); +const POLL_MS = parseInt(process.env.ORCH_POLL_MS || '120000', 10); // 2 min +const SHIELD_URL = process.env.ORCH_SHIELD_URL || 'http://127.0.0.1:3000/api/quantum-integrity/status'; +const HEALTH_URL = process.env.ORCH_HEALTH_URL || 'http://127.0.0.1:3000/api/health'; +const INNOVATION_URL = process.env.ORCH_INNOVATION_URL || 'http://127.0.0.1:3000/api/innovation-loop/status'; +const DEPLOY_CMD = process.env.ORCH_DEPLOY_CMD || 'bash scripts/deploy-hetzner.js 2>&1 || bash deploy.sh 2>&1'; +const ROLLBACK_CMD = process.env.ORCH_ROLLBACK_CMD || 'bash scripts/rollback-last-backup.sh'; +const LINT_CMD = process.env.ORCH_LINT_CMD || 'npm run lint --if-present'; +const TEST_CMD = process.env.ORCH_TEST_CMD || 'npm test --if-present'; +const MAX_LOG = 300; + +// ─── State ─────────────────────────────────────────────────────────────────── +const state = { + startedAt: Date.now(), + cycles: 0, + lastCommitSha: null, + deployCount: 0, + rollbackCount: 0, + consecutiveErrors: 0, + log: [], +}; + +// ─── Helpers ────────────────────────────────────────────────────────────────── +function ts() { return new Date().toISOString(); } + +function log(level, msg, extra) { + const entry = { ts: ts(), level, msg, ...(extra ? { extra } : {}) }; + state.log.push(entry); + if (state.log.length > MAX_LOG) state.log.shift(); + const prefix = level === 'ERROR' ? '❌' : level === 'WARN' ? '⚠️ ' : level === 'OK' ? '✅' : 'ℹ️ '; + if (extra) console.log(`[MainOrch] ${entry.ts} ${prefix} ${msg}`, extra); + else console.log(`[MainOrch] ${entry.ts} ${prefix} ${msg}`); +} + +function run(cmd, opts = {}) { + return new Promise((resolve) => { + exec(cmd, { cwd: ROOT, timeout: 120000, ...opts }, (err, stdout, stderr) => { + resolve({ + ok: !err, + code: err ? err.code : 0, + stdout: (stdout || '').trim().slice(0, 2000), + stderr: (stderr || '').trim().slice(0, 1000), + }); + }); + }); +} + +function httpGet(url, timeoutMs = 8000) { + return new Promise((resolve) => { + const lib = url.startsWith('https') ? https : http; + const req = lib.get(url, (res) => { + let body = ''; + res.on('data', c => { body += c; }); + res.on('end', () => { + try { resolve({ ok: res.statusCode < 400, status: res.statusCode, body: JSON.parse(body) }); } + catch { resolve({ ok: res.statusCode < 400, status: res.statusCode, body }); } + }); + }); + req.on('error', err => resolve({ ok: false, error: err.message })); + req.setTimeout(timeoutMs, () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); }); + }); +} + +// ─── 1. checkRepo() ─────────────────────────────────────────────────────────── +async function checkRepo() { + log('INFO', 'checkRepo — verifică branch main și commit-uri noi'); + + // Fetch latest from origin + const fetch = await run('git fetch origin main --quiet 2>&1'); + + // Get current HEAD sha + const headRes = await run('git rev-parse HEAD'); + const localSha = headRes.stdout; + + // Get remote sha + const remoteRes = await run('git rev-parse origin/main'); + const remoteSha = remoteRes.stdout; + + const newCommits = localSha !== remoteSha; + + // Detect conflicts (unmerged paths) + const conflictRes = await run('git diff --name-only --diff-filter=U 2>&1'); + const hasConflicts = conflictRes.stdout.length > 0; + + // Check for missing critical files + const criticalFiles = [ + 'backend/index.js', + 'src/index.js', + 'package.json', + 'ecosystem.config.js', + ]; + const missingFiles = criticalFiles.filter(f => { + const fs = require('fs'); + return !fs.existsSync(path.join(ROOT, f)); + }); + + // Recent commits summary + const logRes = await run('git log --oneline -5 origin/main 2>/dev/null || git log --oneline -5'); + + const result = { + localSha, + remoteSha, + newCommits, + hasConflicts, + missingFiles, + recentLog: logRes.stdout, + fetchOk: fetch.ok, + }; + + if (newCommits) log('INFO', `checkRepo — commit nou detectat: ${remoteSha.slice(0, 8)}`, { localSha: localSha.slice(0, 8), remoteSha: remoteSha.slice(0, 8) }); + if (hasConflicts) log('WARN', 'checkRepo — conflicte detectate!', { files: conflictRes.stdout }); + if (missingFiles.length) log('WARN', 'checkRepo — fișiere lipsă!', { missingFiles }); + if (!newCommits && !hasConflicts && !missingFiles.length) log('OK', 'checkRepo — repo intact, fără schimbări'); + + // Track last known commit + if (!newCommits) state.lastCommitSha = localSha; + else state.lastCommitSha = remoteSha; + + return result; +} + +// ─── 2. validateCode() ──────────────────────────────────────────────────────── +async function validateCode() { + log('INFO', 'validateCode — rulează lint + teste'); + + const lintRes = await run(LINT_CMD, { timeout: 60000 }); + const testRes = await run(TEST_CMD, { timeout: 90000 }); + + const ok = lintRes.ok && testRes.ok; + + if (!lintRes.ok) log('WARN', 'validateCode — lint a eșuat', { stderr: lintRes.stderr.slice(0, 400) }); + if (!testRes.ok) log('WARN', 'validateCode — teste au eșuat', { stderr: testRes.stderr.slice(0, 400) }); + if (ok) log('OK', 'validateCode — lint + teste OK'); + + return { ok, lint: { ok: lintRes.ok, output: lintRes.stdout }, tests: { ok: testRes.ok, output: testRes.stdout } }; +} + +// ─── 3. deploy() ────────────────────────────────────────────────────────────── +async function deploy() { + state.deployCount++; + log('INFO', `deploy #${state.deployCount} — execută deploy pe Hetzner`); + + // Pull latest code first + const pullRes = await run('git pull origin main --rebase 2>&1 || git pull origin main 2>&1'); + if (!pullRes.ok) { + log('WARN', 'deploy — git pull a eșuat (continuăm cu codul local)', { err: pullRes.stderr.slice(0, 300) }); + } + + // Run deploy command + const deployRes = await run(DEPLOY_CMD, { timeout: 300000 }); + + if (!deployRes.ok) { + log('ERROR', 'deploy — deploy.sh a eșuat', { stderr: deployRes.stderr.slice(0, 500) }); + return { ok: false, reason: 'deploy_failed', output: deployRes.stderr }; + } + + // Validate after deploy + await new Promise(r => setTimeout(r, 5000)); + const health = await httpGet(HEALTH_URL); + if (!health.ok) { + log('ERROR', 'deploy — health check a eșuat după deploy; declanșăm rollback'); + return { ok: false, reason: 'post_deploy_health_fail', health }; + } + + log('OK', `deploy #${state.deployCount} — deploy reușit, backend healthy`); + return { ok: true, deployOutput: deployRes.stdout.slice(0, 500) }; +} + +// ─── 4. rollback() ──────────────────────────────────────────────────────────── +async function rollback(reason = 'manual') { + state.rollbackCount++; + log('WARN', `rollback #${state.rollbackCount} — motiv: ${reason}`); + + const rollRes = await run(ROLLBACK_CMD, { timeout: 300000 }); + + if (!rollRes.ok) { + // Fallback: revert one commit and restart PM2 + log('WARN', 'rollback — script de rollback a eșuat, încearcă git revert HEAD~1'); + await run('git revert HEAD~1 --no-edit 2>&1 || git reset --hard HEAD~1 2>&1'); + await run('npm install --production 2>&1'); + await run(`pm2 startOrRestart "${path.join(ROOT, 'ecosystem.config.js')}" --only unicorn,unicorn-orchestrator 2>&1`); + } + + await new Promise(r => setTimeout(r, 8000)); + const health = await httpGet(HEALTH_URL); + const stable = health.ok; + + if (stable) log('OK', 'rollback — sistem stabil după rollback'); + else log('ERROR', 'rollback — sistemul NU este stabil nici după rollback!'); + + return { ok: stable, rollbackOutput: rollRes.stdout.slice(0, 300) }; +} + +// ─── 5. notify() ────────────────────────────────────────────────────────────── +async function notify(event, payload = {}) { + const message = { ts: ts(), event, ...payload }; + + // Notify via backend event endpoint (fire-and-forget) + const targets = [ + `http://127.0.0.1:3000/api/orchestrator/notify`, + `http://127.0.0.1:3000/api/quantum-integrity/notify`, + ]; + + for (const url of targets) { + try { + const body = JSON.stringify(message); + const req = http.request(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, + timeout: 3000, + }); + req.on('error', () => {}); // fire-and-forget + req.write(body); + req.end(); + } catch { /* ignore */ } + } + + log('INFO', `notify — eveniment trimis: ${event}`); +} + +// ─── 6. orchestrate() — buclă principală ───────────────────────────────────── +async function orchestrate() { + state.cycles++; + log('INFO', `orchestrate — ciclu #${state.cycles}`); + + try { + // Step 1: Check repo + const repoStatus = await checkRepo(); + + // Step 2: If there are new commits, validate and deploy + if (repoStatus.newCommits) { + log('INFO', 'orchestrate — commit nou detectat, validăm codul'); + const validation = await validateCode(); + + if (validation.ok) { + log('INFO', 'orchestrate — validare OK, declanșăm deploy'); + const deployResult = await deploy(); + + if (!deployResult.ok) { + log('ERROR', 'orchestrate — deploy eșuat, declanșăm rollback'); + await notify('deploy_failed', { reason: deployResult.reason }); + const rb = await rollback('deploy_failed'); + await notify('rollback_done', { stable: rb.ok }); + } else { + await notify('deploy_success', { cycle: state.cycles }); + } + } else { + log('WARN', 'orchestrate — validare eșuată, NU facem deploy'); + await notify('validation_failed', { lint: validation.lint.ok, tests: validation.tests.ok }); + } + } + + // Step 3: Check for conflicts or missing files — repair via shield notification + if (repoStatus.hasConflicts || repoStatus.missingFiles.length > 0) { + await notify('repo_issues', { + conflicts: repoStatus.hasConflicts, + missingFiles: repoStatus.missingFiles, + }); + } + + // Step 4: Health ping + const health = await httpGet(HEALTH_URL); + if (!health.ok) { + state.consecutiveErrors++; + log('ERROR', `orchestrate — backend unhealthy (${state.consecutiveErrors} consecutive)`); + if (state.consecutiveErrors >= 3) { + log('ERROR', 'orchestrate — 3 eșecuri consecutive, declanșăm rollback de urgență'); + await rollback('consecutive_health_failure'); + state.consecutiveErrors = 0; + } + } else { + if (state.consecutiveErrors > 0) { + log('OK', `orchestrate — backend recuperat după ${state.consecutiveErrors} eșec(uri)`); + } + state.consecutiveErrors = 0; + } + + log('OK', `orchestrate — ciclu #${state.cycles} finalizat`); + + } catch (err) { + log('ERROR', `orchestrate — eroare neașteptată în ciclu #${state.cycles}`, { error: err.message }); + await notify('orchestrator_error', { error: err.message, cycle: state.cycles }); + } +} + +// ─── Startup ────────────────────────────────────────────────────────────────── +async function main() { + log('INFO', '🎼 Unicorn Main Orchestrator pornit'); + + // Initial cycle after short delay + setTimeout(() => orchestrate().catch(e => log('ERROR', 'initial orchestrate failed', { error: e.message })), 10000); + + // Recurring orchestration loop + setInterval(() => orchestrate().catch(e => log('ERROR', 'orchestrate loop error', { error: e.message })), POLL_MS); + + // Keep process alive + process.on('uncaughtException', err => log('ERROR', 'uncaughtException', { error: err.message })); + process.on('unhandledRejection', err => log('ERROR', 'unhandledRejection', { error: String(err) })); +} + +main(); diff --git a/UNICORN_FINAL/scripts/unicorn-shield.js b/UNICORN_FINAL/scripts/unicorn-shield.js new file mode 100644 index 00000000..3022c1af --- /dev/null +++ b/UNICORN_FINAL/scripts/unicorn-shield.js @@ -0,0 +1,356 @@ +#!/usr/bin/env node +// ===================================================================== +// OWNERSHIP: Acest fișier este proprietatea exclusivă a lui Vladoi Ionut +// Email: vladoi_ionut@yahoo.com +// BTC Address: bc1q4f7e66z87mdfj56kz0dj5hvcnpmh0qh4wuv22e +// ===================================================================== + +/** + * UNICORN SHIELD — WATCHDOG + INTEGRITATE + * + * Protejează Unicorn: + * 1. watchFiles() — monitorizează backend/, frontend/ (client/), Nginx config + * 2. watchProcesses() — monitorizează PM2 + backend API + * 3. autoRepair() — reface fișiere lipsă din repo, repornește servicii + * 4. emergencyRollback() — declanșează rollback prin orchestrator + * 5. shieldLoop() — buclă continuă de veghe + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const http = require('http'); +const { exec } = require('child_process'); + +// ─── Config ────────────────────────────────────────────────────────────────── +const ROOT = path.join(__dirname, '..'); +const SHIELD_INTERVAL = parseInt(process.env.SHIELD_INTERVAL_MS || '30000', 10); // 30s +const PROCESS_INTERVAL = parseInt(process.env.SHIELD_PROC_MS || '20000', 10); // 20s +const FAIL_THRESHOLD = parseInt(process.env.SHIELD_FAIL_THRESHOLD || '3', 10); +const HEALTH_URL = process.env.SHIELD_HEALTH_URL || 'http://127.0.0.1:3000/api/health'; +const ORCH_URL = process.env.SHIELD_ORCH_URL || 'http://127.0.0.1:3000/api/orchestrator/notify'; +const ROLLBACK_CMD = process.env.SHIELD_ROLLBACK_CMD || 'bash scripts/rollback-last-backup.sh'; +const REPAIR_CMD = process.env.SHIELD_REPAIR_CMD || 'pm2 startOrRestart ecosystem.config.js --only unicorn,unicorn-orchestrator,unicorn-health-guardian,unicorn-quantum-watchdog'; +const MAX_LOG = 400; + +// Critical files that must exist at all times +const CRITICAL_FILES = [ + 'backend/index.js', + 'src/index.js', + 'package.json', + 'ecosystem.config.js', + 'backend/modules/central-orchestrator.js', + 'backend/modules/auto-innovation-loop.js', +]; + +// Critical directories +const CRITICAL_DIRS = [ + 'backend', + 'backend/modules', + 'src', +]; + +// Required PM2 process names +const CRITICAL_PROCESSES = [ + 'unicorn', + 'unicorn-orchestrator', + 'unicorn-health-guardian', + 'unicorn-quantum-watchdog', +]; + +// ─── State ─────────────────────────────────────────────────────────────────── +const state = { + startedAt: Date.now(), + shieldCycles: 0, + repairCount: 0, + rollbackCount: 0, + consecutiveFailures: 0, + rollbackArmed: true, + anomalies: [], + log: [], + // Track file checksums / mtimes + fileSnapshots: {}, +}; + +// ─── Helpers ────────────────────────────────────────────────────────────────── +function ts() { return new Date().toISOString(); } + +function log(level, msg, extra) { + const entry = { ts: ts(), level, msg, ...(extra ? { extra } : {}) }; + state.log.push(entry); + if (state.log.length > MAX_LOG) state.log.shift(); + const icon = level === 'ERROR' ? '🚨' : level === 'WARN' ? '⚠️ ' : level === 'REPAIR' ? '🔧' : '🛡️ '; + if (extra) console.log(`[Shield] ${entry.ts} ${icon} ${msg}`, extra); + else console.log(`[Shield] ${entry.ts} ${icon} ${msg}`); +} + +function addAnomaly(type, detail) { + const a = { ts: ts(), type, detail }; + state.anomalies.push(a); + if (state.anomalies.length > 200) state.anomalies.shift(); + log('WARN', `Anomalie detectată: ${type}`, detail); +} + +function run(cmd, opts = {}) { + return new Promise((resolve) => { + exec(cmd, { cwd: ROOT, timeout: 120000, ...opts }, (err, stdout, stderr) => { + resolve({ + ok: !err, + stdout: (stdout || '').trim().slice(0, 2000), + stderr: (stderr || '').trim().slice(0, 1000), + }); + }); + }); +} + +function httpGet(url, timeoutMs = 6000) { + return new Promise((resolve) => { + const req = http.get(url, (res) => { + let body = ''; + res.on('data', c => { body += c; }); + res.on('end', () => { + try { resolve({ ok: res.statusCode < 400, status: res.statusCode, body: JSON.parse(body) }); } + catch { resolve({ ok: res.statusCode < 400, status: res.statusCode, body }); } + }); + }); + req.on('error', err => resolve({ ok: false, error: err.message })); + req.setTimeout(timeoutMs, () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); }); + }); +} + +// ─── 1. watchFiles() ───────────────────────────────────────────────────────── +function watchFiles() { + const issues = []; + + // Check critical files exist + for (const rel of CRITICAL_FILES) { + const full = path.join(ROOT, rel); + if (!fs.existsSync(full)) { + issues.push({ type: 'missing_file', file: rel }); + addAnomaly('missing_file', { file: rel }); + continue; + } + // Check for suspicious zero-byte files + try { + const stat = fs.statSync(full); + if (stat.size === 0) { + issues.push({ type: 'empty_file', file: rel }); + addAnomaly('empty_file', { file: rel, size: stat.size }); + } + } catch (e) { + issues.push({ type: 'stat_error', file: rel, error: e.message }); + } + } + + // Check critical directories exist + for (const rel of CRITICAL_DIRS) { + const full = path.join(ROOT, rel); + if (!fs.existsSync(full)) { + issues.push({ type: 'missing_dir', dir: rel }); + addAnomaly('missing_dir', { dir: rel }); + } + } + + // Check Nginx config if present + const nginxConf = path.join(ROOT, 'scripts', 'nginx-unicorn.conf'); + if (!fs.existsSync(nginxConf)) { + // Not critical if not on a server + log('INFO', 'watchFiles — nginx config nu există local (normal în CI)'); + } + + if (issues.length === 0) { + log('INFO', `watchFiles — toate fișierele critice (${CRITICAL_FILES.length}) intacte`); + } + + return issues; +} + +// ─── 2. watchProcesses() ───────────────────────────────────────────────────── +async function watchProcesses() { + const issues = []; + + // Check backend API health + const health = await httpGet(HEALTH_URL); + if (!health.ok) { + issues.push({ type: 'backend_down', url: HEALTH_URL, reason: health.error || health.status }); + addAnomaly('backend_down', { url: HEALTH_URL }); + } + + // Check PM2 process list + const pm2Res = await run('pm2 jlist 2>/dev/null || echo "[]"'); + let pm2List = []; + try { + pm2List = JSON.parse(pm2Res.stdout || '[]'); + } catch { + // pm2 not available (CI/dev environment) + log('INFO', 'watchProcesses — pm2 nu e disponibil (mediu CI/dev, OK)'); + return issues; + } + + const runningNames = pm2List + .filter(p => p.pm2_env && p.pm2_env.status === 'online') + .map(p => p.name); + + for (const proc of CRITICAL_PROCESSES) { + if (!runningNames.includes(proc)) { + // Only warn if pm2 has any processes at all (server env) + if (pm2List.length > 0) { + issues.push({ type: 'process_down', process: proc }); + addAnomaly('process_down', { process: proc, running: runningNames }); + } + } + } + + if (issues.length === 0) { + log('INFO', `watchProcesses — backend OK, procese PM2 active: ${runningNames.join(', ') || 'n/a'}`); + } + + return issues; +} + +// ─── 3. autoRepair() ───────────────────────────────────────────────────────── +async function autoRepair(fileIssues = [], processIssues = []) { + state.repairCount++; + log('REPAIR', `autoRepair #${state.repairCount} — încep repararea`); + + // Repair missing/corrupt files from git + for (const issue of fileIssues) { + if (issue.type === 'missing_file' || issue.type === 'empty_file') { + log('REPAIR', `autoRepair — refac fișierul ${issue.file} din repo`); + const res = await run(`git checkout -- "${issue.file}" 2>&1 || git checkout HEAD -- "${issue.file}" 2>&1`); + if (res.ok) log('REPAIR', `autoRepair — ${issue.file} refăcut cu succes`); + else log('ERROR', `autoRepair — nu am putut reface ${issue.file}`, { stderr: res.stderr.slice(0, 200) }); + } + if (issue.type === 'missing_dir') { + log('REPAIR', `autoRepair — reconstruiesc directorul ${issue.dir}`); + await run(`git checkout -- "${issue.dir}/" 2>&1 || true`); + } + } + + // Repair PM2 processes + for (const issue of processIssues) { + if (issue.type === 'process_down') { + log('REPAIR', `autoRepair — repornesc procesul PM2: ${issue.process}`); + const res = await run(`pm2 startOrRestart ecosystem.config.js --only ${issue.process} 2>&1`); + if (!res.ok) { + // Try full repair + log('WARN', `autoRepair — pm2 restart eșuat pentru ${issue.process}, încercăm reparare completă`); + await run(REPAIR_CMD); + } + } + if (issue.type === 'backend_down') { + log('REPAIR', 'autoRepair — repornim backend-ul'); + await run(REPAIR_CMD); + // Give it time to start + await new Promise(r => setTimeout(r, 5000)); + } + } + + log('REPAIR', `autoRepair #${state.repairCount} — finalizat`); +} + +// ─── 4. emergencyRollback() ─────────────────────────────────────────────────── +async function emergencyRollback(reason = 'shield_triggered') { + state.rollbackCount++; + state.rollbackArmed = false; + + log('ERROR', `emergencyRollback #${state.rollbackCount} — motiv: ${reason}`); + + // Notify orchestrator first + try { + const body = JSON.stringify({ event: 'emergency_rollback', reason, ts: ts() }); + const req = http.request(ORCH_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, + timeout: 3000, + }); + req.on('error', () => {}); + req.write(body); + req.end(); + } catch { /* ignore */ } + + // Execute rollback + const res = await run(ROLLBACK_CMD, { timeout: 300000 }); + if (!res.ok) { + log('ERROR', 'emergencyRollback — script de rollback a eșuat, încearcă git reset hard'); + await run('git reset --hard HEAD~1 2>&1 || true'); + await run(REPAIR_CMD); + } + + await new Promise(r => setTimeout(r, 8000)); + const health = await httpGet(HEALTH_URL); + if (health.ok) { + log('REPAIR', 'emergencyRollback — sistem stabil după rollback'); + state.consecutiveFailures = 0; + state.rollbackArmed = true; + } else { + log('ERROR', 'emergencyRollback — sistemul NU este stabil nici după rollback!'); + } +} + +// ─── 5. shieldLoop() — buclă principală ────────────────────────────────────── +async function shieldLoop() { + state.shieldCycles++; + + try { + // Watch files + const fileIssues = watchFiles(); + + // Watch processes + const processIssues = await watchProcesses(); + + const totalIssues = fileIssues.length + processIssues.length; + + if (totalIssues > 0) { + state.consecutiveFailures++; + log('WARN', `shieldLoop — ${totalIssues} problemă(e) detectate (consecutiv: ${state.consecutiveFailures})`); + + // Auto-repair + await autoRepair(fileIssues, processIssues); + + // Emergency rollback if too many consecutive failures + if (state.consecutiveFailures >= FAIL_THRESHOLD && state.rollbackArmed) { + log('ERROR', `shieldLoop — ${FAIL_THRESHOLD} eșecuri consecutive, declanșăm rollback de urgență`); + await emergencyRollback(`${FAIL_THRESHOLD}_consecutive_failures`); + } + } else { + if (state.consecutiveFailures > 0) { + log('INFO', `shieldLoop — ecosistemul recuperat după ${state.consecutiveFailures} eșec(uri)`); + } + state.consecutiveFailures = 0; + state.rollbackArmed = true; + if (state.shieldCycles % 10 === 0) { + log('INFO', `shieldLoop — ciclu #${state.shieldCycles}: ecosistem sănătos ✓`); + } + } + + } catch (err) { + log('ERROR', `shieldLoop — eroare neașteptată în ciclu #${state.shieldCycles}`, { error: err.message }); + } +} + +// ─── Startup ────────────────────────────────────────────────────────────────── +function main() { + log('INFO', '🛡️ Unicorn Shield pornit'); + + // File watching — periodic polling + setInterval(() => shieldLoop().catch(e => log('ERROR', 'shieldLoop error', { error: e.message })), SHIELD_INTERVAL); + + // Process watching — separate (faster) interval + setInterval(() => watchProcesses() + .then(issues => { + if (issues.length > 0) autoRepair([], issues).catch(() => {}); + }) + .catch(e => log('ERROR', 'process watch error', { error: e.message })), + PROCESS_INTERVAL + ); + + // Initial check after brief delay + setTimeout(() => shieldLoop().catch(e => log('ERROR', 'initial shieldLoop error', { error: e.message })), 5000); + + process.on('uncaughtException', err => log('ERROR', 'uncaughtException', { error: err.message })); + process.on('unhandledRejection', err => log('ERROR', 'unhandledRejection', { error: String(err) })); +} + +main(); From 79b790ffa194fea645aad0cf55058a4980640765 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:59:56 +0000 Subject: [PATCH 2/2] fix: correct PM2 process names and deploy command in orchestrator/shield/ecosystem Agent-Logs-Url: https://github.com/ruffy80/ZeusAI/sessions/1019f149-ab44-47c8-b914-678062505c41 Co-authored-by: ruffy80 <29306714+ruffy80@users.noreply.github.com> --- UNICORN_FINAL/ecosystem.config.js | 2 +- UNICORN_FINAL/scripts/unicorn-main-orchestrator.js | 4 ++-- UNICORN_FINAL/scripts/unicorn-shield.js | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/UNICORN_FINAL/ecosystem.config.js b/UNICORN_FINAL/ecosystem.config.js index 8d9f9c82..bad8d19e 100644 --- a/UNICORN_FINAL/ecosystem.config.js +++ b/UNICORN_FINAL/ecosystem.config.js @@ -198,7 +198,7 @@ module.exports = { SHIELD_HEALTH_URL: 'http://127.0.0.1:3000/api/health', SHIELD_ORCH_URL: 'http://127.0.0.1:3000/api/orchestrator/notify', SHIELD_ROLLBACK_CMD: 'bash scripts/rollback-last-backup.sh', - SHIELD_REPAIR_CMD: `pm2 startOrRestart "${ECOSYSTEM_PATH}" --only unicorn,unicorn-orchestrator,unicorn-health-guardian,unicorn-quantum-watchdog`, + SHIELD_REPAIR_CMD: `pm2 startOrRestart "${ECOSYSTEM_PATH}" --only unicorn,unicorn-main-orchestrator,unicorn-health-daemon,unicorn-shield,unicorn-quantum-watchdog`, }, error_file: 'logs/shield-error.log', out_file: 'logs/shield-out.log', diff --git a/UNICORN_FINAL/scripts/unicorn-main-orchestrator.js b/UNICORN_FINAL/scripts/unicorn-main-orchestrator.js index 0cc2ea18..68cfa9fe 100644 --- a/UNICORN_FINAL/scripts/unicorn-main-orchestrator.js +++ b/UNICORN_FINAL/scripts/unicorn-main-orchestrator.js @@ -30,7 +30,7 @@ const POLL_MS = parseInt(process.env.ORCH_POLL_MS || '120000', 10) const SHIELD_URL = process.env.ORCH_SHIELD_URL || 'http://127.0.0.1:3000/api/quantum-integrity/status'; const HEALTH_URL = process.env.ORCH_HEALTH_URL || 'http://127.0.0.1:3000/api/health'; const INNOVATION_URL = process.env.ORCH_INNOVATION_URL || 'http://127.0.0.1:3000/api/innovation-loop/status'; -const DEPLOY_CMD = process.env.ORCH_DEPLOY_CMD || 'bash scripts/deploy-hetzner.js 2>&1 || bash deploy.sh 2>&1'; +const DEPLOY_CMD = process.env.ORCH_DEPLOY_CMD || 'node scripts/deploy-hetzner.js 2>&1 || bash deploy.sh 2>&1'; const ROLLBACK_CMD = process.env.ORCH_ROLLBACK_CMD || 'bash scripts/rollback-last-backup.sh'; const LINT_CMD = process.env.ORCH_LINT_CMD || 'npm run lint --if-present'; const TEST_CMD = process.env.ORCH_TEST_CMD || 'npm test --if-present'; @@ -205,7 +205,7 @@ async function rollback(reason = 'manual') { log('WARN', 'rollback — script de rollback a eșuat, încearcă git revert HEAD~1'); await run('git revert HEAD~1 --no-edit 2>&1 || git reset --hard HEAD~1 2>&1'); await run('npm install --production 2>&1'); - await run(`pm2 startOrRestart "${path.join(ROOT, 'ecosystem.config.js')}" --only unicorn,unicorn-orchestrator 2>&1`); + await run(`pm2 startOrRestart "${path.join(ROOT, 'ecosystem.config.js')}" --only unicorn,unicorn-main-orchestrator 2>&1`); } await new Promise(r => setTimeout(r, 8000)); diff --git a/UNICORN_FINAL/scripts/unicorn-shield.js b/UNICORN_FINAL/scripts/unicorn-shield.js index 3022c1af..759173c0 100644 --- a/UNICORN_FINAL/scripts/unicorn-shield.js +++ b/UNICORN_FINAL/scripts/unicorn-shield.js @@ -31,7 +31,7 @@ const FAIL_THRESHOLD = parseInt(process.env.SHIELD_FAIL_THRESHOLD || '3', const HEALTH_URL = process.env.SHIELD_HEALTH_URL || 'http://127.0.0.1:3000/api/health'; const ORCH_URL = process.env.SHIELD_ORCH_URL || 'http://127.0.0.1:3000/api/orchestrator/notify'; const ROLLBACK_CMD = process.env.SHIELD_ROLLBACK_CMD || 'bash scripts/rollback-last-backup.sh'; -const REPAIR_CMD = process.env.SHIELD_REPAIR_CMD || 'pm2 startOrRestart ecosystem.config.js --only unicorn,unicorn-orchestrator,unicorn-health-guardian,unicorn-quantum-watchdog'; +const REPAIR_CMD = process.env.SHIELD_REPAIR_CMD || 'pm2 startOrRestart ecosystem.config.js --only unicorn,unicorn-main-orchestrator,unicorn-health-daemon,unicorn-shield,unicorn-quantum-watchdog'; const MAX_LOG = 400; // Critical files that must exist at all times @@ -54,8 +54,9 @@ const CRITICAL_DIRS = [ // Required PM2 process names const CRITICAL_PROCESSES = [ 'unicorn', - 'unicorn-orchestrator', - 'unicorn-health-guardian', + 'unicorn-main-orchestrator', + 'unicorn-health-daemon', + 'unicorn-shield', 'unicorn-quantum-watchdog', ];