diff --git a/UNICORN_FINAL/autonomous-orchestrator.js b/UNICORN_FINAL/autonomous-orchestrator.js index ce2f09b1..81d6ad19 100644 --- a/UNICORN_FINAL/autonomous-orchestrator.js +++ b/UNICORN_FINAL/autonomous-orchestrator.js @@ -24,8 +24,10 @@ const DECISION_INTERVAL = parseInt(process.env.DECISION_INTERVAL || '45' const HEALTH_INTERVAL = 15 * 1000; const BACKEND_HEAL_CMD = process.env.BACKEND_HEAL_CMD || ''; const EDGE_HEALTH_URL = process.env.EDGE_HEALTH_URL || process.env.PUBLIC_APP_URL || ''; +const GH_PULL_INTERVAL = parseInt(process.env.GH_PULL_INTERVAL || '600', 10) * 1000; // 10 min +const GH_BRANCH = process.env.GITHUB_DEFAULT_BRANCH || 'main'; -let innovationEngine, revenueEngine, viralEngine, controlPlaneAgent, profitControlLoop; +let innovationEngine, revenueEngine, viralEngine, controlPlaneAgent, profitControlLoop, githubOps; // ─── Load engines gracefully (modules export singleton instances) ──────────── try { @@ -63,6 +65,13 @@ try { console.warn('⚠️ [ORCHESTRATOR] Profit Control Loop unavailable:', e.message); } +try { + githubOps = require(path.join(BASE, 'github-ops')); + console.log('🔧 [ORCHESTRATOR] GitHub Ops loaded'); +} catch (e) { + console.warn('⚠️ [ORCHESTRATOR] GitHub Ops unavailable:', e.message); +} + // ─── Stats tracking ────────────────────────────────────────────────────────── const stats = { startTime: new Date(), @@ -74,6 +83,7 @@ const stats = { monitorCycles: 0, decisionCycles: 0, healthChecks: 0, + githubCycles: 0, errors: 0, }; @@ -305,7 +315,55 @@ async function runDecisionCycle() { } } -// ─── Health report ──────────────────────────────────────────────────────────── +// ─── GitHub Operations cycle ────────────────────────────────────────────────── +async function runGithubCycle() { + stats.githubCycles++; + log('🔧', `GitHub Ops cycle #${stats.githubCycles}`); + if (!githubOps) { + log('⚠️', 'GitHub Ops not loaded — skipping'); + return; + } + try { + const status = githubOps.getStatus(); + if (!status.configured) { + log('⚠️', 'GitHub Ops not configured (GITHUB_TOKEN/GITHUB_REPOSITORY missing)'); + return; + } + + // 1. Pull latest from main branch + try { + const pullResult = await githubOps.pullLatest(GH_BRANCH); + log('⬇️', `git pull ${GH_BRANCH}`, { summary: pullResult.summary }); + } catch (pullErr) { + log('⚠️', `git pull failed: ${pullErr.message}`); + } + + // 2. Check if last CI workflow run failed → consider rollback + try { + const runs = await githubOps.getWorkflowRuns('deploy-hetzner.yml', 3); + const latestRun = runs[0]; + if (latestRun && latestRun.conclusion === 'failure') { + log('🚨', 'Last deploy workflow FAILED — checking if rollback needed', { run: latestRun.id, sha: latestRun.sha }); + const GH_AUTO_ROLLBACK = String(process.env.GH_AUTO_ROLLBACK || 'false').toLowerCase() === 'true'; + if (GH_AUTO_ROLLBACK) { + log('⏪', `Auto-rollback enabled — reverting ${latestRun.sha}`); + await githubOps.rollback(latestRun.sha, GH_BRANCH); + } + } else if (latestRun) { + log('✅', `Latest workflow run: ${latestRun.status}/${latestRun.conclusion || 'pending'}`, { id: latestRun.id }); + } + } catch (ciErr) { + log('⚠️', `CI status check failed: ${ciErr.message}`); + } + + log('🔧', 'GitHub Ops cycle complete', githubOps.getStatus().ops); + } catch (e) { + stats.errors++; + log('❌', 'GitHub Ops cycle error:', e.message); + } +} + + function printHealthReport() { stats.healthChecks++; const report = { @@ -318,12 +376,14 @@ function printHealthReport() { monitorCycles: stats.monitorCycles, decisionCycles: stats.decisionCycles, healthChecks: stats.healthChecks, + githubCycles: stats.githubCycles, errors: stats.errors, innovationEngine: innovationEngine ? 'ACTIVE' : 'MOCKED', revenueEngine: revenueEngine ? 'ACTIVE' : 'MOCKED', viralEngine: viralEngine ? 'ACTIVE' : 'MOCKED', controlPlaneAgent: controlPlaneAgent ? 'ACTIVE' : 'MOCKED', profitControlLoop: profitControlLoop ? 'ACTIVE' : 'MOCKED', + githubOps: githubOps ? 'ACTIVE' : 'UNAVAILABLE', nextInnovation: `${Math.round(INNOVATION_INTERVAL / 1000)}s`, nextRevenue: `${Math.round(REVENUE_INTERVAL / 1000)}s`, nextViral: `${Math.round(VIRAL_INTERVAL / 1000)}s`, @@ -354,6 +414,7 @@ process.on('uncaughtException', (err) => { log('🦄', ` Platform check every ${PLATFORM_INTERVAL / 1000}s`); log('🦄', ` Auto-Monitor every ${MONITOR_INTERVAL / 1000}s`); log('🦄', ` Auto-Decision AI every ${DECISION_INTERVAL / 1000}s`); + log('🦄', ` GitHub pull/rollback every ${GH_PULL_INTERVAL / 1000}s`); log('🦄', '══════════════════════════════════════════'); // Immediate first cycles @@ -364,6 +425,7 @@ process.on('uncaughtException', (err) => { await runPlatformCycle(); await runMonitorCycle(); await runDecisionCycle(); + await runGithubCycle(); printHealthReport(); // Schedule recurring cycles @@ -374,7 +436,8 @@ process.on('uncaughtException', (err) => { setInterval(runPlatformCycle, PLATFORM_INTERVAL); setInterval(runMonitorCycle, MONITOR_INTERVAL); setInterval(runDecisionCycle, DECISION_INTERVAL); + setInterval(runGithubCycle, GH_PULL_INTERVAL); setInterval(printHealthReport, HEALTH_INTERVAL); - log('✅', 'All 8 autonomous cycles scheduled and running 🚀'); + log('✅', 'All 9 autonomous cycles scheduled and running 🚀'); })(); diff --git a/UNICORN_FINAL/backend/index.js b/UNICORN_FINAL/backend/index.js index e0639cf7..32fcd36c 100644 --- a/UNICORN_FINAL/backend/index.js +++ b/UNICORN_FINAL/backend/index.js @@ -462,6 +462,7 @@ const profitLoop = require('./modules/profit-control-loop'); const centralOrchestrator = require('./modules/central-orchestrator'); const selfHealingEngine = require('./modules/self-healing-engine'); const autoInnovationLoop = require('./modules/auto-innovation-loop'); +const githubOps = require('./modules/github-ops'); // ==================== DYNAMIC PRICING ENGINE ==================== const dynamicPricing = require('./modules/dynamic-pricing'); @@ -3286,6 +3287,40 @@ app.post('/api/innovation-loop/trigger', adminTokenMiddleware, async (req, res) } catch (e) { res.status(500).json({ error: e.message }); } }); +// ==================== GITHUB OPS ROUTES ==================== +app.get('/api/github-ops/status', (req, res) => { + res.json(githubOps.getStatus()); +}); +app.get('/api/github-ops/workflow-runs', adminTokenMiddleware, async (req, res) => { + try { + const workflowId = req.query.workflow || 'deploy-hetzner.yml'; + const limit = Math.min(parseInt(req.query.limit || '10', 10), 50); + const runs = await githubOps.getWorkflowRuns(workflowId, limit); + res.json({ workflowId, runs }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); +app.post('/api/github-ops/pull', adminTokenMiddleware, async (req, res) => { + try { + const branch = req.body.branch || process.env.GITHUB_DEFAULT_BRANCH || 'main'; + const result = await githubOps.pullLatest(branch); + res.json({ success: true, branch, summary: result.summary }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); +app.post('/api/github-ops/trigger-workflow', adminTokenMiddleware, async (req, res) => { + try { + const { workflowId = 'deploy-hetzner.yml', branch, inputs } = req.body; + const result = await githubOps.triggerWorkflow(workflowId, branch, inputs); + res.json(result); + } catch (e) { res.status(500).json({ error: e.message }); } +}); +app.post('/api/github-ops/rollback', adminTokenMiddleware, async (req, res) => { + try { + const { commitSha, branch } = req.body; + const result = await githubOps.rollback(commitSha, branch); + res.json(result); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + // ==================== SELF CONSTRUCTION & TOTAL SYSTEM HEALER ROUTES ==================== app.get('/api/self-construction/status', adminTokenMiddleware, (req, res) => { res.json({ module: 'SelfConstruction', status: 'active', hasRun: selfConstruction.hasRun }); diff --git a/UNICORN_FINAL/backend/modules/github-ops.js b/UNICORN_FINAL/backend/modules/github-ops.js new file mode 100644 index 00000000..21b0c02a --- /dev/null +++ b/UNICORN_FINAL/backend/modules/github-ops.js @@ -0,0 +1,297 @@ +// ===================================================================== +// OWNERSHIP: Acest fișier este proprietatea exclusivă a lui Vladoi Ionut +// Email: vladoi_ionut@yahoo.com +// BTC Address: bc1q4f7e66z87mdfj56kz0dj5hvcnpmh0qh4wuv22e +// Data: 2026-04-13T19:44:00.000Z +// Orice copiere, modificare sau distribuție neautorizată este interzisă. +// ===================================================================== + +/** + * GITHUB OPS — Orchestrator GitHub Integration + * + * Oferă operații GitHub pentru orchestrator: + * 1. pullLatest(branch) — git pull origin + * 2. createBranch(name, from) — crează un branch nou + * 3. createPR(opts) — deschide un Pull Request via GitHub API + * 4. mergePR(number, method) — merge PR via GitHub API + * 5. triggerWorkflow(id,branch) — declanșează GitHub Actions workflow (CI/teste) + * 6. getWorkflowRuns(id,limit) — citește ultimele rulări ale unui workflow + * 7. rollback(commitSha) — revert commit + push (sau force-reset) + * 8. getStatus() — returnează starea modulului + * + * Token: GITHUB_TOKEN (PAT cu scope repo + workflow) + * Repo: GITHUB_REPOSITORY (owner/repo) sau GITHUB_REPO_OWNER + GITHUB_REPO_NAME + */ + +'use strict'; + +const path = require('path'); +const crypto = require('crypto'); + +// Lazy-load simple-git pentru a evita erori dacă pachetul lipseşte +let _git = null; +function getGit() { + if (_git) return _git; + try { + const simpleGit = require('simple-git'); + const rootPath = path.join(__dirname, '..', '..'); // UNICORN_FINAL root + _git = simpleGit(rootPath, { binary: 'git' }); + return _git; + } catch { + return null; + } +} + +// ─── Config ────────────────────────────────────────────────────────────────── +function getToken() { return process.env.GITHUB_TOKEN || ''; } +function getOwner() { return process.env.GITHUB_REPO_OWNER || (getRepo().split('/')[0] || ''); } +function getName() { return process.env.GITHUB_REPO_NAME || (getRepo().split('/')[1] || ''); } +function getRepo() { return process.env.GITHUB_REPOSITORY || ''; } +const DEFAULT_BRANCH = process.env.GITHUB_DEFAULT_BRANCH || 'main'; +const API_HOST = 'api.github.com'; +const MAX_LOG = 200; + +// ─── State ─────────────────────────────────────────────────────────────────── +const _log = []; +const _ops = { pull: 0, createPR: 0, mergePR: 0, workflow: 0, rollback: 0, errors: 0 }; +const startedAt = new Date().toISOString(); + +function _addLog(type, msg, extra) { + const entry = { ts: new Date().toISOString(), type, msg }; + if (extra) entry.extra = typeof extra === 'string' ? extra.slice(0, 300) : extra; + _log.push(entry); + if (_log.length > MAX_LOG) _log.shift(); + // Use separate args (not template literal format) to avoid tainted-format-string + console.log('[GithubOps]', entry.ts, '[' + String(type) + ']', String(msg), extra || ''); +} + +// ─── GitHub API helper ──────────────────────────────────────────────────────── +function githubAPI(method, path, body) { + return new Promise((resolve, reject) => { + const token = getToken(); + if (!token) return reject(new Error('GITHUB_TOKEN not configured')); + + const payload = body ? JSON.stringify(body) : null; + const https = require('https'); + const opts = { + hostname: API_HOST, + path, + method, + headers: { + 'User-Agent': 'unicorn-github-ops/1.0', + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + }, + timeout: 15000, + }; + if (payload) { + opts.headers['Content-Type'] = 'application/json'; + opts.headers['Content-Length'] = Buffer.byteLength(payload); + } + + const req = https.request(opts, (res) => { + let data = ''; + res.on('data', (c) => { data += c; }); + res.on('end', () => { + try { + const json = data ? JSON.parse(data) : {}; + if (res.statusCode >= 400) { + return reject(Object.assign(new Error(`GitHub API ${res.statusCode}: ${json.message || data.slice(0, 100)}`), { statusCode: res.statusCode, json })); + } + resolve({ status: res.statusCode, data: json }); + } catch { + resolve({ status: res.statusCode, data }); + } + }); + }); + + req.on('error', reject); + req.on('timeout', () => { req.destroy(new Error('GitHub API timeout')); }); + if (payload) req.write(payload); + req.end(); + }); +} + +// ─── Operations ────────────────────────────────────────────────────────────── + +/** + * git pull origin + */ +async function pullLatest(branch = DEFAULT_BRANCH) { + const git = getGit(); + if (!git) throw new Error('simple-git not available'); + _addLog('PULL', `pulling origin/${branch}`); + const token = getToken(); + const owner = getOwner(); + const name = getName(); + if (token && owner && name) { + try { + const remote = `https://x-access-token:${encodeURIComponent(token)}@github.com/${owner}/${name}.git`; + await git.remote(['set-url', 'origin', remote]); + } catch { /* ignore */ } + } + const result = await git.pull('origin', branch, { '--rebase': 'false' }); + _ops.pull++; + _addLog('PULL_OK', `pulled ${branch}`, result.summary); + return result; +} + +/** + * Crează un branch local + push la origin + */ +async function createBranch(branchName, fromBranch = DEFAULT_BRANCH) { + const git = getGit(); + if (!git) throw new Error('simple-git not available'); + _addLog('CREATE_BRANCH', `${fromBranch} → ${branchName}`); + await git.checkoutBranch(branchName, `origin/${fromBranch}`); + await git.push('origin', branchName, { '--set-upstream': null }); + _addLog('CREATE_BRANCH_OK', branchName); + return { branch: branchName, from: fromBranch }; +} + +/** + * Deschide un Pull Request via GitHub API + * @param {{ title, body, head, base }} opts + */ +async function createPR({ title, body = '', head, base = DEFAULT_BRANCH }) { + const owner = getOwner(); + const repo = getName(); + if (!owner || !repo) throw new Error('GITHUB_REPOSITORY not configured'); + _addLog('CREATE_PR', `${head} → ${base}: ${title}`); + const res = await githubAPI('POST', `/repos/${owner}/${repo}/pulls`, { title, body, head, base, draft: false }); + _ops.createPR++; + _addLog('CREATE_PR_OK', `PR #${res.data.number}`, res.data.html_url); + return res.data; +} + +/** + * Merge un Pull Request via GitHub API + */ +async function mergePR(number, mergeMethod = 'squash') { + const owner = getOwner(); + const repo = getName(); + if (!owner || !repo) throw new Error('GITHUB_REPOSITORY not configured'); + _addLog('MERGE_PR', `#${number} method=${mergeMethod}`); + const res = await githubAPI('PUT', `/repos/${owner}/${repo}/pulls/${number}/merge`, { + merge_method: mergeMethod, + commit_title: `[Orchestrator] Auto-merge PR #${number}`, + commit_message: `Merged automatically by Unicorn Orchestrator at ${new Date().toISOString()}`, + }); + _ops.mergePR++; + _addLog('MERGE_PR_OK', `#${number} merged`, res.data.sha); + return res.data; +} + +/** + * Declanșează un GitHub Actions workflow (workflow_dispatch event) + */ +async function triggerWorkflow(workflowId = 'deploy-hetzner.yml', branch = DEFAULT_BRANCH, inputs = {}) { + const owner = getOwner(); + const repo = getName(); + if (!owner || !repo) throw new Error('GITHUB_REPOSITORY not configured'); + _addLog('TRIGGER_WORKFLOW', `${workflowId} @ ${branch}`); + await githubAPI('POST', `/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflowId)}/dispatches`, { + ref: branch, + inputs, + }); + _ops.workflow++; + _addLog('TRIGGER_WORKFLOW_OK', `${workflowId} triggered`); + return { triggered: true, workflow: workflowId, branch }; +} + +/** + * Citește ultimele rulări ale unui workflow + */ +async function getWorkflowRuns(workflowId = 'deploy-hetzner.yml', limit = 5) { + const owner = getOwner(); + const repo = getName(); + if (!owner || !repo) throw new Error('GITHUB_REPOSITORY not configured'); + const res = await githubAPI('GET', `/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflowId)}/runs?per_page=${limit}`); + return (res.data.workflow_runs || []).map((r) => ({ + id: r.id, + status: r.status, + conclusion: r.conclusion, + createdAt: r.created_at, + branch: r.head_branch, + sha: r.head_sha, + url: r.html_url, + })); +} + +/** + * Rollback: revert la ultimul commit bun sau la un SHA specific + * Strategia 1: git revert --no-edit + push + * Strategia 2: dacă nu există sha — revine la HEAD~1 + */ +async function rollback(commitSha = null, branch = DEFAULT_BRANCH) { + const git = getGit(); + if (!git) throw new Error('simple-git not available'); + + // Actualizăm remote cu token pentru push autentificat + const token = getToken(); + const owner = getOwner(); + const name = getName(); + if (token && owner && name) { + try { + const remote = `https://x-access-token:${encodeURIComponent(token)}@github.com/${owner}/${name}.git`; + await git.remote(['set-url', 'origin', remote]); + } catch { /* ignore */ } + } + + const targetSha = commitSha || 'HEAD'; + _addLog('ROLLBACK', `reverting ${targetSha} on ${branch}`); + _ops.rollback++; + + try { + // Checkout branch şi pull latest + await git.checkout(branch); + await git.pull('origin', branch, { '--rebase': 'false' }); + // Revert commit + await git.revert([targetSha, '--no-edit']); + // Push rollback commit + await git.push('origin', branch); + _addLog('ROLLBACK_OK', `reverted ${targetSha}, pushed to ${branch}`); + return { ok: true, reverted: targetSha, branch }; + } catch (err) { + _ops.errors++; + _addLog('ROLLBACK_ERR', err.message); + throw err; + } +} + +/** + * Verifică dacă ultimul workflow run a eşuat şi returnează SHA-ul commit-ului + */ +async function getLastFailedCommit(workflowId = 'deploy-hetzner.yml') { + const runs = await getWorkflowRuns(workflowId, 3); + const failed = runs.find((r) => r.conclusion === 'failure'); + return failed || null; +} + +/** + * Returnează starea modulului + */ +function getStatus() { + return { + name: 'GithubOps', + startedAt, + configured: !!(getToken() && getOwner() && getName()), + owner: getOwner() || null, + repo: getName() || null, + ops: { ..._ops }, + recentLog: _log.slice(-20), + }; +} + +module.exports = { + pullLatest, + createBranch, + createPR, + mergePR, + triggerWorkflow, + getWorkflowRuns, + rollback, + getLastFailedCommit, + getStatus, +}; diff --git a/UNICORN_FINAL/ecosystem.config.js b/UNICORN_FINAL/ecosystem.config.js index bad8d19e..84ef58f5 100644 --- a/UNICORN_FINAL/ecosystem.config.js +++ b/UNICORN_FINAL/ecosystem.config.js @@ -63,6 +63,8 @@ module.exports = { REVENUE_INTERVAL: '15', VIRAL_INTERVAL: '20', DEPLOYMENT_INTERVAL: '120', + GH_PULL_INTERVAL: '600', + GH_AUTO_ROLLBACK: 'false', BACKEND_HEAL_CMD: 'pm2 restart unicorn', BACKEND_BASE_URL: 'http://127.0.0.1:3000', DOMAIN: SITE_DOMAIN, @@ -88,7 +90,10 @@ module.exports = { NODE_ENV: 'production', HEALTH_GUARDIAN_URL: 'http://127.0.0.1:3000/api/health', HEALTH_GUARDIAN_SHIELD_URL: 'http://127.0.0.1:3000/api/quantum-integrity/status', + HEALTH_GUARDIAN_EXTERNAL_URL: `${PUBLIC_APP_URL}/health`, + HEALTH_GUARDIAN_ORCHESTRATOR_URL: 'http://127.0.0.1:3000/api/orchestrator/check', HEALTH_GUARDIAN_INTERVAL_MS: '30000', + HEALTH_GUARDIAN_EXTERNAL_MS: '120000', HEALTH_GUARDIAN_FAIL_THRESHOLD: '3', HEALTH_GUARDIAN_HEAL_CMD: `pm2 startOrRestart "${ECOSYSTEM_PATH}" --only unicorn,unicorn-orchestrator,unicorn-health-guardian,unicorn-quantum-watchdog`, HEALTH_GUARDIAN_ROLLBACK_CMD: 'bash scripts/rollback-last-backup.sh' @@ -285,6 +290,35 @@ module.exports = { log_date_format: 'YYYY-MM-DD HH:mm:ss' }, + // ── 5b. System Shield (real-time file watch + process monitor + deploy lock) ─ + { + name: 'unicorn-system-shield', + script: 'scripts/system-shield.js', + cwd: __dirname, + instances: 1, + autorestart: true, + watch: false, + max_restarts: 20, + min_uptime: '10s', + restart_delay: 5000, + env: { + NODE_ENV: 'production', + SHIELD_PORT: '3099', + SHIELD_PROCESS_POLL: '30000', + SHIELD_FILE_RESTORE: 'true', + SHIELD_HEAL_COOLDOWN_MS: '60000', + SHIELD_REQUIRED_PROCS: 'unicorn,unicorn-orchestrator,unicorn-health-guardian,unicorn-quantum-watchdog,unicorn-platform-connector,unicorn-uaic', + SHIELD_NOTIFY_URL: 'http://127.0.0.1:3000/api/orchestrator/check', + SHIELD_LOCK_FILE: '/tmp/unicorn-deploy.lock', + DOMAIN: SITE_DOMAIN, + PUBLIC_APP_URL + }, + error_file: 'logs/system-shield-error.log', + out_file: 'logs/system-shield-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss' + }, + + // ── 6. Llama Bridge (starts Ollama serve at P4 / nice +10) ──────────────── // Prerequisite: `ollama` must be installed on the server. // ── 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 diff --git a/UNICORN_FINAL/scripts/health-guardian.js b/UNICORN_FINAL/scripts/health-guardian.js index 71684ee0..ecc3d0e8 100644 --- a/UNICORN_FINAL/scripts/health-guardian.js +++ b/UNICORN_FINAL/scripts/health-guardian.js @@ -1,25 +1,32 @@ #!/usr/bin/env node 'use strict'; -const http = require('http'); +const http = require('http'); +const https = require('https'); const { exec } = require('child_process'); -const CHECK_URL = process.env.HEALTH_GUARDIAN_URL || 'http://127.0.0.1:3000/api/health'; -const SHIELD_URL = process.env.HEALTH_GUARDIAN_SHIELD_URL || 'http://127.0.0.1:3000/api/quantum-integrity/status'; -const CHECK_INTERVAL_MS = Math.max(parseInt(process.env.HEALTH_GUARDIAN_INTERVAL_MS || '60000', 10), 10000); -const FAIL_THRESHOLD = Math.max(parseInt(process.env.HEALTH_GUARDIAN_FAIL_THRESHOLD || '3', 10), 1); -const HEAL_CMD = process.env.HEALTH_GUARDIAN_HEAL_CMD || 'systemctl restart unicorn'; -const ROLLBACK_CMD = process.env.HEALTH_GUARDIAN_ROLLBACK_CMD || ''; +const CHECK_URL = process.env.HEALTH_GUARDIAN_URL || 'http://127.0.0.1:3000/api/health'; +const SHIELD_URL = process.env.HEALTH_GUARDIAN_SHIELD_URL || 'http://127.0.0.1:3000/api/quantum-integrity/status'; +// URL extern public (ex. https://zeusai.pro sau https://zeusai.pro/health) +const EXTERNAL_URL = process.env.HEALTH_GUARDIAN_EXTERNAL_URL || process.env.PUBLIC_APP_URL || process.env.EDGE_HEALTH_URL || ''; +// URL orchestrator pentru notificări (POST JSON) +const ORCHESTRATOR_URL = process.env.HEALTH_GUARDIAN_ORCHESTRATOR_URL || 'http://127.0.0.1:3000/api/orchestrator/check'; +const CHECK_INTERVAL_MS = Math.max(parseInt(process.env.HEALTH_GUARDIAN_INTERVAL_MS || '60000', 10), 10000); +const EXTERNAL_INTERVAL_MS = Math.max(parseInt(process.env.HEALTH_GUARDIAN_EXTERNAL_MS || '120000', 10), 30000); +const FAIL_THRESHOLD = Math.max(parseInt(process.env.HEALTH_GUARDIAN_FAIL_THRESHOLD || '3', 10), 1); +const HEAL_CMD = process.env.HEALTH_GUARDIAN_HEAL_CMD || 'systemctl restart unicorn'; +const ROLLBACK_CMD = process.env.HEALTH_GUARDIAN_ROLLBACK_CMD || ''; -let consecutiveFailures = 0; +let consecutiveFailures = 0; let consecutiveIntegrityFailures = 0; +let consecutiveExternalFailures = 0; function log(msg, extra) { const ts = new Date().toISOString(); if (extra) { - console.log(`[HealthGuardian] ${ts} ${msg}`, extra); + console.log('[HealthGuardian]', ts, String(msg), extra); } else { - console.log(`[HealthGuardian] ${ts} ${msg}`); + console.log('[HealthGuardian]', ts, String(msg)); } } @@ -54,36 +61,61 @@ function checkHealth() { }); } -function checkShield() { +/** Verifică URL-ul public extern (https://zeusai.pro) */ +function checkExternal() { + if (!EXTERNAL_URL) return Promise.resolve({ ok: true, skipped: true }); return new Promise((resolve) => { - const req = http.get(SHIELD_URL, (res) => { + let parsedUrl; + try { parsedUrl = new URL(EXTERNAL_URL); } catch { + return resolve({ ok: false, reason: 'invalid-url' }); + } + const mod = parsedUrl.protocol === 'https:' ? https : http; + const checkPath = (parsedUrl.pathname && parsedUrl.pathname !== '/') ? parsedUrl.pathname : '/health'; + const opts = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), + path: checkPath, + method: 'GET', + timeout: 12000, + headers: { 'User-Agent': 'unicorn-health-guardian/1.0' }, + }; + const req = mod.request(opts, (res) => { let body = ''; - res.on('data', (chunk) => { - body += chunk; - }); + res.on('data', (c) => { body += c; }); res.on('end', () => { - if (res.statusCode !== 200) { - return resolve({ ok: false, reason: `status:${res.statusCode}` }); - } - try { - const json = JSON.parse(body || '{}'); - const integrity = String(json.integrity || '').toLowerCase(); - const ok = integrity === 'intact' || integrity === 'pending'; - resolve({ ok, reason: ok ? 'ok' : integrity || 'unknown' }); - } catch { - resolve({ ok: false, reason: 'invalid-json' }); + if (res.statusCode >= 200 && res.statusCode < 400) { + return resolve({ ok: true, status: res.statusCode }); } + resolve({ ok: false, reason: `external:status:${res.statusCode}`, body: body.slice(0, 100) }); }); }); - - req.on('error', (err) => resolve({ ok: false, reason: err.message })); - req.setTimeout(8000, () => { - req.destroy(new Error('timeout')); - resolve({ ok: false, reason: 'timeout' }); - }); + req.on('error', (err) => resolve({ ok: false, reason: `external:${err.message}` })); + req.on('timeout', () => { req.destroy(); resolve({ ok: false, reason: 'external:timeout' }); }); + req.end(); }); } +/** Notifică orchestratorul cu un event */ +function notifyOrchestrator(event, details) { + if (!ORCHESTRATOR_URL) return; + const payload = JSON.stringify({ source: 'health-guardian', event, details, ts: new Date().toISOString() }); + try { + const url = new URL(ORCHESTRATOR_URL); + const mod = url.protocol === 'https:' ? https : http; + const req = mod.request({ + hostname: url.hostname, + port: url.port || (url.protocol === 'https:' ? 443 : 80), + path: url.pathname, + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }, + timeout: 5000, + }, (res) => { res.resume(); }); + req.on('error', (e) => { log(`orchestrator notify failed: ${ORCHESTRATOR_URL}: ${e.message}`); }); + req.write(payload); + req.end(); + } catch { /* ignore */ } +} + function heal() { return new Promise((resolve) => { exec(HEAL_CMD, (err, stdout, stderr) => { @@ -102,18 +134,19 @@ async function loop() { if (result.ok) { if (consecutiveFailures > 0) { log(`health restored after ${consecutiveFailures} failure(s)`); + notifyOrchestrator('health_restored', { url: CHECK_URL }); } consecutiveFailures = 0; - return; - } - - consecutiveFailures += 1; - log(`health check failed (${consecutiveFailures}/${FAIL_THRESHOLD})`, result.reason || result.body || 'unknown'); + } else { + consecutiveFailures += 1; + log(`health check failed (${consecutiveFailures}/${FAIL_THRESHOLD})`, result.reason || result.body || 'unknown'); + notifyOrchestrator('health_failure', { url: CHECK_URL, reason: result.reason, failures: consecutiveFailures }); - if (consecutiveFailures >= FAIL_THRESHOLD) { - log('triggering auto-heal...'); - await heal(); - consecutiveFailures = 0; + if (consecutiveFailures >= FAIL_THRESHOLD) { + log('triggering auto-heal...'); + await heal(); + consecutiveFailures = 0; + } } if (shield.ok) { @@ -121,30 +154,60 @@ async function loop() { log(`integrity restored after ${consecutiveIntegrityFailures} failure(s)`); } consecutiveIntegrityFailures = 0; + } else { + consecutiveIntegrityFailures += 1; + log(`integrity check failed (${consecutiveIntegrityFailures}/${FAIL_THRESHOLD})`, shield.reason || 'unknown'); + notifyOrchestrator('integrity_failure', { reason: shield.reason, failures: consecutiveIntegrityFailures }); + if (consecutiveIntegrityFailures >= FAIL_THRESHOLD) { + log('triggering integrity heal...'); + await heal(); + if (ROLLBACK_CMD) { + await new Promise((resolve) => { + exec(ROLLBACK_CMD, (err, stdout, stderr) => { + if (err) { + log('rollback command failed', { error: err.message, stderr: (stderr || '').slice(0, 250) }); + } else { + log('rollback command executed', { stdout: (stdout || '').slice(0, 250) }); + } + resolve(); + }); + }); + } + consecutiveIntegrityFailures = 0; + } + } +} + +async function externalLoop() { + const result = await checkExternal(); + if (result.skipped) return; + + if (result.ok) { + if (consecutiveExternalFailures > 0) { + log(`external health restored after ${consecutiveExternalFailures} failure(s)`, { url: EXTERNAL_URL }); + notifyOrchestrator('external_restored', { url: EXTERNAL_URL }); + } + consecutiveExternalFailures = 0; return; } - consecutiveIntegrityFailures += 1; - log(`integrity check failed (${consecutiveIntegrityFailures}/${FAIL_THRESHOLD})`, shield.reason || 'unknown'); - if (consecutiveIntegrityFailures >= FAIL_THRESHOLD) { - log('triggering integrity heal...'); + consecutiveExternalFailures += 1; + log(`external health check failed (${consecutiveExternalFailures}/${FAIL_THRESHOLD})`, { url: EXTERNAL_URL, reason: result.reason }); + notifyOrchestrator('external_failure', { url: EXTERNAL_URL, reason: result.reason, failures: consecutiveExternalFailures }); + + if (consecutiveExternalFailures >= FAIL_THRESHOLD) { + log('external down threshold reached — triggering heal', { url: EXTERNAL_URL }); await heal(); - if (ROLLBACK_CMD) { - await new Promise((resolve) => { - exec(ROLLBACK_CMD, (err, stdout, stderr) => { - if (err) { - log('rollback command failed', { error: err.message, stderr: (stderr || '').slice(0, 250) }); - } else { - log('rollback command executed', { stdout: (stdout || '').slice(0, 250) }); - } - resolve(); - }); - }); - } - consecutiveIntegrityFailures = 0; + consecutiveExternalFailures = 0; } } -log(`started. interval=${CHECK_INTERVAL_MS}ms, threshold=${FAIL_THRESHOLD}, url=${CHECK_URL}, shield=${SHIELD_URL}`); +const externalInfo = EXTERNAL_URL ? `, external=${EXTERNAL_URL}` : ' (no external URL configured)'; +log(`started. interval=${CHECK_INTERVAL_MS}ms, threshold=${FAIL_THRESHOLD}, url=${CHECK_URL}, shield=${SHIELD_URL}${externalInfo}`); setInterval(loop, CHECK_INTERVAL_MS); loop(); + +if (EXTERNAL_URL) { + setInterval(externalLoop, EXTERNAL_INTERVAL_MS); + setTimeout(externalLoop, 10000); // primă verificare externă după 10s +} diff --git a/UNICORN_FINAL/scripts/system-shield.js b/UNICORN_FINAL/scripts/system-shield.js new file mode 100644 index 00000000..955cd2ca --- /dev/null +++ b/UNICORN_FINAL/scripts/system-shield.js @@ -0,0 +1,415 @@ +#!/usr/bin/env node +// ===================================================================== +// OWNERSHIP: Acest fișier este proprietatea exclusivă a lui Vladoi Ionut +// Email: vladoi_ionut@yahoo.com +// BTC Address: bc1q4f7e66z87mdfj56kz0dj5hvcnpmh0qh4wuv22e +// Data: 2026-04-13T19:44:00.000Z +// Orice copiere, modificare sau distribuție neautorizată este interzisă. +// ===================================================================== + +/** + * SYSTEM SHIELD — Scut autonom pentru fișiere, procese şi deploy-uri + * + * Responsabilități: + * 1. Monitorizare fișiere critice în timp real (chokidar fs.watch) + * 2. Monitorizare procese PM2 la fiecare PROCESS_POLL_MS + * 3. Reparare fișiere corupte/lipsă din git (git checkout -- ) + * 4. Repornire procese căzute via pm2 restart + * 5. Deploy-lock: blochează deploy-uri incomplete (lock file) + * 6. HTTP API pe portul SHIELD_PORT (/status, /lock, /unlock, /heal) + * + * Env vars: + * SHIELD_PORT — portul HTTP intern (default: 3099) + * SHIELD_PROCESS_POLL — interval poll procese ms (default: 30000) + * SHIELD_FILE_RESTORE — 'true' să restaureze din git (default: true) + * SHIELD_REQUIRED_PROCS — procese PM2 obligatorii (csv, default: unicorn,unicorn-orchestrator,unicorn-health-guardian) + * SHIELD_CRITICAL_FILES — fișiere critice de monitorizat (csv) + * SHIELD_LOCK_FILE — calea lock-ului de deploy (default: /tmp/unicorn-deploy.lock) + * SHIELD_NOTIFY_URL — URL orchestrator pentru notificări (POST) + */ + +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const { execSync } = require('child_process'); +const { execFile } = require('child_process'); +const http = require('http'); + +// ─── Config ─────────────────────────────────────────────────────────────────── +const ROOT = path.join(__dirname, '..'); // UNICORN_FINAL/ +const SHIELD_PORT = parseInt(process.env.SHIELD_PORT || '3099', 10); +const PROCESS_POLL_MS = Math.max(parseInt(process.env.SHIELD_PROCESS_POLL || '30000', 10), 10000); +const FILE_RESTORE = String(process.env.SHIELD_FILE_RESTORE || 'true').toLowerCase() !== 'false'; +const LOCK_FILE = process.env.SHIELD_LOCK_FILE || '/tmp/unicorn-deploy.lock'; +const NOTIFY_URL = process.env.SHIELD_NOTIFY_URL || process.env.ORCHESTRATOR_NOTIFY_URL || ''; +const MAX_LOG = 300; +const HEAL_COOLDOWN_MS = parseInt(process.env.SHIELD_HEAL_COOLDOWN_MS || '60000', 10); + +const REQUIRED_PROCS = (process.env.SHIELD_REQUIRED_PROCS || + 'unicorn,unicorn-orchestrator,unicorn-health-guardian,unicorn-quantum-watchdog,unicorn-platform-connector') + .split(',').map((s) => s.trim()).filter(Boolean); + +const DEFAULT_CRITICAL_FILES = [ + path.join(ROOT, 'backend/index.js'), + path.join(ROOT, 'ecosystem.config.js'), + path.join(ROOT, 'package.json'), + path.join(ROOT, 'scripts/health-guardian.js'), + path.join(ROOT, 'scripts/system-shield.js'), + path.join(ROOT, 'scripts/rollback-last-backup.sh'), + path.join(ROOT, 'autonomous-orchestrator.js'), +]; + +const CRITICAL_FILES = process.env.SHIELD_CRITICAL_FILES + ? process.env.SHIELD_CRITICAL_FILES.split(',').map((f) => f.trim()) + : DEFAULT_CRITICAL_FILES; + +// ─── State ──────────────────────────────────────────────────────────────────── +const state = { + startedAt: new Date().toISOString(), + watchedFiles: [], + missingFiles: [], + restoredFiles: [], + missingProcs: [], + restarted: [], + deployLocked: false, + deployLockInfo: null, + healCount: 0, + errors: 0, + eventLog: [], +}; + +const lastHealAt = {}; // procName → timestamp + +function log(type, msg, extra) { + const entry = { ts: new Date().toISOString(), type, msg }; + if (extra !== undefined) entry.extra = typeof extra === 'string' ? extra.slice(0, 300) : extra; + state.eventLog.push(entry); + if (state.eventLog.length > MAX_LOG) state.eventLog.shift(); + // Use separate args (not template literal format) to avoid tainted-format-string + console.log('[SystemShield]', entry.ts, '[' + String(type) + ']', String(msg), extra !== undefined ? extra : ''); +} + +// ─── Validation helpers ─────────────────────────────────────────────────────── + +// PM2 process names: alphanumeric, hyphen, underscore, dot only +const SAFE_PROC_NAME_RE = /^[a-zA-Z0-9_.\-]+$/; +function isSafeProcName(name) { return SAFE_PROC_NAME_RE.test(name); } + +// Ensure filePath stays within ROOT to prevent path traversal +function isSafeFilePath(filePath) { + const resolvedRoot = path.resolve(ROOT); + const resolvedFile = path.resolve(filePath); + const rel = path.relative(resolvedRoot, resolvedFile); + return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel); +} + + + +function lockDeploy(reason = '') { + const info = { lockedAt: new Date().toISOString(), reason, pid: process.pid }; + fs.writeFileSync(LOCK_FILE, JSON.stringify(info, null, 2), 'utf8'); + state.deployLocked = true; + state.deployLockInfo = info; + log('DEPLOY_LOCK', `Deploy blocat: ${reason}`, info); +} + +function unlockDeploy() { + try { fs.unlinkSync(LOCK_FILE); } catch (e) { + if (e.code !== 'ENOENT') log('UNLOCK_ERR', e.message); + } + state.deployLocked = false; + state.deployLockInfo = null; + log('DEPLOY_UNLOCK', 'Deploy deblocat'); +} + +function isDeployLocked() { + try { + if (fs.existsSync(LOCK_FILE)) { + let info; + try { + info = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf8') || '{}'); + } catch (parseErr) { + log('LOCK_PARSE_ERR', parseErr.message); + info = {}; + } + state.deployLocked = true; + state.deployLockInfo = info; + // Auto-expire lock după 30 minute + const age = Date.now() - new Date(info.lockedAt || 0).getTime(); + if (age > 30 * 60 * 1000) { + log('DEPLOY_LOCK_EXPIRED', 'Lock expirat automat (> 30 min), deblocare'); + unlockDeploy(); + return false; + } + return true; + } + } catch (e) { + log('LOCK_CHECK_ERR', e.message); + } + state.deployLocked = false; + state.deployLockInfo = null; + return false; +} + +// ─── File Watcher ───────────────────────────────────────────────────────────── + +function startFileWatcher() { + let chokidar; + try { chokidar = require('chokidar'); } catch { chokidar = null; } + + if (!chokidar) { + log('WATCHER_WARN', 'chokidar nu este disponibil, se foloseste polling'); + _startPollingWatcher(); + return; + } + + const watcher = chokidar.watch(CRITICAL_FILES, { + persistent: true, + ignoreInitial: true, + awaitWriteFinish: { stabilityThreshold: 1000, pollInterval: 200 }, + }); + + watcher.on('add', (f) => log('FILE_ADDED', f)); + watcher.on('unlink', (f) => { log('FILE_DELETED', f); _handleMissingFile(f); }); + watcher.on('change', (f) => log('FILE_CHANGED', f)); + watcher.on('error', (e) => { state.errors++; log('WATCHER_ERROR', e.message); }); + + state.watchedFiles = CRITICAL_FILES.slice(); + log('WATCHER_STARTED', `Monitorizez ${CRITICAL_FILES.length} fișiere critice`); +} + +function _startPollingWatcher() { + // Fallback: verifică existenţa fișierelor la fiecare 60s + const knownHashes = {}; + for (const f of CRITICAL_FILES) { + try { knownHashes[f] = fs.statSync(f).mtimeMs; } catch { knownHashes[f] = null; } + } + setInterval(() => { + for (const f of CRITICAL_FILES) { + try { + const mtime = fs.statSync(f).mtimeMs; + if (knownHashes[f] === null) { + log('FILE_RESTORED_EXT', f); + knownHashes[f] = mtime; + } else if (mtime !== knownHashes[f]) { + log('FILE_CHANGED_POLL', f); + knownHashes[f] = mtime; + } + } catch { + if (knownHashes[f] !== null) { + log('FILE_DELETED_POLL', f); + knownHashes[f] = null; + _handleMissingFile(f); + } + } + } + }, 60000); + state.watchedFiles = CRITICAL_FILES.slice(); +} + +function _handleMissingFile(filePath) { + state.missingFiles.push({ file: filePath, at: new Date().toISOString() }); + if (!FILE_RESTORE) { + log('FILE_MISSING', filePath, 'restaurare dezactivata'); + _notify('file_missing', { file: filePath }); + return; + } + _restoreFileFromGit(filePath); +} + +function _restoreFileFromGit(filePath) { + if (!isSafeFilePath(filePath)) { + log('FILE_RESTORE_BLOCKED', `Path unsafe, skipping restore: ${filePath}`); + return; + } + const relPath = path.relative(ROOT, filePath); + // Validate relPath contains no shell-special characters + if (/[;&|`$<>\\]/.test(relPath)) { + log('FILE_RESTORE_BLOCKED', `Path contains unsafe characters: ${relPath}`); + return; + } + log('FILE_RESTORE', 'Restaurare din git: ' + relPath); + execFile('git', ['-C', ROOT, 'checkout', '--', relPath], (err) => { + if (err) { + log('FILE_RESTORE_ERR', relPath, err.message); + _notify('file_restore_failed', { file: relPath, error: err.message }); + } else { + log('FILE_RESTORE_OK', relPath); + state.restoredFiles.push({ file: filePath, at: new Date().toISOString() }); + _notify('file_restored', { file: relPath }); + } + }); +} + +// ─── Process Monitor ───────────────────────────────────────────────────────── + +function startProcessMonitor() { + setInterval(_checkProcesses, PROCESS_POLL_MS); + setTimeout(_checkProcesses, 5000); // verificare inițială + log('PROCESS_MONITOR_STARTED', `Poll la fiecare ${PROCESS_POLL_MS}ms, procese: ${REQUIRED_PROCS.join(', ')}`); +} + +function _checkProcesses() { + execFile('pm2', ['jlist'], { timeout: 10000 }, (err, stdout) => { + if (err) { + log('PM2_UNAVAILABLE', err.message); + return; + } + let list; + try { list = JSON.parse(stdout || '[]'); } catch { list = []; } + + const online = new Set( + list + .filter((p) => p?.pm2_env?.status === 'online') + .map((p) => String(p.name || '').trim()) + ); + + const missing = REQUIRED_PROCS.filter((name) => !online.has(name)); + state.missingProcs = missing; + + if (missing.length === 0) return; + + log('PROCESSES_DOWN', `Procese căzute: ${missing.join(', ')}`); + for (const procName of missing) _restartProcess(procName); + }); +} + +function _restartProcess(procName) { + if (!isSafeProcName(procName)) { + log('RESTART_BLOCKED', `Process name contains unsafe characters: ${procName}`); + return; + } + const now = Date.now(); + const last = lastHealAt[procName] || 0; + if (now - last < HEAL_COOLDOWN_MS) { + log('HEAL_COOLDOWN', `${procName} — cooldown activ, aştept`); + return; + } + lastHealAt[procName] = now; + state.healCount++; + + log('RESTART_PROC', 'pm2 restart ' + procName); + execFile('pm2', ['restart', procName], { timeout: 15000 }, (err, stdout) => { + if (err) { + log('RESTART_ERR', procName, err.message); + _notify('process_restart_failed', { process: procName, error: err.message }); + // Încercare startOrRestart dacă restart a eşuat + execFile('pm2', ['startOrRestart', 'ecosystem.config.js', '--only', procName], { cwd: ROOT, timeout: 20000 }, (err2) => { + if (err2) log('START_ERR', procName, err2.message); + else { log('START_OK', procName); state.restarted.push({ process: procName, at: new Date().toISOString() }); } + }); + } else { + log('RESTART_OK', procName); + state.restarted.push({ process: procName, at: new Date().toISOString(), out: (stdout || '').slice(0, 100) }); + _notify('process_restarted', { process: procName }); + } + }); +} + +// ─── Notificare Orchestrator ────────────────────────────────────────────────── + +function _notify(event, data) { + if (!NOTIFY_URL) return; + const payload = JSON.stringify({ source: 'system-shield', event, data, ts: new Date().toISOString() }); + try { + const url = new URL(NOTIFY_URL); + const mod = url.protocol === 'https:' ? require('https') : require('http'); + const req = mod.request({ + hostname: url.hostname, + port: url.port || (url.protocol === 'https:' ? 443 : 80), + path: url.pathname, + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }, + timeout: 5000, + }, (res) => { res.resume(); }); + req.on('error', (e) => { log('NOTIFY_ERR', `${NOTIFY_URL}: ${e.message}`); }); + req.write(payload); + req.end(); + } catch { /* ignore */ } +} + +// ─── HTTP API ───────────────────────────────────────────────────────────────── + +function startAPI() { + const server = http.createServer((req, res) => { + const { method, url } = req; + + if (url === '/status' || url === '/') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ + ...state, + eventLog: state.eventLog.slice(-50), + deployLocked: isDeployLocked(), + })); + } + + if (url === '/lock' && method === 'POST') { + let body = ''; + req.on('data', (c) => { body += c; }); + req.on('end', () => { + let reason = ''; + try { reason = JSON.parse(body).reason || ''; } catch { /* noop */ } + lockDeploy(reason || 'locked via API'); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ locked: true, info: state.deployLockInfo })); + }); + return; + } + + if (url === '/unlock' && method === 'POST') { + unlockDeploy(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ locked: false })); + } + + if (url === '/heal' && method === 'POST') { + log('MANUAL_HEAL', 'Declanşat via API'); + _checkProcesses(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ triggered: true })); + } + + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'not found' })); + }); + + server.listen(SHIELD_PORT, '127.0.0.1', () => { + log('API_STARTED', `Shield HTTP API pe 127.0.0.1:${SHIELD_PORT}`); + }); + + server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + log('API_WARN', `Port ${SHIELD_PORT} ocupat — API HTTP dezactivat`); + } else { + log('API_ERROR', err.message); + } + }); +} + +// ─── Bootstrap ─────────────────────────────────────────────────────────────── + +log('SHIELD_START', `System Shield pornit — PID ${process.pid}`); +log('CONFIG', 'Config', { + port: SHIELD_PORT, + pollMs: PROCESS_POLL_MS, + fileRestore: FILE_RESTORE, + lockFile: LOCK_FILE, + requiredProcs: REQUIRED_PROCS, + criticalFiles: CRITICAL_FILES.length, +}); + +isDeployLocked(); // încarcă starea curentă a lock-ului +startFileWatcher(); +startProcessMonitor(); +startAPI(); + +// Menţine procesul viu +process.on('uncaughtException', (err) => { + state.errors++; + log('UNCAUGHT', err.message, err.stack ? err.stack.slice(0, 300) : ''); +}); +process.on('unhandledRejection', (reason) => { + state.errors++; + log('UNHANDLED_REJECTION', String(reason).slice(0, 200)); +});