diff --git a/panel/api/endpoints/proxy.php b/panel/api/endpoints/proxy.php index 45f7dd6..12ed122 100644 --- a/panel/api/endpoints/proxy.php +++ b/panel/api/endpoints/proxy.php @@ -141,6 +141,33 @@ try { $action === 'test-remote' && $method === 'POST' => Response::json(['success' => true, 'data' => ProxyManager::testRemote()]), + // POST setup-remote — run nginx setup on remote VM, stream output via SSE + ($action === 'setup-remote') && $method === 'POST' => (function() { + header('Content-Type: text/event-stream'); + header('Cache-Control: no-cache'); + header('X-Accel-Buffering: no'); + ob_implicit_flush(true); + while (ob_get_level() > 0) ob_end_flush(); + foreach (ProxyManager::runSetupOnRemote() as $line) { + echo 'data: ' . json_encode(['line' => $line]) . "\n\n"; + flush(); + } + echo "data: " . json_encode(['done' => true]) . "\n\n"; + flush(); + exit; + })(), + + // DELETE uninstall — remove proxy configs (and optionally nginx) + $action === 'uninstall' && $method === 'DELETE' => (function() use ($body) { + $removeNginx = !empty($body['remove_nginx']); + $result = ProxyManager::uninstall($removeNginx); + if ($removeNginx) { + $db = DB::getInstance(); + $db->execute("INSERT INTO settings (`key`, value) VALUES ('proxy_mode','disabled') ON DUPLICATE KEY UPDATE value='disabled'"); + } + Response::json(['success' => true, 'data' => ['result' => $result]]); + })(), + default => Response::error('Not found', 404), }; diff --git a/panel/bin/collect-stats.php b/panel/bin/collect-stats.php index 1e3b394..662c84f 100644 --- a/panel/bin/collect-stats.php +++ b/panel/bin/collect-stats.php @@ -63,3 +63,7 @@ $db->execute( // Prune rows older than 30 days $db->execute("DELETE FROM server_stats WHERE recorded_at < DATE_SUB(NOW(), INTERVAL 30 DAY)"); + +// Proxy health check — restart nginx on remote proxy VM if it's stopped +require_once NOVACPX_LIB . '/ProxyManager.php'; +ProxyManager::healthCheck(); diff --git a/panel/lib/ProxyManager.php b/panel/lib/ProxyManager.php index 4072d2e..fd87c64 100644 --- a/panel/lib/ProxyManager.php +++ b/panel/lib/ProxyManager.php @@ -284,6 +284,74 @@ class ProxyManager { return ['ok' => true, 'message' => 'Connected — ' . trim($out)]; } + // --- Remote setup & uninstall --- + + public static function runSetupOnRemote(): \Generator { + $r = self::getRemote(); + if (!$r['host']) { yield "ERROR: No remote host configured\n"; return; } + + $steps = [ + 'Updating package lists' => 'apt-get update -qq 2>&1', + 'Installing nginx' => 'apt-get install -y nginx 2>&1', + 'Disabling default site' => 'rm -f /etc/nginx/sites-enabled/default', + 'Creating conf directories' => 'mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled', + 'Writing tune config' => 'printf "client_max_body_size 256M;\nproxy_buffers 16 16k;\nproxy_buffer_size 16k;\n" > /etc/nginx/conf.d/novacpx-proxy.conf', + 'Writing catch-all vhost' => 'printf "server {\n listen 80 default_server;\n server_name _;\n return 444;\n}\n" > /etc/nginx/sites-available/novacpx-default.conf && ln -sf /etc/nginx/sites-available/novacpx-default.conf /etc/nginx/sites-enabled/', + 'Testing nginx config' => 'nginx -t 2>&1', + 'Enabling and starting nginx' => 'systemctl enable nginx 2>/dev/null && systemctl restart nginx 2>&1 && systemctl is-active nginx', + ]; + + foreach ($steps as $label => $cmd) { + yield "» {$label}...\n"; + $out = self::remoteExec($cmd); + if ($out) yield trim($out) . "\n"; + // Bail on critical failures + if (str_contains($label, 'install') && !str_contains($out ?? '', 'nginx')) { + $chk = self::remoteExec('which nginx 2>/dev/null'); + if (!trim($chk)) { yield "ERROR: nginx install failed\n"; return; } + } + } + yield "✓ Nginx proxy setup complete on {$r['host']}\n"; + } + + public static function uninstall(bool $removeNginx = false): string { + if (self::isRemote()) { + // Remove all NovaCPX proxy configs from remote + self::remoteExec('rm -f /etc/nginx/sites-available/novacpx-proxy-*.conf /etc/nginx/sites-enabled/novacpx-proxy-*.conf /etc/nginx/conf.d/novacpx-proxy.conf'); + self::remoteExec('rm -f /etc/nginx/sites-available/novacpx-default.conf /etc/nginx/sites-enabled/novacpx-default.conf'); + if ($removeNginx) { + self::remoteExec('systemctl stop nginx 2>/dev/null; apt-get remove -y nginx nginx-common 2>/dev/null'); + return 'nginx removed from remote VM'; + } + // Just reload to apply config removal + self::remoteExec('nginx -t 2>/dev/null && systemctl reload nginx 2>/dev/null || true'); + return 'proxy configs removed from remote VM'; + } + // Local uninstall + foreach (glob(self::$confDir . '/' . self::$confPrefix . '*.conf') ?: [] as $f) @unlink($f); + foreach (glob(self::$enabledDir . '/' . self::$confPrefix . '*.conf') ?: [] as $f) @unlink($f); + if ($removeNginx) { + shell_exec('systemctl stop nginx 2>/dev/null; apt-get remove -y nginx nginx-common 2>/dev/null'); + return 'nginx removed'; + } + shell_exec('systemctl reload nginx 2>/dev/null'); + return 'proxy configs removed'; + } + + // --- Health check (called from cron / watchdog) --- + + public static function healthCheck(): string { + $db = DB::getInstance(); + $mode = $db->fetchOne("SELECT value FROM settings WHERE `key`='proxy_mode'")['value'] ?? 'disabled'; + if ($mode === 'disabled') return 'disabled'; + if (!self::isRunning()) { + $result = self::sysctl('start'); + novacpx_log('warn', "ProxyManager: nginx was stopped, attempted restart: $result"); + return "restarted: $result"; + } + return 'ok'; + } + // --- Setup Script --- public static function setupScript(): string { diff --git a/panel/public/assets/js/admin.js b/panel/public/assets/js/admin.js index a9a2083..cc0be01 100644 --- a/panel/public/assets/js/admin.js +++ b/panel/public/assets/js/admin.js @@ -2610,11 +2610,18 @@ async function nginxProxyPage() { Setup Guide + ${isRemote && cfg.remote_host ? ` + + ` : ''} ${inst ? ` + ` : ''} @@ -2842,6 +2849,48 @@ window.proxySetupInstructions = async () => { `, null, { cancelLabel: 'Close', showConfirm: false }); }; +window.proxyRunSetup = () => { + const ov = Nova.modal('Setting Up Remote Nginx Proxy', ` +

Running setup on the remote proxy VM — this takes about 30 seconds.

+
Connecting…\n
+ `, null, { cancelLabel: 'Close', showConfirm: false }); + + const log = document.getElementById('proxy-setup-log'); + const es = new EventSource('/api/proxy/setup-remote'); + let done = false; + + es.onmessage = (e) => { + try { + const d = JSON.parse(e.data); + if (d.line) { log.textContent += d.line; log.scrollTop = log.scrollHeight; } + if (d.done) { done = true; es.close(); log.textContent += '\n— Done. Refreshing status…\n'; setTimeout(() => Nova.loadPage('nginx-proxy', window._novaPages), 1200); } + } catch {} + }; + es.onerror = () => { + if (!done) { + es.close(); + log.textContent += '\n— Connection lost. Check remote host settings and try again.\n'; + } + }; + // Close SSE when modal is dismissed + ov.querySelector('.modal-close')?.addEventListener('click', () => es.close()); +}; + +window.proxyUninstall = () => { + Nova.modal('Uninstall Nginx Proxy', ` +

Choose what to remove from the remote proxy VM:

+
+
+ +
+ `, async () => { + const full = document.querySelector('input[name="uninst"]:checked')?.value === 'full'; + const r = await Nova.api('proxy', 'uninstall', { method: 'DELETE', body: { remove_nginx: full } }); + Nova.toast(r?.data?.result || r?.message || 'Done', r?.success ? 'success' : 'error'); + if (r?.success) Nova.loadPage('nginx-proxy', window._novaPages); + }, { confirmLabel: 'Uninstall', danger: true }); +}; + window.proxySettings = async () => { const r = await Nova.api('proxy', 'settings'); const cfg = r?.data || {};