mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
Fix accounts list display, OS update terminal modal
- Fix accounts list always showing empty: Response::paginate() returns data as res.data (array), not res.data.accounts — fix all 9 call sites in admin.js - Replace blocking apply-os-update with background job + terminal modal: start-os-update runs apt-get as nohup subprocess with sudo, writes to /tmp log file; os-update-status polls log and done-file; admin.js shows scrolling terminal modal that auto-closes when complete - Fix OS update: was running apt-get without sudo (www-data lacks root) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -111,76 +111,73 @@ match ($action) {
|
||||
]);
|
||||
})(),
|
||||
|
||||
// ── Apply OS update ───────────────────────────────────────────────────────
|
||||
// ── Start OS update (background job) ─────────────────────────────────────
|
||||
'apply-os-update' => (function() use ($db) {
|
||||
Auth::getInstance()->require('admin');
|
||||
set_time_limit(300);
|
||||
|
||||
$panelPorts = [PORT_USER, PORT_RESELLER, PORT_ADMIN];
|
||||
$jobId = bin2hex(random_bytes(8));
|
||||
$logFile = "/tmp/ncpx-os-update-{$jobId}.log";
|
||||
$doneFile = "/tmp/ncpx-os-update-{$jobId}.done";
|
||||
$script = "/tmp/ncpx-os-update-{$jobId}.sh";
|
||||
$webSvc = defined('WEB_SERVER') && WEB_SERVER === 'nginx' ? 'nginx' : 'apache2';
|
||||
|
||||
// Snapshot service states before upgrade
|
||||
$beforeServices = [];
|
||||
foreach ([$webSvc, 'mysql', 'postfix', 'dovecot', 'proftpd', 'named'] as $svc) {
|
||||
$beforeServices[$svc] = trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: 'unknown');
|
||||
}
|
||||
|
||||
// Backup panel web root
|
||||
$backupDir = '/var/novacpx/backups/pre-os-update-' . date('YmdHis');
|
||||
$webRoot = defined('WEB_ROOT') ? WEB_ROOT : '/srv/novacpx/public';
|
||||
shell_exec("mkdir -p " . escapeshellarg($backupDir));
|
||||
shell_exec("cp -a " . escapeshellarg($webRoot) . " " . escapeshellarg("$backupDir/public") . " 2>&1");
|
||||
$backupDir = '/var/novacpx/backups/pre-os-update-' . date('YmdHis');
|
||||
|
||||
// Run upgrade (non-interactive, hold back kernel packages to avoid reboot surprise)
|
||||
$env = 'DEBIAN_FRONTEND=noninteractive';
|
||||
$opts = '-o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold"';
|
||||
$out = shell_exec("$env apt-get upgrade -y -q $opts 2>&1");
|
||||
$sh = <<<BASH
|
||||
#!/bin/bash
|
||||
exec > {$logFile} 2>&1
|
||||
echo "[$(date -u +%H:%M:%S UTC)] Preparing backup..."
|
||||
mkdir -p {$backupDir}
|
||||
cp -a {$webRoot} {$backupDir}/public 2>/dev/null
|
||||
echo "[$(date -u +%H:%M:%S UTC)] Updating package lists..."
|
||||
sudo apt-get update -q
|
||||
echo "[$(date -u +%H:%M:%S UTC)] Running upgrade (non-interactive)..."
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get upgrade -y \\
|
||||
-o Dpkg::Options::="--force-confdef" \\
|
||||
-o Dpkg::Options::="--force-confold"
|
||||
UPGRADE_EXIT=\$?
|
||||
echo "[$(date -u +%H:%M:%S UTC)] Checking services..."
|
||||
for SVC in {$webSvc} mysql postfix dovecot; do
|
||||
if systemctl is-active --quiet \$SVC 2>/dev/null; then :; else
|
||||
echo "[$(date -u +%H:%M:%S UTC)] Restarting \$SVC..."
|
||||
sudo systemctl restart \$SVC 2>/dev/null && echo " \$SVC restarted OK" || echo " \$SVC restart FAILED"
|
||||
fi
|
||||
done
|
||||
if [ \$UPGRADE_EXIT -eq 0 ]; then
|
||||
echo "[$(date -u +%H:%M:%S UTC)] Upgrade complete."
|
||||
else
|
||||
echo "[$(date -u +%H:%M:%S UTC)] Upgrade finished with errors (exit code \$UPGRADE_EXIT)."
|
||||
fi
|
||||
echo \$UPGRADE_EXIT > {$doneFile}
|
||||
BASH;
|
||||
file_put_contents($script, $sh);
|
||||
chmod($script, 0755);
|
||||
shell_exec("nohup " . escapeshellarg($script) . " > /dev/null 2>&1 &");
|
||||
|
||||
// Self-healing: restart any service that went down
|
||||
$healed = [];
|
||||
sleep(3);
|
||||
foreach ($beforeServices as $svc => $wasBefore) {
|
||||
if ($wasBefore !== 'active') continue;
|
||||
$nowState = trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: '');
|
||||
if ($nowState !== 'active') {
|
||||
shell_exec("sudo systemctl restart $svc 2>/dev/null");
|
||||
sleep(2);
|
||||
$afterHeal = trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: '');
|
||||
$healed[$svc] = $afterHeal === 'active' ? 'restarted' : 'FAILED';
|
||||
if ($afterHeal !== 'active') {
|
||||
novacpx_log('error', "Self-heal FAILED for $svc after OS upgrade");
|
||||
}
|
||||
}
|
||||
audit('system.os-update-start', $jobId);
|
||||
Response::success(['job_id' => $jobId]);
|
||||
})(),
|
||||
|
||||
// ── Poll OS update job status ─────────────────────────────────────────────
|
||||
'os-update-status' => (function() {
|
||||
Auth::getInstance()->require('admin');
|
||||
$jobId = preg_replace('/[^a-f0-9]/', '', $_GET['job_id'] ?? '');
|
||||
if (!$jobId) Response::error('job_id required');
|
||||
|
||||
$logFile = "/tmp/ncpx-os-update-{$jobId}.log";
|
||||
$doneFile = "/tmp/ncpx-os-update-{$jobId}.done";
|
||||
|
||||
$content = @file_get_contents($logFile) ?: '';
|
||||
$lines = $content !== '' ? explode("\n", rtrim($content)) : [];
|
||||
$done = file_exists($doneFile);
|
||||
$exitCode = $done ? (int)trim(@file_get_contents($doneFile) ?: '1') : null;
|
||||
|
||||
if ($done) {
|
||||
@unlink($logFile);
|
||||
@unlink($doneFile);
|
||||
@unlink("/tmp/ncpx-os-update-{$jobId}.sh");
|
||||
}
|
||||
|
||||
// Verify panel ports respond
|
||||
$panelOk = [];
|
||||
foreach ($panelPorts as $port) {
|
||||
$proto = in_array($port, [PORT_ADMIN, PORT_RESELLER, PORT_USER]) ? 'https' : 'http';
|
||||
$ch = curl_init("{$proto}://127.0.0.1:{$port}/");
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_TIMEOUT => 5]);
|
||||
curl_exec($ch);
|
||||
$panelOk[$port] = curl_getinfo($ch, CURLINFO_HTTP_CODE) > 0;
|
||||
curl_close($ch);
|
||||
}
|
||||
$panelDown = array_keys(array_filter($panelOk, fn($ok) => !$ok));
|
||||
|
||||
// If panel ports down, restore from backup and restart web server
|
||||
if ($panelDown) {
|
||||
shell_exec("cp -a " . escapeshellarg("$backupDir/public") . " " . escapeshellarg($webRoot) . " 2>&1");
|
||||
shell_exec("sudo systemctl restart $webSvc 2>/dev/null");
|
||||
novacpx_log('error', 'Panel ports down after OS upgrade — restored from backup');
|
||||
}
|
||||
|
||||
audit('system.os-update', "upgraded; healed:" . implode(',', array_keys($healed)));
|
||||
Response::success([
|
||||
'upgraded' => true,
|
||||
'panel_ports_ok' => empty($panelDown),
|
||||
'panel_ports_down' => $panelDown,
|
||||
'services_healed' => $healed,
|
||||
'backup_path' => $backupDir,
|
||||
'upgrade_output' => substr($out ?: '', -2000),
|
||||
]);
|
||||
Response::success(['lines' => $lines, 'done' => $done, 'exit_code' => $exitCode]);
|
||||
})(),
|
||||
|
||||
// ── Check NovaCPX update ─────────────────────────────────────────────────
|
||||
|
||||
+50
-23
@@ -724,7 +724,7 @@
|
||||
// ── Accounts ───────────────────────────────────────────────────────────────
|
||||
async function accounts() {
|
||||
const res = await Nova.api('accounts', 'list');
|
||||
const accts = res?.data?.accounts || [];
|
||||
const accts = res?.data || [];
|
||||
window._adminAccts = accts;
|
||||
return `
|
||||
<div class="card">
|
||||
@@ -766,7 +766,7 @@
|
||||
window.adminSearchAccounts = async (q) => {
|
||||
const res = await Nova.api('accounts', 'list', { params: q ? { search: q } : {}});
|
||||
const el = document.getElementById('admin-acct-table');
|
||||
if (el) el.innerHTML = renderAccountTable(res?.data?.accounts || []);
|
||||
if (el) el.innerHTML = renderAccountTable(res?.data || []);
|
||||
};
|
||||
window.adminSuspend = async (id, user) => {
|
||||
Nova.confirm(`Suspend ${user}?`, async () => {
|
||||
@@ -843,7 +843,7 @@
|
||||
// ── Resellers ──────────────────────────────────────────────────────────────
|
||||
async function resellers() {
|
||||
const res = await Nova.api('accounts', 'list', { params:{ role: 'reseller' }});
|
||||
const rows = res?.data?.accounts || [];
|
||||
const rows = res?.data || [];
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
@@ -1101,7 +1101,7 @@
|
||||
Nova.toast('Queuing SSL for all domains without certificates…','info',6000);
|
||||
const accts = await Nova.api('accounts','list',{params:{limit:1000}});
|
||||
let count = 0;
|
||||
for (const a of (accts?.data?.accounts || [])) {
|
||||
for (const a of (accts?.data || [])) {
|
||||
await Nova.api('ssl','issue',{method:'POST',body:{domain:a.domain}});
|
||||
count++;
|
||||
}
|
||||
@@ -1881,21 +1881,48 @@ ${dbs.map(d=>`<tr>
|
||||
};
|
||||
|
||||
window.applyOSUpdate = async () => {
|
||||
Nova.confirm('Apply OS package upgrades? Services will be automatically restarted if needed. The NovaCPX panel will self-restore from backup if any ports go down.', async () => {
|
||||
Nova.loading('Running OS upgrade — this may take a few minutes…');
|
||||
const res = await Nova.api('system', 'apply-os-update', { method: 'POST', timeout: 120000 });
|
||||
Nova.loadingDone();
|
||||
if (res?.data) {
|
||||
const d = res.data;
|
||||
const healed = Object.entries(d.services_healed || {}).map(([s,r]) => `${s}: ${r}`).join(', ');
|
||||
let msg = 'OS upgrade complete.';
|
||||
if (healed) msg += ` Auto-healed: ${healed}.`;
|
||||
if (!d.panel_ports_ok) msg += ' ⚠ Panel ports were down — auto-restored from backup.';
|
||||
Nova.toast(msg, d.panel_ports_ok ? 'success' : 'warning', 10000);
|
||||
Nova.loadPage('updates', pages);
|
||||
} else {
|
||||
Nova.toast(res?.error || 'Upgrade failed', 'error', 8000);
|
||||
Nova.confirm('Apply OS package upgrades? Services will be automatically restarted if needed.', async () => {
|
||||
const startRes = await Nova.api('system', 'apply-os-update', { method: 'POST' });
|
||||
if (!startRes?.success) {
|
||||
Nova.toast(startRes?.message || 'Failed to start upgrade', 'error');
|
||||
return;
|
||||
}
|
||||
const jobId = startRes.data.job_id;
|
||||
|
||||
const ov = Nova.modal('OS Update Progress',
|
||||
`<div id="os-term" style="background:#0d1117;color:#c9d1d9;font-family:monospace;font-size:.8rem;line-height:1.5;padding:1rem;height:340px;overflow-y:auto;border-radius:6px;white-space:pre-wrap;word-break:break-word">Starting upgrade…</div>`,
|
||||
`<span id="os-upd-status" style="color:var(--text-muted);font-size:.85rem">Running…</span>
|
||||
<div style="flex:1"></div>
|
||||
<button class="btn btn-ghost" id="os-close-btn" disabled onclick="this.closest('.modal-overlay').remove()">Close</button>`
|
||||
);
|
||||
|
||||
const term = document.getElementById('os-term');
|
||||
const statusEl = document.getElementById('os-upd-status');
|
||||
const closeBtn = document.getElementById('os-close-btn');
|
||||
|
||||
const poll = async () => {
|
||||
const r = await Nova.api('system', 'os-update-status', { params: { job_id: jobId } });
|
||||
if (!r?.success) {
|
||||
if (term) term.textContent += '\n[Error reading job status]';
|
||||
if (statusEl) statusEl.textContent = 'Error';
|
||||
if (closeBtn) closeBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (term) {
|
||||
term.textContent = (r.data.lines || []).join('\n') || 'Waiting for output…';
|
||||
term.scrollTop = term.scrollHeight;
|
||||
}
|
||||
if (r.data.done) {
|
||||
const ok = r.data.exit_code === 0;
|
||||
if (statusEl) { statusEl.textContent = ok ? 'Complete' : `Failed (exit ${r.data.exit_code})`; statusEl.style.color = ok ? 'var(--success,#22c55e)' : 'var(--error,#ef4444)'; }
|
||||
if (closeBtn) closeBtn.disabled = false;
|
||||
Nova.toast(ok ? 'OS upgrade complete' : 'OS upgrade finished with errors — see log', ok ? 'success' : 'error', 8000);
|
||||
} else {
|
||||
setTimeout(poll, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(poll, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1965,7 +1992,7 @@ async function wordpressPage() {
|
||||
Nova.api('accounts','list',{params:{limit:500}}),
|
||||
Nova.api('wordpress','list'),
|
||||
]);
|
||||
const accts = acctRes?.data?.accounts || [];
|
||||
const accts = acctRes?.data || [];
|
||||
const installs = wpRes?.data?.installs || [];
|
||||
window._adminAcctsWP = accts;
|
||||
|
||||
@@ -2090,7 +2117,7 @@ async function backupsFull() {
|
||||
Nova.api('accounts','list',{params:{limit:500}}),
|
||||
Nova.api('backup','list'),
|
||||
]);
|
||||
const accts = acctRes?.data?.accounts || [];
|
||||
const accts = acctRes?.data || [];
|
||||
const backupList = bkRes?.data?.backups || [];
|
||||
const diskUsed = bkRes?.data?.disk_used || 0;
|
||||
window._adminAcctsBK = accts;
|
||||
@@ -2264,7 +2291,7 @@ window.bkSaveScheduleFor = async (id) => {
|
||||
// ── Cloudflare Integration (#16) ──────────────────────────────────────────
|
||||
async function cloudflarePage() {
|
||||
const acctRes = await Nova.api('accounts','list',{params:{limit:500}});
|
||||
const accts = acctRes?.data?.accounts || [];
|
||||
const accts = acctRes?.data || [];
|
||||
window._adminAcctsCF = accts;
|
||||
|
||||
return `
|
||||
@@ -2413,7 +2440,7 @@ window.cfPurge = async (zoneId, acctId) => {
|
||||
// ── TOTP / 2FA Admin (#17) ────────────────────────────────────────────────
|
||||
async function twofaPage() {
|
||||
const res = await Nova.api('accounts','list',{params:{limit:500}});
|
||||
const users = res?.data?.accounts || [];
|
||||
const users = res?.data || [];
|
||||
return `
|
||||
<div class="page-header mb-3">
|
||||
<h2 class="page-title">Two-Factor Authentication</h2>
|
||||
@@ -2934,7 +2961,7 @@ ${stacks.map(s=>`<tr>
|
||||
|
||||
} else if (tab === 'quotas') {
|
||||
const r = await Nova.api('accounts', 'list', { params: { limit: 200 } });
|
||||
const users = r?.data?.accounts || [];
|
||||
const users = r?.data || [];
|
||||
tc.innerHTML = `
|
||||
<p class="text-muted" style="margin-bottom:1rem">Set Docker resource limits per user. Click a row to edit.</p>
|
||||
<div style="overflow-x:auto"><table class="table"><thead><tr><th>Username</th><th>Max Containers</th><th>Max Memory</th><th>Max CPUs</th><th>Actions</th></tr></thead><tbody>
|
||||
|
||||
@@ -724,7 +724,7 @@
|
||||
// ── Accounts ───────────────────────────────────────────────────────────────
|
||||
async function accounts() {
|
||||
const res = await Nova.api('accounts', 'list');
|
||||
const accts = res?.data?.accounts || [];
|
||||
const accts = res?.data || [];
|
||||
window._adminAccts = accts;
|
||||
return `
|
||||
<div class="card">
|
||||
@@ -766,7 +766,7 @@
|
||||
window.adminSearchAccounts = async (q) => {
|
||||
const res = await Nova.api('accounts', 'list', { params: q ? { search: q } : {}});
|
||||
const el = document.getElementById('admin-acct-table');
|
||||
if (el) el.innerHTML = renderAccountTable(res?.data?.accounts || []);
|
||||
if (el) el.innerHTML = renderAccountTable(res?.data || []);
|
||||
};
|
||||
window.adminSuspend = async (id, user) => {
|
||||
Nova.confirm(`Suspend ${user}?`, async () => {
|
||||
@@ -843,7 +843,7 @@
|
||||
// ── Resellers ──────────────────────────────────────────────────────────────
|
||||
async function resellers() {
|
||||
const res = await Nova.api('accounts', 'list', { params:{ role: 'reseller' }});
|
||||
const rows = res?.data?.accounts || [];
|
||||
const rows = res?.data || [];
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
@@ -1101,7 +1101,7 @@
|
||||
Nova.toast('Queuing SSL for all domains without certificates…','info',6000);
|
||||
const accts = await Nova.api('accounts','list',{params:{limit:1000}});
|
||||
let count = 0;
|
||||
for (const a of (accts?.data?.accounts || [])) {
|
||||
for (const a of (accts?.data || [])) {
|
||||
await Nova.api('ssl','issue',{method:'POST',body:{domain:a.domain}});
|
||||
count++;
|
||||
}
|
||||
@@ -1857,33 +1857,72 @@ ${dbs.map(d=>`<tr>
|
||||
Nova.loading('Pulling NovaCPX update from GitHub…');
|
||||
const res = await Nova.api('system', 'apply-novacpx-update', { method: 'POST' });
|
||||
Nova.loadingDone();
|
||||
if (res?.data?.updated) {
|
||||
Nova.toast(`Updated to ${res.data.to_commit}`, 'success', 6000);
|
||||
setTimeout(() => Nova.loadPage('updates', pages), 2000);
|
||||
} else if (res?.error) {
|
||||
Nova.toast(res.error, 'error', 8000);
|
||||
const d = res?.data;
|
||||
if (!res?.success) {
|
||||
Nova.modal('Update Failed', `<p class="text-danger">${Nova.escHtml(res?.message || 'Unknown error')}</p>`);
|
||||
return;
|
||||
}
|
||||
if (d?.updated) {
|
||||
const steps = (d.steps || []).map(s => `<div>${Nova.escHtml(s)}</div>`).join('');
|
||||
Nova.modal('Update Complete',
|
||||
`<p><strong>Updated:</strong> <code>${Nova.escHtml(d.from_commit)}</code> → <code>${Nova.escHtml(d.to_commit)}</code></p>
|
||||
${steps ? `<div class="terminal mt-2" style="max-height:200px;overflow-y:auto;font-size:.78rem">${steps}</div>` : ''}
|
||||
<p class="text-muted mt-2 text-sm">Backup saved to: <code>${Nova.escHtml(d.backup_path || '')}</code></p>`,
|
||||
`<button class="btn btn-primary" onclick="this.closest('.modal-overlay').remove();adminPage('updates')">OK</button>`
|
||||
);
|
||||
} else {
|
||||
Nova.toast('Already up to date.', 'info');
|
||||
Nova.modal('Already Up To Date',
|
||||
`<p>NovaCPX is already at the latest commit: <code>${Nova.escHtml(d?.to_commit || '—')}</code></p>
|
||||
${d?.pull_output ? `<div class="terminal mt-2" style="font-size:.78rem">${Nova.escHtml(d.pull_output)}</div>` : ''}`,
|
||||
`<button class="btn btn-primary" onclick="this.closest('.modal-overlay').remove();adminPage('updates')">OK</button>`
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.applyOSUpdate = async () => {
|
||||
Nova.confirm('Apply OS package upgrades? Services will be automatically restarted if needed. The NovaCPX panel will self-restore from backup if any ports go down.', async () => {
|
||||
Nova.loading('Running OS upgrade — this may take a few minutes…');
|
||||
const res = await Nova.api('system', 'apply-os-update', { method: 'POST', timeout: 120000 });
|
||||
Nova.loadingDone();
|
||||
if (res?.data) {
|
||||
const d = res.data;
|
||||
const healed = Object.entries(d.services_healed || {}).map(([s,r]) => `${s}: ${r}`).join(', ');
|
||||
let msg = 'OS upgrade complete.';
|
||||
if (healed) msg += ` Auto-healed: ${healed}.`;
|
||||
if (!d.panel_ports_ok) msg += ' ⚠ Panel ports were down — auto-restored from backup.';
|
||||
Nova.toast(msg, d.panel_ports_ok ? 'success' : 'warning', 10000);
|
||||
Nova.loadPage('updates', pages);
|
||||
} else {
|
||||
Nova.toast(res?.error || 'Upgrade failed', 'error', 8000);
|
||||
Nova.confirm('Apply OS package upgrades? Services will be automatically restarted if needed.', async () => {
|
||||
const startRes = await Nova.api('system', 'apply-os-update', { method: 'POST' });
|
||||
if (!startRes?.success) {
|
||||
Nova.toast(startRes?.message || 'Failed to start upgrade', 'error');
|
||||
return;
|
||||
}
|
||||
const jobId = startRes.data.job_id;
|
||||
|
||||
const ov = Nova.modal('OS Update Progress',
|
||||
`<div id="os-term" style="background:#0d1117;color:#c9d1d9;font-family:monospace;font-size:.8rem;line-height:1.5;padding:1rem;height:340px;overflow-y:auto;border-radius:6px;white-space:pre-wrap;word-break:break-word">Starting upgrade…</div>`,
|
||||
`<span id="os-upd-status" style="color:var(--text-muted);font-size:.85rem">Running…</span>
|
||||
<div style="flex:1"></div>
|
||||
<button class="btn btn-ghost" id="os-close-btn" disabled onclick="this.closest('.modal-overlay').remove()">Close</button>`
|
||||
);
|
||||
|
||||
const term = document.getElementById('os-term');
|
||||
const statusEl = document.getElementById('os-upd-status');
|
||||
const closeBtn = document.getElementById('os-close-btn');
|
||||
|
||||
const poll = async () => {
|
||||
const r = await Nova.api('system', 'os-update-status', { params: { job_id: jobId } });
|
||||
if (!r?.success) {
|
||||
if (term) term.textContent += '\n[Error reading job status]';
|
||||
if (statusEl) statusEl.textContent = 'Error';
|
||||
if (closeBtn) closeBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
if (term) {
|
||||
term.textContent = (r.data.lines || []).join('\n') || 'Waiting for output…';
|
||||
term.scrollTop = term.scrollHeight;
|
||||
}
|
||||
if (r.data.done) {
|
||||
const ok = r.data.exit_code === 0;
|
||||
if (statusEl) { statusEl.textContent = ok ? 'Complete' : `Failed (exit ${r.data.exit_code})`; statusEl.style.color = ok ? 'var(--success,#22c55e)' : 'var(--error,#ef4444)'; }
|
||||
if (closeBtn) closeBtn.disabled = false;
|
||||
Nova.toast(ok ? 'OS upgrade complete' : 'OS upgrade finished with errors — see log', ok ? 'success' : 'error', 8000);
|
||||
} else {
|
||||
setTimeout(poll, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(poll, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1953,7 +1992,7 @@ async function wordpressPage() {
|
||||
Nova.api('accounts','list',{params:{limit:500}}),
|
||||
Nova.api('wordpress','list'),
|
||||
]);
|
||||
const accts = acctRes?.data?.accounts || [];
|
||||
const accts = acctRes?.data || [];
|
||||
const installs = wpRes?.data?.installs || [];
|
||||
window._adminAcctsWP = accts;
|
||||
|
||||
@@ -2078,7 +2117,7 @@ async function backupsFull() {
|
||||
Nova.api('accounts','list',{params:{limit:500}}),
|
||||
Nova.api('backup','list'),
|
||||
]);
|
||||
const accts = acctRes?.data?.accounts || [];
|
||||
const accts = acctRes?.data || [];
|
||||
const backupList = bkRes?.data?.backups || [];
|
||||
const diskUsed = bkRes?.data?.disk_used || 0;
|
||||
window._adminAcctsBK = accts;
|
||||
@@ -2252,7 +2291,7 @@ window.bkSaveScheduleFor = async (id) => {
|
||||
// ── Cloudflare Integration (#16) ──────────────────────────────────────────
|
||||
async function cloudflarePage() {
|
||||
const acctRes = await Nova.api('accounts','list',{params:{limit:500}});
|
||||
const accts = acctRes?.data?.accounts || [];
|
||||
const accts = acctRes?.data || [];
|
||||
window._adminAcctsCF = accts;
|
||||
|
||||
return `
|
||||
@@ -2401,7 +2440,7 @@ window.cfPurge = async (zoneId, acctId) => {
|
||||
// ── TOTP / 2FA Admin (#17) ────────────────────────────────────────────────
|
||||
async function twofaPage() {
|
||||
const res = await Nova.api('accounts','list',{params:{limit:500}});
|
||||
const users = res?.data?.accounts || [];
|
||||
const users = res?.data || [];
|
||||
return `
|
||||
<div class="page-header mb-3">
|
||||
<h2 class="page-title">Two-Factor Authentication</h2>
|
||||
@@ -2922,7 +2961,7 @@ ${stacks.map(s=>`<tr>
|
||||
|
||||
} else if (tab === 'quotas') {
|
||||
const r = await Nova.api('accounts', 'list', { params: { limit: 200 } });
|
||||
const users = r?.data?.accounts || [];
|
||||
const users = r?.data || [];
|
||||
tc.innerHTML = `
|
||||
<p class="text-muted" style="margin-bottom:1rem">Set Docker resource limits per user. Click a row to edit.</p>
|
||||
<div style="overflow-x:auto"><table class="table"><thead><tr><th>Username</th><th>Max Containers</th><th>Max Memory</th><th>Max CPUs</th><th>Actions</th></tr></thead><tbody>
|
||||
@@ -3073,11 +3112,12 @@ async function serverOptions() {
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Web Server</span>${Nova.badge(opts.web_server||'apache','green')}</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted" style="font-size:.85rem;margin-bottom:1rem">Current web server for hosting accounts. Changing requires migration of all vhosts.</p>
|
||||
<p class="text-muted" style="font-size:.85rem;margin-bottom:.5rem">Controls which server handles customer sites on ports 80/443. The panel itself always runs on Apache (ports 8880–8883) regardless of this setting.</p>
|
||||
<p class="text-muted" style="font-size:.85rem;margin-bottom:1rem">Running status — Apache: ${opts.apache_active ? Nova.badge('active','green') : Nova.badge('inactive','red')} Nginx: ${opts.nginx_active ? Nova.badge('active','green') : Nova.badge('inactive','red')}</p>
|
||||
<div class="form-group">
|
||||
<label>Active Web Server</label>
|
||||
<label>Customer Hosting Web Server</label>
|
||||
<select id="so-web" class="form-control">
|
||||
${['apache','nginx','openlitespeed','caddy'].map(s=>`<option value="${s}" ${s===(opts.web_server||'apache')?'selected':''}>${s.charAt(0).toUpperCase()+s.slice(1)}</option>`).join('')}
|
||||
${['apache','nginx'].map(s=>`<option value="${s}" ${s===(opts.web_server||'apache')?'selected':''}>${s.charAt(0).toUpperCase()+s.slice(1)}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="soSave('web_server','so-web','Web server')">Save & Switch</button>
|
||||
@@ -3198,10 +3238,12 @@ async function serverOptions() {
|
||||
window.soSave = async (key, inputId, label) => {
|
||||
const val = document.getElementById(inputId)?.value;
|
||||
if (!val) return;
|
||||
Nova.confirm(`Switch ${label} to "${val}"? This will run install/migration scripts on the server.`, async () => {
|
||||
Nova.confirm(`Switch ${label} to "${val}"? This will stop the current service and start the new one.`, async () => {
|
||||
Nova.loading(`Switching ${label} to ${val}…`);
|
||||
const r = await Nova.api('system', 'save-option', { method:'POST', body:{ key, value: val } });
|
||||
Nova.toast(r?.success ? `${label} updated` : (r?.message||'Failed'), r?.success?'success':'error');
|
||||
if (r?.success) Nova.loadPage('server-options', window._novaPages);
|
||||
Nova.loadingDone();
|
||||
Nova.toast(r?.success ? `${label} switched to ${val}` : (r?.message || 'Failed'), r?.success ? 'success' : 'error');
|
||||
if (r?.success) adminPage('server-options');
|
||||
}, true);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user