mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
Fix service controls, loading overlay, DB engine awareness
- system.php: add sudo to all systemctl/apt-get calls (www-data runs as non-root) - system.php: flush command for postfix uses postqueue -f - system.php: save-option writes web_server to config.ini so VhostManager picks it up - databases.php: list endpoint supports admin (no account_id), defaults db type to active_db_engine setting - nova.js: add Nova.loading() / Nova.loadingDone() spinner overlay - admin.js: adminServiceAction shows loading overlay + optimistic badge update - admin.js: phpInstallVersion, dbEngineAction, docker install, OS/NovaCPX update all show loading overlay - WordPressManager.php: fix Database::getInstance() -> DB::getInstance()->pdo() - DockerManager.php: fix install to write script file and sudo bash (no interactive terminal)
This commit is contained in:
@@ -13,10 +13,18 @@ if ($user['role'] === 'user') {
|
|||||||
|
|
||||||
match ($action) {
|
match ($action) {
|
||||||
|
|
||||||
'list' => (function() use ($db, $accountId) {
|
'list' => (function() use ($db, $accountId, $user) {
|
||||||
if (!$accountId) Response::error("account_id required");
|
if (!$accountId && $user['role'] !== 'admin') Response::error("account_id required");
|
||||||
$rows = $db->fetchAll("SELECT id, db_name, db_user, db_type, size_mb, created_at FROM `databases` WHERE account_id = ?", [$accountId]);
|
if ($accountId) {
|
||||||
foreach ($rows as &$r) { $r['size_mb'] = DatabaseManager::getSize($r['db_name'], $r['db_type']); }
|
$rows = $db->fetchAll(
|
||||||
|
"SELECT d.id, d.db_name, d.db_user, d.db_type, d.size_mb, d.created_at, a.username
|
||||||
|
FROM `databases` d LEFT JOIN accounts a ON a.id=d.account_id WHERE d.account_id = ?", [$accountId]);
|
||||||
|
} else {
|
||||||
|
$rows = $db->fetchAll(
|
||||||
|
"SELECT d.id, d.db_name, d.db_user, d.db_type, d.size_mb, d.created_at, a.username
|
||||||
|
FROM `databases` d LEFT JOIN accounts a ON a.id=d.account_id ORDER BY d.created_at DESC LIMIT 500");
|
||||||
|
}
|
||||||
|
foreach ($rows as &$r) { $r['size_mb'] = DatabaseManager::getSize($r['db_name'], $r['db_type'] ?? 'mysql'); }
|
||||||
Response::success($rows);
|
Response::success($rows);
|
||||||
})(),
|
})(),
|
||||||
|
|
||||||
@@ -28,7 +36,9 @@ match ($action) {
|
|||||||
$count = (int)$db->fetchOne("SELECT COUNT(*) c FROM `databases` WHERE account_id=?", [$accountId])['c'];
|
$count = (int)$db->fetchOne("SELECT COUNT(*) c FROM `databases` WHERE account_id=?", [$accountId])['c'];
|
||||||
if ($count >= (int)$acctPkg['max_databases']) Response::error("Database limit ({$acctPkg['max_databases']}) reached for this package", 403);
|
if ($count >= (int)$acctPkg['max_databases']) Response::error("Database limit ({$acctPkg['max_databases']}) reached for this package", 403);
|
||||||
}
|
}
|
||||||
$type = $body['type'] ?? 'mysql';
|
// Default to active DB engine from settings so autoinstallers use whatever the admin has selected
|
||||||
|
$activeEngine = $db->fetchOne("SELECT `value` FROM settings WHERE `key`='active_db_engine'")['value'] ?? 'mysql';
|
||||||
|
$type = $body['type'] ?? ($activeEngine === 'postgresql' ? 'postgresql' : 'mysql');
|
||||||
$dbName = trim($body['db_name'] ?? '');
|
$dbName = trim($body['db_name'] ?? '');
|
||||||
$dbUser = trim($body['db_user'] ?? $dbName . '_user');
|
$dbUser = trim($body['db_user'] ?? $dbName . '_user');
|
||||||
$dbPass = $body['db_pass'] ?? bin2hex(random_bytes(8));
|
$dbPass = $body['db_pass'] ?? bin2hex(random_bytes(8));
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ match ($action) {
|
|||||||
if ($wasBefore !== 'active') continue;
|
if ($wasBefore !== 'active') continue;
|
||||||
$nowState = trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: '');
|
$nowState = trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: '');
|
||||||
if ($nowState !== 'active') {
|
if ($nowState !== 'active') {
|
||||||
shell_exec("systemctl restart $svc 2>/dev/null");
|
shell_exec("sudo systemctl restart $svc 2>/dev/null");
|
||||||
sleep(2);
|
sleep(2);
|
||||||
$afterHeal = trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: '');
|
$afterHeal = trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: '');
|
||||||
$healed[$svc] = $afterHeal === 'active' ? 'restarted' : 'FAILED';
|
$healed[$svc] = $afterHeal === 'active' ? 'restarted' : 'FAILED';
|
||||||
@@ -156,16 +156,19 @@ match ($action) {
|
|||||||
// Verify panel ports respond
|
// Verify panel ports respond
|
||||||
$panelOk = [];
|
$panelOk = [];
|
||||||
foreach ($panelPorts as $port) {
|
foreach ($panelPorts as $port) {
|
||||||
$resp = @fsockopen('127.0.0.1', $port, $errno, $errstr, 3);
|
$proto = in_array($port, [PORT_ADMIN, PORT_RESELLER, PORT_USER]) ? 'https' : 'http';
|
||||||
$panelOk[$port] = (bool)$resp;
|
$ch = curl_init("{$proto}://127.0.0.1:{$port}/");
|
||||||
if ($resp) fclose($resp);
|
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));
|
$panelDown = array_keys(array_filter($panelOk, fn($ok) => !$ok));
|
||||||
|
|
||||||
// If panel ports down, restore from backup and restart web server
|
// If panel ports down, restore from backup and restart web server
|
||||||
if ($panelDown) {
|
if ($panelDown) {
|
||||||
shell_exec("cp -a " . escapeshellarg("$backupDir/public") . " " . escapeshellarg($webRoot) . " 2>&1");
|
shell_exec("cp -a " . escapeshellarg("$backupDir/public") . " " . escapeshellarg($webRoot) . " 2>&1");
|
||||||
shell_exec("systemctl restart $webSvc 2>/dev/null");
|
shell_exec("sudo systemctl restart $webSvc 2>/dev/null");
|
||||||
novacpx_log('error', 'Panel ports down after OS upgrade — restored from backup');
|
novacpx_log('error', 'Panel ports down after OS upgrade — restored from backup');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,9 +356,13 @@ match ($action) {
|
|||||||
'proftpd','vsftpd','pure-ftpd','named','bind9','pdns','nsd','fail2ban',
|
'proftpd','vsftpd','pure-ftpd','named','bind9','pdns','nsd','fail2ban',
|
||||||
'php7.4-fpm','php8.1-fpm','php8.2-fpm','php8.3-fpm'];
|
'php7.4-fpm','php8.1-fpm','php8.2-fpm','php8.3-fpm'];
|
||||||
if (!in_array($svc, $allowed)) Response::error("Service not managed: $svc");
|
if (!in_array($svc, $allowed)) Response::error("Service not managed: $svc");
|
||||||
if (!in_array($cmd, ['start','stop','restart','reload','status'])) Response::error("Invalid command");
|
if (!in_array($cmd, ['start','stop','restart','reload','status','flush'])) Response::error("Invalid command");
|
||||||
|
|
||||||
$out = shell_exec("systemctl $cmd " . escapeshellarg($svc) . " 2>&1");
|
if ($cmd === 'flush' && $svc === 'postfix') {
|
||||||
|
$out = shell_exec("sudo postqueue -f 2>&1");
|
||||||
|
} else {
|
||||||
|
$out = shell_exec("sudo systemctl $cmd " . escapeshellarg($svc) . " 2>&1");
|
||||||
|
}
|
||||||
audit("service.$cmd", $svc);
|
audit("service.$cmd", $svc);
|
||||||
Response::success(['output' => $out]);
|
Response::success(['output' => $out]);
|
||||||
})(),
|
})(),
|
||||||
@@ -416,23 +423,33 @@ match ($action) {
|
|||||||
// Save before switching so the new value is in DB
|
// Save before switching so the new value is in DB
|
||||||
$db->execute("INSERT INTO settings (`key`,`value`) VALUES (?,?) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)", [$key, $value]);
|
$db->execute("INSERT INTO settings (`key`,`value`) VALUES (?,?) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)", [$key, $value]);
|
||||||
|
|
||||||
|
// Sync config.ini so PHP constants reflect the change immediately on next request
|
||||||
|
$configFile = '/etc/novacpx/config.ini';
|
||||||
|
if (in_array($key, ['web_server','ftp_server','dns_server']) && file_exists($configFile)) {
|
||||||
|
$ini = file_get_contents($configFile);
|
||||||
|
if ($key === 'web_server') {
|
||||||
|
$ini = preg_replace('/^server\s*=\s*.*/m', "server = $value", $ini);
|
||||||
|
}
|
||||||
|
file_put_contents($configFile, $ini);
|
||||||
|
}
|
||||||
|
|
||||||
// Inline service switching — stop all alternatives, start the chosen one
|
// Inline service switching — stop all alternatives, start the chosen one
|
||||||
if ($key === 'web_server') {
|
if ($key === 'web_server') {
|
||||||
$webSvcs = ['apache2','nginx','lighttpd','caddy'];
|
$webSvcs = ['apache2','nginx','lighttpd','caddy'];
|
||||||
foreach ($webSvcs as $s) { shell_exec("systemctl stop $s 2>/dev/null; systemctl disable $s 2>/dev/null"); }
|
foreach ($webSvcs as $s) { shell_exec("sudo systemctl stop $s 2>/dev/null; sudo systemctl disable $s 2>/dev/null"); }
|
||||||
$startSvc = match($value) { 'nginx' => 'nginx', 'apache' => 'apache2', default => 'apache2' };
|
$startSvc = match($value) { 'nginx' => 'nginx', 'apache' => 'apache2', default => 'apache2' };
|
||||||
shell_exec("systemctl enable $startSvc 2>/dev/null && systemctl start $startSvc 2>/dev/null");
|
shell_exec("sudo systemctl enable $startSvc 2>/dev/null && sudo systemctl start $startSvc 2>/dev/null");
|
||||||
} elseif ($key === 'ftp_server') {
|
} elseif ($key === 'ftp_server') {
|
||||||
foreach (['proftpd','vsftpd','pure-ftpd'] as $s) { shell_exec("systemctl stop $s 2>/dev/null; systemctl disable $s 2>/dev/null"); }
|
foreach (['proftpd','vsftpd','pure-ftpd'] as $s) { shell_exec("sudo systemctl stop $s 2>/dev/null; sudo systemctl disable $s 2>/dev/null"); }
|
||||||
$startSvc = match($value) { 'vsftpd' => 'vsftpd', 'pureftpd' => 'pure-ftpd', default => 'proftpd' };
|
$startSvc = match($value) { 'vsftpd' => 'vsftpd', 'pureftpd' => 'pure-ftpd', default => 'proftpd' };
|
||||||
if (trim(shell_exec("dpkg -l $startSvc 2>/dev/null | grep -c '^ii'") ?: '0') > 0) {
|
if (trim(shell_exec("dpkg -l $startSvc 2>/dev/null | grep -c '^ii'") ?: '0') > 0) {
|
||||||
shell_exec("systemctl enable $startSvc 2>/dev/null && systemctl start $startSvc 2>/dev/null");
|
shell_exec("sudo systemctl enable $startSvc 2>/dev/null && sudo systemctl start $startSvc 2>/dev/null");
|
||||||
}
|
}
|
||||||
} elseif ($key === 'dns_server') {
|
} elseif ($key === 'dns_server') {
|
||||||
foreach (['named','bind9','pdns','nsd'] as $s) { shell_exec("systemctl stop $s 2>/dev/null; systemctl disable $s 2>/dev/null"); }
|
foreach (['named','bind9','pdns','nsd'] as $s) { shell_exec("sudo systemctl stop $s 2>/dev/null; sudo systemctl disable $s 2>/dev/null"); }
|
||||||
if ($value !== 'none') {
|
if ($value !== 'none') {
|
||||||
$startSvc = match($value) { 'powerdns' => 'pdns', 'nsd' => 'nsd', default => 'named' };
|
$startSvc = match($value) { 'powerdns' => 'pdns', 'nsd' => 'nsd', default => 'named' };
|
||||||
shell_exec("systemctl enable $startSvc 2>/dev/null && systemctl start $startSvc 2>/dev/null");
|
shell_exec("sudo systemctl enable $startSvc 2>/dev/null && sudo systemctl start $startSvc 2>/dev/null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// mail_server: postfix + dovecot are always running; mail_server setting controls config template only
|
// mail_server: postfix + dovecot are always running; mail_server setting controls config template only
|
||||||
@@ -560,22 +577,22 @@ match ($action) {
|
|||||||
'mariadb' => 'mariadb-server',
|
'mariadb' => 'mariadb-server',
|
||||||
'postgresql' => 'postgresql postgresql-contrib',
|
'postgresql' => 'postgresql postgresql-contrib',
|
||||||
};
|
};
|
||||||
$out = shell_exec("DEBIAN_FRONTEND=noninteractive apt-get install -y $pkg 2>&1");
|
$out = shell_exec("DEBIAN_FRONTEND=noninteractive sudo apt-get install -y $pkg 2>&1");
|
||||||
shell_exec("systemctl enable $engine && systemctl start $engine 2>/dev/null");
|
shell_exec("sudo systemctl enable $engine 2>/dev/null && sudo systemctl start $engine 2>/dev/null");
|
||||||
} elseif ($action === 'remove') {
|
} elseif ($action === 'remove') {
|
||||||
$pkg = match($engine) {
|
$pkg = match($engine) {
|
||||||
'mysql' => 'mysql-server mysql-client',
|
'mysql' => 'mysql-server mysql-client',
|
||||||
'mariadb' => 'mariadb-server mariadb-client',
|
'mariadb' => 'mariadb-server mariadb-client',
|
||||||
'postgresql' => 'postgresql postgresql-contrib',
|
'postgresql' => 'postgresql postgresql-contrib',
|
||||||
};
|
};
|
||||||
shell_exec("systemctl stop $engine 2>/dev/null || true");
|
shell_exec("sudo systemctl stop $engine 2>/dev/null || true");
|
||||||
$out = shell_exec("apt-get remove -y $pkg 2>&1");
|
$out = shell_exec("DEBIAN_FRONTEND=noninteractive sudo apt-get remove -y $pkg 2>&1");
|
||||||
} elseif ($action === 'set-active') {
|
} elseif ($action === 'set-active') {
|
||||||
$db->execute("INSERT INTO settings (`key`,`value`) VALUES ('active_db_engine',?) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)", [$engine]);
|
$db->execute("INSERT INTO settings (`key`,`value`) VALUES ('active_db_engine',?) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)", [$engine]);
|
||||||
audit('settings.active_db_engine', $engine);
|
audit('settings.active_db_engine', $engine);
|
||||||
Response::success(null, "Active database engine set to $engine");
|
Response::success(null, "Active database engine set to $engine");
|
||||||
} else {
|
} else {
|
||||||
shell_exec("systemctl $action $engine 2>/dev/null");
|
shell_exec("sudo systemctl $action $engine 2>/dev/null");
|
||||||
}
|
}
|
||||||
audit("db-engine.$action", $engine);
|
audit("db-engine.$action", $engine);
|
||||||
Response::success(['output' => substr($out ?: '', -1000)], ucfirst($action) . " $engine done");
|
Response::success(['output' => substr($out ?: '', -1000)], ucfirst($action) . " $engine done");
|
||||||
|
|||||||
@@ -505,8 +505,9 @@
|
|||||||
|
|
||||||
window.phpInstallVersion = (ver) => {
|
window.phpInstallVersion = (ver) => {
|
||||||
Nova.confirm(`Install PHP ${ver}? This will run apt-get and may take a minute.`, async () => {
|
Nova.confirm(`Install PHP ${ver}? This will run apt-get and may take a minute.`, async () => {
|
||||||
Nova.toast(`Installing PHP ${ver}…`, 'info', 15000);
|
Nova.loading(`Installing PHP ${ver}…`);
|
||||||
const r = await Nova.api('php', 'install-version', { method: 'POST', body: { version: ver } });
|
const r = await Nova.api('php', 'install-version', { method: 'POST', body: { version: ver } });
|
||||||
|
Nova.loadingDone();
|
||||||
if (r?.success) { Nova.toast(`PHP ${ver} installed`, 'success'); adminPage('php-manager'); }
|
if (r?.success) { Nova.toast(`PHP ${ver} installed`, 'success'); adminPage('php-manager'); }
|
||||||
else Nova.toast(r?.message || 'Install failed', 'error');
|
else Nova.toast(r?.message || 'Install failed', 'error');
|
||||||
});
|
});
|
||||||
@@ -514,14 +515,18 @@
|
|||||||
|
|
||||||
window.phpRemoveVersion = (ver) => {
|
window.phpRemoveVersion = (ver) => {
|
||||||
Nova.confirm(`Remove PHP ${ver}? All FPM pools for this version will stop.`, async () => {
|
Nova.confirm(`Remove PHP ${ver}? All FPM pools for this version will stop.`, async () => {
|
||||||
|
Nova.loading(`Removing PHP ${ver}…`);
|
||||||
const r = await Nova.api('php', 'remove-version', { method: 'POST', body: { version: ver } });
|
const r = await Nova.api('php', 'remove-version', { method: 'POST', body: { version: ver } });
|
||||||
|
Nova.loadingDone();
|
||||||
if (r?.success) { Nova.toast(`PHP ${ver} removed`, 'success'); adminPage('php-manager'); }
|
if (r?.success) { Nova.toast(`PHP ${ver} removed`, 'success'); adminPage('php-manager'); }
|
||||||
else Nova.toast(r?.message || 'Remove failed', 'error');
|
else Nova.toast(r?.message || 'Remove failed', 'error');
|
||||||
}, true);
|
}, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.phpFpmAction = async (ver, cmd) => {
|
window.phpFpmAction = async (ver, cmd) => {
|
||||||
|
Nova.loading(`${cmd} php${ver}-fpm…`);
|
||||||
const r = await Nova.api('php', 'fpm-action', { method: 'POST', body: { version: ver, command: cmd } });
|
const r = await Nova.api('php', 'fpm-action', { method: 'POST', body: { version: ver, command: cmd } });
|
||||||
|
Nova.loadingDone();
|
||||||
if (r?.success) { Nova.toast(r.message, 'success'); refreshSvcStatus(`php${ver}-fpm`); }
|
if (r?.success) { Nova.toast(r.message, 'success'); refreshSvcStatus(`php${ver}-fpm`); }
|
||||||
else Nova.toast(r?.message || 'Action failed', 'error');
|
else Nova.toast(r?.message || 'Action failed', 'error');
|
||||||
};
|
};
|
||||||
@@ -1748,10 +1753,11 @@ ${dbs.map(d=>`<tr>
|
|||||||
}, true);
|
}, true);
|
||||||
};
|
};
|
||||||
window.dbEngineAction = (engine, action) => {
|
window.dbEngineAction = (engine, action) => {
|
||||||
const labels = {install:`Installing ${engine}…`,remove:`Removing ${engine}…`,start:`Starting…`,stop:`Stopping…`,restart:`Restarting…`};
|
const labels = {install:`Installing ${engine}…`,remove:`Removing ${engine}…`,start:`Starting ${engine}…`,stop:`Stopping ${engine}…`,restart:`Restarting ${engine}…`};
|
||||||
const doIt = async () => {
|
const doIt = async () => {
|
||||||
Nova.toast(labels[action]||'Working…','info');
|
Nova.loading(labels[action] || `Working on ${engine}…`);
|
||||||
const r = await Nova.api('system','db-engine-action',{method:'POST',body:{engine,action}});
|
const r = await Nova.api('system','db-engine-action',{method:'POST',body:{engine,action}});
|
||||||
|
Nova.loadingDone();
|
||||||
Nova.toast(r?.message||(r?.success?'Done':'Failed'), r?.success?'success':'error');
|
Nova.toast(r?.message||(r?.success?'Done':'Failed'), r?.success?'success':'error');
|
||||||
if (r?.success) adminPage('mysql-manager');
|
if (r?.success) adminPage('mysql-manager');
|
||||||
};
|
};
|
||||||
@@ -1848,29 +1854,25 @@ ${dbs.map(d=>`<tr>
|
|||||||
|
|
||||||
window.applyNovaCPXUpdate = async () => {
|
window.applyNovaCPXUpdate = async () => {
|
||||||
Nova.confirm('Apply NovaCPX update? PHP syntax is checked first, and a backup is taken automatically. The panel will self-restore if anything breaks.', async () => {
|
Nova.confirm('Apply NovaCPX update? PHP syntax is checked first, and a backup is taken automatically. The panel will self-restore if anything breaks.', async () => {
|
||||||
const btn = document.getElementById('ncpx-update-btn');
|
Nova.loading('Pulling NovaCPX update from GitHub…');
|
||||||
if (btn) { btn.disabled = true; btn.textContent = 'Updating…'; }
|
|
||||||
Nova.toast('Pulling update from GitHub…', 'info', 12000);
|
|
||||||
const res = await Nova.api('system', 'apply-novacpx-update', { method: 'POST' });
|
const res = await Nova.api('system', 'apply-novacpx-update', { method: 'POST' });
|
||||||
|
Nova.loadingDone();
|
||||||
if (res?.data?.updated) {
|
if (res?.data?.updated) {
|
||||||
Nova.toast(`Updated to ${res.data.to_commit}`, 'success', 6000);
|
Nova.toast(`Updated to ${res.data.to_commit}`, 'success', 6000);
|
||||||
setTimeout(() => Nova.loadPage('updates', pages), 2000);
|
setTimeout(() => Nova.loadPage('updates', pages), 2000);
|
||||||
} else if (res?.error) {
|
} else if (res?.error) {
|
||||||
Nova.toast(res.error, 'error', 8000);
|
Nova.toast(res.error, 'error', 8000);
|
||||||
if (btn) { btn.disabled = false; btn.textContent = 'Update NovaCPX'; }
|
|
||||||
} else {
|
} else {
|
||||||
Nova.toast('Already up to date.', 'info');
|
Nova.toast('Already up to date.', 'info');
|
||||||
if (btn) { btn.disabled = false; btn.textContent = 'Update NovaCPX'; }
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.applyOSUpdate = async () => {
|
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.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 () => {
|
||||||
const btn = document.getElementById('os-update-btn');
|
Nova.loading('Running OS upgrade — this may take a few minutes…');
|
||||||
if (btn) { btn.disabled = true; btn.textContent = 'Upgrading…'; }
|
|
||||||
Nova.toast('Running apt-get upgrade — this may take a few minutes…', 'info', 20000);
|
|
||||||
const res = await Nova.api('system', 'apply-os-update', { method: 'POST', timeout: 120000 });
|
const res = await Nova.api('system', 'apply-os-update', { method: 'POST', timeout: 120000 });
|
||||||
|
Nova.loadingDone();
|
||||||
if (res?.data) {
|
if (res?.data) {
|
||||||
const d = res.data;
|
const d = res.data;
|
||||||
const healed = Object.entries(d.services_healed || {}).map(([s,r]) => `${s}: ${r}`).join(', ');
|
const healed = Object.entries(d.services_healed || {}).map(([s,r]) => `${s}: ${r}`).join(', ');
|
||||||
@@ -1881,7 +1883,6 @@ ${dbs.map(d=>`<tr>
|
|||||||
Nova.loadPage('updates', pages);
|
Nova.loadPage('updates', pages);
|
||||||
} else {
|
} else {
|
||||||
Nova.toast(res?.error || 'Upgrade failed', 'error', 8000);
|
Nova.toast(res?.error || 'Upgrade failed', 'error', 8000);
|
||||||
if (btn) { btn.disabled = false; btn.textContent = 'Apply OS Upgrade'; }
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -1889,17 +1890,36 @@ ${dbs.map(d=>`<tr>
|
|||||||
// keep old alias for any lingering references
|
// keep old alias for any lingering references
|
||||||
window.applyUpdate = window.applyNovaCPXUpdate;
|
window.applyUpdate = window.applyNovaCPXUpdate;
|
||||||
window.adminServiceAction = async (svc, cmd) => {
|
window.adminServiceAction = async (svc, cmd) => {
|
||||||
|
const label = { start: 'Starting', stop: 'Stopping', restart: 'Restarting', reload: 'Reloading', flush: 'Flushing queue' }[cmd] || cmd;
|
||||||
|
Nova.loading(`${label} ${svc}…`);
|
||||||
|
// Optimistic immediate badge update
|
||||||
|
const optimistic = cmd === 'stop' ? 'inactive' : cmd === 'flush' ? null : 'activating';
|
||||||
|
if (optimistic) {
|
||||||
|
document.querySelectorAll(`[data-svc-status="${svc}"]`).forEach(el => {
|
||||||
|
el.innerHTML = Nova.badge(optimistic, optimistic === 'inactive' ? 'red' : 'yellow');
|
||||||
|
});
|
||||||
|
document.querySelectorAll(`[data-svc-dot="${svc}"]`).forEach(el => {
|
||||||
|
el.innerHTML = Nova.serviceDot(optimistic);
|
||||||
|
});
|
||||||
|
}
|
||||||
const res = await Nova.api('system', 'service', { method: 'POST', body: { service: svc, command: cmd } });
|
const res = await Nova.api('system', 'service', { method: 'POST', body: { service: svc, command: cmd } });
|
||||||
Nova.toast(`${svc}: ${cmd} → ${res?.success ? 'OK' : res?.message}`, res?.success ? 'success' : 'error');
|
Nova.loadingDone();
|
||||||
if (res?.success && cmd !== 'flush') refreshSvcStatus(svc);
|
if (res?.success) {
|
||||||
|
const msg = cmd === 'flush' ? `Mail queue flushed` : `${svc} ${cmd} complete`;
|
||||||
|
Nova.toast(msg, 'success');
|
||||||
|
if (cmd !== 'flush') window.refreshSvcStatus(svc);
|
||||||
|
} else {
|
||||||
|
Nova.toast(res?.message || `${svc} ${cmd} failed`, 'error');
|
||||||
|
if (cmd !== 'flush') window.refreshSvcStatus(svc, 0);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Polls is-active after a short delay and updates all [data-svc-status] / [data-svc-dot] in the DOM
|
// Polls is-active and updates all [data-svc-status] / [data-svc-dot] in the DOM
|
||||||
window.refreshSvcStatus = async (svc, delay = 1500) => {
|
window.refreshSvcStatus = async (svc, delay = 2000) => {
|
||||||
await new Promise(r => setTimeout(r, delay));
|
if (delay > 0) await new Promise(r => setTimeout(r, delay));
|
||||||
const r = await Nova.api('system', 'svc-check', { params: { service: svc } });
|
const r = await Nova.api('system', 'svc-check', { params: { service: svc } });
|
||||||
const status = r?.data?.status || 'unknown';
|
const status = r?.data?.status || 'unknown';
|
||||||
const color = status === 'active' ? 'green' : 'red';
|
const color = status === 'active' ? 'green' : status === 'activating' ? 'yellow' : 'red';
|
||||||
document.querySelectorAll(`[data-svc-status="${svc}"]`).forEach(el => {
|
document.querySelectorAll(`[data-svc-status="${svc}"]`).forEach(el => {
|
||||||
el.innerHTML = Nova.badge(status, color);
|
el.innerHTML = Nova.badge(status, color);
|
||||||
});
|
});
|
||||||
@@ -2754,10 +2774,13 @@ async function docker() {
|
|||||||
const status = st?.data || {};
|
const status = st?.data || {};
|
||||||
|
|
||||||
window.dockerInstall = async (btn) => {
|
window.dockerInstall = async (btn) => {
|
||||||
btn.disabled = true; btn.textContent = 'Installing… (this may take 2-3 minutes)';
|
btn.disabled = true;
|
||||||
|
Nova.loading('Installing Docker CE… (this may take 2–3 minutes)');
|
||||||
const r = await Nova.api('docker', 'install', { method: 'POST', body: {} });
|
const r = await Nova.api('docker', 'install', { method: 'POST', body: {} });
|
||||||
Nova.toast(r?.message || (r?.success ? 'Installed' : 'Failed'), r?.success ? 'success' : 'error');
|
Nova.loadingDone();
|
||||||
|
Nova.toast(r?.message || (r?.success ? 'Docker installed' : 'Install failed'), r?.success ? 'success' : 'error');
|
||||||
if (r?.success) Nova.loadPage('docker', window._novaPages);
|
if (r?.success) Nova.loadPage('docker', window._novaPages);
|
||||||
|
else btn.disabled = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!status.installed) {
|
if (!status.installed) {
|
||||||
|
|||||||
@@ -137,12 +137,45 @@ window.Nova = (() => {
|
|||||||
return String(str ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
return String(str ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject global CSS animation
|
// ── Loading overlay ───────────────────────────────────────────────────────
|
||||||
|
let _loadingEl = null;
|
||||||
|
let _loadingCount = 0;
|
||||||
|
function loading(msg = 'Working…') {
|
||||||
|
_loadingCount++;
|
||||||
|
if (!_loadingEl) {
|
||||||
|
_loadingEl = document.createElement('div');
|
||||||
|
_loadingEl.id = 'nova-loading-overlay';
|
||||||
|
_loadingEl.style.cssText = [
|
||||||
|
'position:fixed;inset:0;z-index:99999',
|
||||||
|
'background:rgba(0,0,0,.55)',
|
||||||
|
'display:flex;flex-direction:column;align-items:center;justify-content:center',
|
||||||
|
'gap:1rem;animation:fadeIn .15s',
|
||||||
|
].join(';');
|
||||||
|
_loadingEl.innerHTML = `
|
||||||
|
<div style="width:48px;height:48px;border:4px solid rgba(255,255,255,.2);border-top-color:#fff;border-radius:50%;animation:ncpxSpin 0.7s linear infinite"></div>
|
||||||
|
<div id="nova-loading-msg" style="color:#fff;font-size:1rem;font-weight:500;text-shadow:0 1px 3px rgba(0,0,0,.6)">${escHtml(msg)}</div>`;
|
||||||
|
document.body.appendChild(_loadingEl);
|
||||||
|
} else {
|
||||||
|
document.getElementById('nova-loading-msg').textContent = msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function loadingDone() {
|
||||||
|
_loadingCount = Math.max(0, _loadingCount - 1);
|
||||||
|
if (_loadingCount === 0 && _loadingEl) {
|
||||||
|
_loadingEl.remove();
|
||||||
|
_loadingEl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject global CSS animations
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = '@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}';
|
style.textContent = [
|
||||||
|
'@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}',
|
||||||
|
'@keyframes ncpxSpin{to{transform:rotate(360deg)}}',
|
||||||
|
].join('');
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
return { api, toast, modal, confirm, initNav, loadPage, progressBar, bytes, relTime, badge, serviceDot, escHtml };
|
return { api, toast, modal, confirm, initNav, loadPage, progressBar, bytes, relTime, badge, serviceDot, escHtml, loading, loadingDone };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// #26 Mobile sidebar toggle — shared across all panels
|
// #26 Mobile sidebar toggle — shared across all panels
|
||||||
|
|||||||
Reference in New Issue
Block a user