mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
Fix service switching, dynamic dashboard services, DB engine manager
- save-option: inline service switching (web/ftp/dns) instead of missing shell scripts - stats: dynamic service list based on web_server/ftp_server/dns_server settings - service action: allow all variants (nginx, pure-ftpd, pdns, nsd, etc.) - mysqlManager: full rewrite with MySQL/MariaDB/PostgreSQL engine cards (install/remove/start/stop), active engine selector, all-databases table - ftpServer page: dynamic — shows whichever FTP server is configured, not hardcoded proftpd - db-engine-action: fixed duplicate INSERT line Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -302,9 +302,16 @@ match ($action) {
|
|||||||
$diskFree = disk_free_space('/');
|
$diskFree = disk_free_space('/');
|
||||||
$diskPct = $diskTotal > 0 ? round((($diskTotal - $diskFree) / $diskTotal) * 100, 1) : 0;
|
$diskPct = $diskTotal > 0 ? round((($diskTotal - $diskFree) / $diskTotal) * 100, 1) : 0;
|
||||||
|
|
||||||
// Services
|
// Services — dynamic list based on configured servers
|
||||||
|
$webSetting = $db->fetchOne("SELECT `value` FROM settings WHERE `key`='web_server'")['value'] ?? 'apache';
|
||||||
|
$ftpSetting = $db->fetchOne("SELECT `value` FROM settings WHERE `key`='ftp_server'")['value'] ?? 'proftpd';
|
||||||
|
$dnsSetting = $db->fetchOne("SELECT `value` FROM settings WHERE `key`='dns_server'")['value'] ?? 'bind9';
|
||||||
|
$webSvc = match($webSetting) { 'nginx' => 'nginx', 'apache' => 'apache2', default => 'apache2' };
|
||||||
|
$ftpSvc = match($ftpSetting) { 'vsftpd' => 'vsftpd', 'pureftpd' => 'pure-ftpd', default => 'proftpd' };
|
||||||
|
$dnsSvc = match($dnsSetting) { 'powerdns' => 'pdns', 'nsd' => 'nsd', 'none' => null, default => 'named' };
|
||||||
|
$svcList = array_filter([$webSvc,'mysql','postfix','dovecot',$ftpSvc,$dnsSvc,'fail2ban']);
|
||||||
$services = [];
|
$services = [];
|
||||||
foreach (['apache2','nginx','mysql','postfix','dovecot','proftpd','named','fail2ban'] as $svc) {
|
foreach ($svcList as $svc) {
|
||||||
$active = trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: '');
|
$active = trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: '');
|
||||||
if ($active) $services[$svc] = $active;
|
if ($active) $services[$svc] = $active;
|
||||||
}
|
}
|
||||||
@@ -329,7 +336,9 @@ match ($action) {
|
|||||||
Auth::getInstance()->require('admin');
|
Auth::getInstance()->require('admin');
|
||||||
$svc = preg_replace('/[^a-z0-9\-_]/', '', $body['service'] ?? '');
|
$svc = preg_replace('/[^a-z0-9\-_]/', '', $body['service'] ?? '');
|
||||||
$cmd = $body['command'] ?? 'status';
|
$cmd = $body['command'] ?? 'status';
|
||||||
$allowed = ['apache2','nginx','mysql','postfix','dovecot','proftpd','named','fail2ban','php7.4-fpm','php8.1-fpm','php8.2-fpm','php8.3-fpm'];
|
$allowed = ['apache2','nginx','lighttpd','caddy','mysql','mariadb','postgresql','postfix','dovecot',
|
||||||
|
'proftpd','vsftpd','pure-ftpd','named','bind9','pdns','nsd','fail2ban',
|
||||||
|
'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'])) Response::error("Invalid command");
|
||||||
|
|
||||||
@@ -390,15 +399,31 @@ match ($action) {
|
|||||||
$value = $body['value'] ?? '';
|
$value = $body['value'] ?? '';
|
||||||
$allowed = ['web_server','mail_server','ftp_server','dns_server','whmcs_api_key','whmcs_enabled','ns1_hostname','ns2_hostname'];
|
$allowed = ['web_server','mail_server','ftp_server','dns_server','whmcs_api_key','whmcs_enabled','ns1_hostname','ns2_hostname'];
|
||||||
if (!in_array($key, $allowed)) Response::error("Invalid setting key: $key");
|
if (!in_array($key, $allowed)) Response::error("Invalid setting key: $key");
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
|
||||||
// For server switches, run install/reload scripts
|
// Inline service switching — stop all alternatives, start the chosen one
|
||||||
if (in_array($key, ['web_server','ftp_server','dns_server','mail_server'])) {
|
if ($key === 'web_server') {
|
||||||
$script = "/opt/novacpx/bin/switch-{$key}.sh";
|
$webSvcs = ['apache2','nginx','lighttpd','caddy'];
|
||||||
if (is_executable($script)) {
|
foreach ($webSvcs as $s) { shell_exec("systemctl stop $s 2>/dev/null; systemctl disable $s 2>/dev/null"); }
|
||||||
shell_exec("sudo {$script} " . escapeshellarg($value) . " > /var/log/novacpx/switch-{$key}.log 2>&1 &");
|
$startSvc = match($value) { 'nginx' => 'nginx', 'apache' => 'apache2', default => 'apache2' };
|
||||||
|
shell_exec("systemctl enable $startSvc 2>/dev/null && systemctl start $startSvc 2>/dev/null");
|
||||||
|
} 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"); }
|
||||||
|
$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) {
|
||||||
|
shell_exec("systemctl enable $startSvc 2>/dev/null && systemctl start $startSvc 2>/dev/null");
|
||||||
|
}
|
||||||
|
} 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"); }
|
||||||
|
if ($value !== 'none') {
|
||||||
|
$startSvc = match($value) { 'powerdns' => 'pdns', 'nsd' => 'nsd', default => 'named' };
|
||||||
|
shell_exec("systemctl enable $startSvc 2>/dev/null && systemctl start $startSvc 2>/dev/null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// mail_server: postfix + dovecot are always running; mail_server setting controls config template only
|
||||||
|
|
||||||
audit("settings.{$key}", $value);
|
audit("settings.{$key}", $value);
|
||||||
Response::success(null, "Setting saved: {$key} = {$value}");
|
Response::success(null, "Setting saved: {$key} = {$value}");
|
||||||
})(),
|
})(),
|
||||||
@@ -471,5 +496,77 @@ match ($action) {
|
|||||||
else Response::error("CyberMail returned HTTP {$code}: " . substr($resp, 0, 200));
|
else Response::error("CyberMail returned HTTP {$code}: " . substr($resp, 0, 200));
|
||||||
})(),
|
})(),
|
||||||
|
|
||||||
|
// ── Database engine management ────────────────────────────────────────────
|
||||||
|
'db-engines' => (function() use ($db) {
|
||||||
|
Auth::getInstance()->require('admin');
|
||||||
|
$engines = [];
|
||||||
|
foreach (['mysql','mariadb','postgresql'] as $eng) {
|
||||||
|
$svc = match($eng) {
|
||||||
|
'mysql' => 'mysql',
|
||||||
|
'mariadb' => 'mariadb',
|
||||||
|
'postgresql' => 'postgresql',
|
||||||
|
};
|
||||||
|
$active = trim(shell_exec("systemctl is-active $svc 2>/dev/null") ?: '');
|
||||||
|
$enabled = trim(shell_exec("systemctl is-enabled $svc 2>/dev/null") ?: '');
|
||||||
|
$pkgCheck = match($eng) {
|
||||||
|
'mysql' => 'dpkg -l mysql-server 2>/dev/null | grep -c "^ii"',
|
||||||
|
'mariadb' => 'dpkg -l mariadb-server 2>/dev/null | grep -c "^ii"',
|
||||||
|
'postgresql' => 'dpkg -l postgresql 2>/dev/null | grep -c "^ii"',
|
||||||
|
};
|
||||||
|
$installed = (int)trim(shell_exec($pkgCheck) ?: '0') > 0;
|
||||||
|
$version = '';
|
||||||
|
if ($installed) {
|
||||||
|
$version = match($eng) {
|
||||||
|
'mysql' => trim(shell_exec("mysql --version 2>/dev/null | grep -oP '[0-9]+\.[0-9]+\.[0-9]+'") ?: ''),
|
||||||
|
'mariadb' => trim(shell_exec("mariadb --version 2>/dev/null | grep -oP '[0-9]+\.[0-9]+\.[0-9]+'") ?: ''),
|
||||||
|
'postgresql' => trim(shell_exec("psql --version 2>/dev/null | grep -oP '[0-9]+\.[0-9]+'") ?: ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
$engines[$eng] = [
|
||||||
|
'installed' => $installed,
|
||||||
|
'active' => $active === 'active',
|
||||||
|
'enabled' => $enabled === 'enabled',
|
||||||
|
'version' => $version,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$active_db = $db->fetchOne("SELECT `value` FROM settings WHERE `key` = 'active_db_engine'")['value'] ?? 'mysql';
|
||||||
|
Response::success(['engines' => $engines, 'active_engine' => $active_db]);
|
||||||
|
})(),
|
||||||
|
|
||||||
|
'db-engine-action' => (function() use ($db, $body) {
|
||||||
|
Auth::getInstance()->require('admin');
|
||||||
|
$engine = preg_replace('/[^a-z]/', '', $body['engine'] ?? '');
|
||||||
|
$action = $body['action'] ?? '';
|
||||||
|
if (!in_array($engine, ['mysql','mariadb','postgresql'])) Response::error("Invalid engine");
|
||||||
|
if (!in_array($action, ['install','remove','start','stop','restart','set-active'])) Response::error("Invalid action");
|
||||||
|
|
||||||
|
$out = '';
|
||||||
|
if ($action === 'install') {
|
||||||
|
$pkg = match($engine) {
|
||||||
|
'mysql' => 'mysql-server',
|
||||||
|
'mariadb' => 'mariadb-server',
|
||||||
|
'postgresql' => 'postgresql postgresql-contrib',
|
||||||
|
};
|
||||||
|
$out = shell_exec("DEBIAN_FRONTEND=noninteractive apt-get install -y $pkg 2>&1");
|
||||||
|
shell_exec("systemctl enable $engine && systemctl start $engine 2>/dev/null");
|
||||||
|
} elseif ($action === 'remove') {
|
||||||
|
$pkg = match($engine) {
|
||||||
|
'mysql' => 'mysql-server mysql-client',
|
||||||
|
'mariadb' => 'mariadb-server mariadb-client',
|
||||||
|
'postgresql' => 'postgresql postgresql-contrib',
|
||||||
|
};
|
||||||
|
shell_exec("systemctl stop $engine 2>/dev/null || true");
|
||||||
|
$out = shell_exec("apt-get remove -y $pkg 2>&1");
|
||||||
|
} elseif ($action === 'set-active') {
|
||||||
|
$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);
|
||||||
|
Response::success(null, "Active database engine set to $engine");
|
||||||
|
} else {
|
||||||
|
shell_exec("systemctl $action $engine 2>/dev/null");
|
||||||
|
}
|
||||||
|
audit("db-engine.$action", $engine);
|
||||||
|
Response::success(['output' => substr($out ?: '', -1000)], ucfirst($action) . " $engine done");
|
||||||
|
})(),
|
||||||
|
|
||||||
default => Response::error("Unknown system action: $action", 404),
|
default => Response::error("Unknown system action: $action", 404),
|
||||||
};
|
};
|
||||||
|
|||||||
+105
-22
@@ -1597,24 +1597,81 @@ ${ips.length ? `
|
|||||||
|
|
||||||
// ── MySQL/DB Manager ───────────────────────────────────────────────────────
|
// ── MySQL/DB Manager ───────────────────────────────────────────────────────
|
||||||
async function mysqlManager() {
|
async function mysqlManager() {
|
||||||
const res = await Nova.api('databases','list',{params:{account_id:0}});
|
const [engRes, dbRes] = await Promise.all([
|
||||||
const dbs = res?.data || [];
|
Nova.api('system','db-engines'),
|
||||||
return `
|
Nova.api('databases','list',{params:{account_id:0}}),
|
||||||
|
]);
|
||||||
|
const eng = engRes?.data?.engines || {};
|
||||||
|
const actE = engRes?.data?.active_engine || 'mysql';
|
||||||
|
const dbs = dbRes?.data || [];
|
||||||
|
|
||||||
|
const engineCard = (id, label, icon) => {
|
||||||
|
const e = eng[id] || {};
|
||||||
|
const statusColor = e.active ? 'green' : (e.installed ? 'red' : 'default');
|
||||||
|
const statusText = !e.installed ? 'Not Installed' : (e.active ? 'Running' : 'Stopped');
|
||||||
|
return `
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header"><span class="card-title">Databases</span></div>
|
<div class="card-header">
|
||||||
${dbs.length ? `<table class="table"><thead><tr><th>Database</th><th>User</th><th>Type</th><th>Account</th><th>Size</th><th>Actions</th></tr></thead><tbody>
|
<span class="card-title">${icon} ${label}</span>
|
||||||
${dbs.map(d => `<tr>
|
${Nova.badge(statusText, statusColor)}
|
||||||
<td><strong>${d.db_name}</strong></td>
|
${e.version ? `<span class="text-muted" style="font-size:.8rem;margin-left:.5rem">v${e.version}</span>` : ''}
|
||||||
<td>${d.db_user}</td>
|
</div>
|
||||||
<td>${Nova.badge(d.db_type,'default')}</td>
|
<div class="card-body">
|
||||||
<td>${d.username||'—'}</td>
|
<div style="display:flex;flex-wrap:wrap;gap:.4rem;margin-bottom:.75rem">
|
||||||
<td>${d.size||'—'}</td>
|
${!e.installed
|
||||||
<td><button class="btn btn-xs btn-danger" onclick="adminDropDB(${d.id},'${d.db_name}')">Drop</button></td>
|
? `<button class="btn btn-xs btn-primary" onclick="dbEngineAction('${id}','install')">Install</button>`
|
||||||
</tr>`).join('')}
|
: `
|
||||||
</tbody></table>`
|
<button class="btn btn-xs" onclick="dbEngineAction('${id}','start')">Start</button>
|
||||||
: '<div class="empty" style="padding:2rem">No databases.</div>'}
|
<button class="btn btn-xs" onclick="dbEngineAction('${id}','stop')">Stop</button>
|
||||||
|
<button class="btn btn-xs" onclick="dbEngineAction('${id}','restart')">Restart</button>
|
||||||
|
<button class="btn btn-xs btn-danger" onclick="dbEngineAction('${id}','remove')">Remove</button>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
${e.installed && id !== 'postgresql' ? `<a href="http://${location.hostname}/phpmyadmin" target="_blank" class="btn btn-xs btn-ghost">phpMyAdmin ↗</a>` : ''}
|
||||||
|
${e.installed && id === 'postgresql' ? `<a href="http://${location.hostname}/pgadmin" target="_blank" class="btn btn-xs btn-ghost">pgAdmin ↗</a>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dbTable = dbs.length ? `
|
||||||
|
<table class="table"><thead><tr><th>Database</th><th>User</th><th>Type</th><th>Account</th><th>Actions</th></tr></thead><tbody>
|
||||||
|
${dbs.map(d=>`<tr>
|
||||||
|
<td><strong>${Nova.escHtml(d.db_name)}</strong></td>
|
||||||
|
<td>${Nova.escHtml(d.db_user||'—')}</td>
|
||||||
|
<td>${Nova.badge(d.db_type||'mysql','default')}</td>
|
||||||
|
<td>${Nova.escHtml(d.username||'—')}</td>
|
||||||
|
<td><button class="btn btn-xs btn-danger" onclick="adminDropDB(${d.id},'${Nova.escHtml(d.db_name)}')">Drop</button></td>
|
||||||
|
</tr>`).join('')}
|
||||||
|
</tbody></table>` : '<div class="empty" style="padding:2rem">No databases yet.</div>';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="page-header"><h2 class="page-title">Database Engine Manager</h2></div>
|
||||||
|
|
||||||
|
<div class="grid-3 gap-2" style="margin-bottom:1.5rem">
|
||||||
|
${engineCard('mysql', 'MySQL', '🐬')}
|
||||||
|
${engineCard('mariadb', 'MariaDB', '🦭')}
|
||||||
|
${engineCard('postgresql','PostgreSQL','🐘')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-bottom:1.5rem">
|
||||||
|
<div class="card-header"><span class="card-title">Active Engine</span><span class="text-muted" style="font-size:.82rem">Used for new account databases</span></div>
|
||||||
|
<div class="card-body" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
||||||
|
<select id="db-active-engine" class="form-control" style="max-width:200px">
|
||||||
|
<option value="mysql" ${actE==='mysql'?'selected':''}>MySQL</option>
|
||||||
|
<option value="mariadb" ${actE==='mariadb'?'selected':''}>MariaDB</option>
|
||||||
|
<option value="postgresql" ${actE==='postgresql'?'selected':''}>PostgreSQL</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="dbSetActive()">Set Active</button>
|
||||||
|
<span class="text-muted" style="font-size:.82rem">Currently: ${Nova.badge(actE,'green')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><span class="card-title">All Databases</span><span class="text-muted" style="font-size:.8rem">${dbs.length} total</span></div>
|
||||||
|
${dbTable}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.adminDropDB = (id, name) => {
|
window.adminDropDB = (id, name) => {
|
||||||
Nova.confirm(`Drop database ${name}? ALL DATA WILL BE LOST.`, async () => {
|
Nova.confirm(`Drop database ${name}? ALL DATA WILL BE LOST.`, async () => {
|
||||||
const r = await Nova.api('databases','drop',{method:'POST',body:{id}});
|
const r = await Nova.api('databases','drop',{method:'POST',body:{id}});
|
||||||
@@ -1622,6 +1679,25 @@ ${ips.length ? `
|
|||||||
else Nova.toast(r?.message,'error');
|
else Nova.toast(r?.message,'error');
|
||||||
}, true);
|
}, true);
|
||||||
};
|
};
|
||||||
|
window.dbEngineAction = (engine, action) => {
|
||||||
|
const labels = {install:`Installing ${engine}…`,remove:`Removing ${engine}…`,start:`Starting…`,stop:`Stopping…`,restart:`Restarting…`};
|
||||||
|
const doIt = async () => {
|
||||||
|
Nova.toast(labels[action]||'Working…','info');
|
||||||
|
const r = await Nova.api('system','db-engine-action',{method:'POST',body:{engine,action}});
|
||||||
|
Nova.toast(r?.message||(r?.success?'Done':'Failed'), r?.success?'success':'error');
|
||||||
|
if (r?.success) adminPage('mysql-manager');
|
||||||
|
};
|
||||||
|
if (['install','remove'].includes(action)) {
|
||||||
|
Nova.confirm(`${action === 'install' ? 'Install' : 'Remove'} ${engine}?`, doIt, action === 'remove');
|
||||||
|
} else { doIt(); }
|
||||||
|
};
|
||||||
|
window.dbSetActive = async () => {
|
||||||
|
const engine = document.getElementById('db-active-engine')?.value;
|
||||||
|
if (!engine) return;
|
||||||
|
const r = await Nova.api('system','db-engine-action',{method:'POST',body:{engine,action:'set-active'}});
|
||||||
|
Nova.toast(r?.message||(r?.success?'Active engine updated':'Failed'), r?.success?'success':'error');
|
||||||
|
if (r?.success) adminPage('mysql-manager');
|
||||||
|
};
|
||||||
|
|
||||||
// ── Mail Server ────────────────────────────────────────────────────────────
|
// ── Mail Server ────────────────────────────────────────────────────────────
|
||||||
async function mailServer() {
|
async function mailServer() {
|
||||||
@@ -1660,21 +1736,28 @@ ${ips.length ? `
|
|||||||
|
|
||||||
// ── FTP Server ────────────────────────────────────────────────────────────
|
// ── FTP Server ────────────────────────────────────────────────────────────
|
||||||
async function ftpServer() {
|
async function ftpServer() {
|
||||||
const r = await Nova.api('system','stats');
|
const [sRes, optsRes] = await Promise.all([
|
||||||
const ftpStatus = r?.data?.services?.proftpd || 'unknown';
|
Nova.api('system','stats'),
|
||||||
|
Nova.api('system','server-options'),
|
||||||
|
]);
|
||||||
|
const svcs = sRes?.data?.services || {};
|
||||||
|
const ftpConf = optsRes?.data?.ftp_server || 'proftpd';
|
||||||
|
const svcName = ftpConf === 'vsftpd' ? 'vsftpd' : (ftpConf === 'pureftpd' ? 'pure-ftpd' : 'proftpd');
|
||||||
|
const label = ftpConf === 'vsftpd' ? 'vsftpd' : (ftpConf === 'pureftpd' ? 'Pure-FTPd' : 'ProFTPD');
|
||||||
|
const status = svcs[svcName] || 'unknown';
|
||||||
return `
|
return `
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title">FTP Server (ProFTPD)</span>
|
<span class="card-title">FTP Server (${label})</span>
|
||||||
${Nova.badge(ftpStatus, ftpStatus==='active'?'green':'red')}
|
${Nova.badge(status, status==='active'?'green':'red')}
|
||||||
<div style="display:flex;gap:.5rem;margin-left:auto">
|
<div style="display:flex;gap:.5rem;margin-left:auto">
|
||||||
<button class="btn btn-sm" onclick="adminServiceAction('proftpd','restart')">Restart</button>
|
<button class="btn btn-sm" onclick="adminServiceAction('${svcName}','restart')">Restart</button>
|
||||||
<button class="btn btn-sm" onclick="adminServiceAction('proftpd','reload')">Reload</button>
|
<button class="btn btn-sm" onclick="adminServiceAction('${svcName}','reload')">Reload</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:1.25rem">
|
<div style="padding:1.25rem">
|
||||||
<div style="color:var(--muted);font-size:.85rem">
|
<div style="color:var(--muted);font-size:.85rem">
|
||||||
<p>ProFTPD uses virtual users stored in <code>/etc/proftpd/novacpx-users.passwd</code></p>
|
<p>Active FTP server: <strong>${label}</strong> — change in <a href="#" onclick="adminPage('server-options')">Server Options</a>.</p>
|
||||||
<p style="margin-top:.5rem">FTP connections use SFTP on port 22 or passive FTP on ports 20/21.</p>
|
<p style="margin-top:.5rem">FTP connections use SFTP on port 22 or passive FTP on ports 20/21.</p>
|
||||||
<p style="margin-top:.5rem">Per-account FTP management is available in each account's FTP page.</p>
|
<p style="margin-top:.5rem">Per-account FTP management is available in each account's FTP page.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user