mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
Proxy: setup progress stream, self-healing, uninstall, health check cron
- ProxyManager::runSetupOnRemote() — generator yields step-by-step progress; drives SSE stream from /api/proxy/setup-remote POST - ProxyManager::uninstall(bool) — removes configs from remote or local; optionally apt-get removes nginx and sets mode=disabled - ProxyManager::healthCheck() — called every 5 min from collect-stats.php; restarts nginx on remote if found stopped - proxy.php: POST /api/proxy/setup-remote (SSE stream), DELETE /api/proxy/uninstall - admin.js: proxyRunSetup() streams output to a live log modal; proxyUninstall() with configs-only vs full removal choice; 'Run Setup on Remote VM' / 'Uninstall' buttons in page header Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -2610,11 +2610,18 @@ async function nginxProxyPage() {
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
|
||||
Setup Guide
|
||||
</button>
|
||||
${isRemote && cfg.remote_host ? `
|
||||
<button class="btn btn-ghost btn-sm" onclick="proxyRunSetup()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
||||
${inst ? 'Re-run Setup' : 'Run Setup on Remote VM'}
|
||||
</button>
|
||||
` : ''}
|
||||
${inst ? `
|
||||
<button class="btn btn-sm btn-secondary" onclick="proxySync()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
|
||||
Sync Accounts
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger btn-ghost" onclick="proxyUninstall()" style="margin-left:0.25rem">Uninstall</button>
|
||||
<button class="btn btn-sm btn-primary" onclick="proxyAddHost()">+ Add Host</button>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -2842,6 +2849,48 @@ window.proxySetupInstructions = async () => {
|
||||
`, null, { cancelLabel: 'Close', showConfirm: false });
|
||||
};
|
||||
|
||||
window.proxyRunSetup = () => {
|
||||
const ov = Nova.modal('Setting Up Remote Nginx Proxy', `
|
||||
<p style="color:var(--text-muted);margin-bottom:0.75rem">Running setup on the remote proxy VM — this takes about 30 seconds.</p>
|
||||
<pre id="proxy-setup-log" style="background:var(--bg-secondary);padding:0.75rem;border-radius:6px;font-size:0.78rem;max-height:320px;overflow-y:auto;white-space:pre-wrap;font-family:monospace">Connecting…\n</pre>
|
||||
`, 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', `
|
||||
<p>Choose what to remove from the <strong>remote proxy VM</strong>:</p>
|
||||
<div class="form-group" style="margin-top:1rem">
|
||||
<label><input type="radio" name="uninst" value="configs" checked> Remove proxy host configs only <small class="text-muted">(keep nginx running)</small></label><br>
|
||||
<label style="margin-top:0.5rem"><input type="radio" name="uninst" value="full"> Remove everything <small class="text-muted">(uninstall nginx, delete all configs, disable proxy mode)</small></label>
|
||||
</div>
|
||||
`, 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 || {};
|
||||
|
||||
Reference in New Issue
Block a user