/** * NovaCPX Admin Panel — page controllers */ (async () => { // ── Auth guard ───────────────────────────────────────────────────────────── // Inline login handler on port 8882 const loginForm = document.getElementById('login-form'); if (loginForm) { loginForm.addEventListener('submit', async e => { e.preventDefault(); const btn = document.getElementById('l-btn'); const err = document.getElementById('login-err'); btn.disabled = true; btn.textContent = 'Signing in…'; err.style.display = 'none'; const res = await Nova.api('auth', 'login', { method: 'POST', body: { username: document.getElementById('l-user').value, password: document.getElementById('l-pass').value } }); if (res?.success && res.data?.user?.role === 'admin') { location.reload(); } else { err.textContent = res?.message || 'Invalid credentials or insufficient role'; err.style.display = ''; btn.disabled = false; btn.textContent = 'Sign In to Admin'; } }); } const me = await Nova.api('auth', 'me'); if (!me?.success || me.data.role !== 'admin') { // Already showing the login form in #auth-check return; } document.getElementById('auth-check').style.display = 'none'; document.getElementById('app').style.display = ''; document.getElementById('user-name').textContent = me.data.username; document.getElementById('user-avatar').textContent = me.data.username[0].toUpperCase(); // ── Logout ───────────────────────────────────────────────────────────────── document.getElementById('logout-btn').addEventListener('click', async e => { e.preventDefault(); await Nova.api('auth', 'logout', { method: 'POST' }); location.href = '/'; }); // ── Page definitions ─────────────────────────────────────────────────────── const pages = { dashboard, 'server-status': serverStatus, accounts, resellers, packages, 'create-account': createAccount, 'dns-zones': dnsZones, nameservers, 'web-server': webServer, 'php-manager': phpManager, 'mysql-manager': mysqlManager, 'mail-server': mailServer, 'ftp-server': ftpServer, 'ssl-manager': sslManager, firewall, 'audit-log': auditLog, updates, backups, settings, }; Nova.initNav(pages); await Nova.loadPage('dashboard', pages); checkUpdates(); // ── Dashboard ────────────────────────────────────────────────────────────── async function dashboard() { const [stats, version] = await Promise.all([ Nova.api('system', 'stats'), Nova.api('system', 'version'), ]); const s = stats?.data || {}; const v = version?.data || {}; document.getElementById('server-ip').textContent = ''; return `
CPU Usage
${s.cpu?.pct ?? 0}%
Load: ${(s.cpu?.load || [0,0,0]).join(' / ')}
${Nova.progressBar(s.cpu?.pct || 0)}
Memory
${s.ram?.pct ?? 0}%
${Nova.bytes((s.ram?.used_kb||0)*1024)} / ${Nova.bytes((s.ram?.total_kb||0)*1024)}
${Nova.progressBar(s.ram?.pct || 0)}
Disk
${s.disk?.pct ?? 0}%
${Nova.bytes(s.disk?.total - s.disk?.free || 0)} used
${Nova.progressBar(s.disk?.pct || 0)}
Uptime
${s.uptime || '—'}
PHP ${v.php_version || '—'}
Services
${Object.entries(s.services || {}).map(([svc, status]) => ` `).join('')}
${Nova.serviceDot(status)} ${svc} ${Nova.badge(status, status === 'active' ? 'green' : 'red')}
NovaCPX Version
Installed${v.installed_version || '—'}
Branch${v.git_branch || 'main'}
Commit${v.git_commit || '—'}${v.git_dirty ? ' dirty' : ''}
PHP${v.php_version || '—'}
OS${v.os || '—'}
`; } // ── Server Status ────────────────────────────────────────────────────────── async function serverStatus() { const res = await Nova.api('system', 'stats'); const s = res?.data || {}; return `
Real-Time Server Status

CPU

${s.cpu?.pct}%

${Nova.progressBar(s.cpu?.pct||0)}

RAM

${s.ram?.pct}%

${Nova.progressBar(s.ram?.pct||0)}

Disk

${s.disk?.pct}%

${Nova.progressBar(s.disk?.pct||0)}

Load Average

${(s.cpu?.load||[]).join(' / ')}

Uptime

${s.uptime}

`; } // ── Updates ──────────────────────────────────────────────────────────────── async function updates() { const [ver, check] = await Promise.all([ Nova.api('system', 'version'), Nova.api('system', 'check-update'), ]); const v = ver?.data || {}; const upd = check?.data || {}; const count = upd.updates_available || 0; return `
NovaCPX Updates ${count > 0 ? Nova.badge(count + ' update' + (count > 1 ? 's' : '') + ' available', 'yellow') : Nova.badge('Up to date', 'green')}

Installed Version

${v.installed_version}

Git Commit

${v.git_commit || '—'}

Branch

${v.git_branch || 'main'}

Dirty Working Tree

${v.git_dirty ? Nova.badge('Yes','yellow') : Nova.badge('No','green')}

${count > 0 ? `
Pending Commits
${upd.commits?.map(c => `
${c}
`).join('') || 'None'}
` : `

NovaCPX is up to date.

`}
`; } // ── Audit Log ────────────────────────────────────────────────────────────── async function auditLog() { const res = await Nova.api('system', 'audit-log', { params: { per_page: 50 } }); const rows = res?.data || []; return `
Audit Log
${rows.map(r => ` `).join('')}
TimeUserActionResourceIP
${Nova.relTime(r.created_at)} ${r.username || '—'} ${r.action} ${r.resource || '—'} ${r.ip_address || '—'}
`; } // ── PHP Manager ──────────────────────────────────────────────────────────── async function phpManager() { return `
PHP Version Manager

Manage installed PHP versions and global extensions.

${['7.4','8.1','8.2','8.3'].map(v => `
PHP ${v}
${Nova.badge('Active','green')}
`).join('')}

Global PHP Extensions

Extensions installed across all PHP versions: mbstring, curl, gd, xml, zip, opcache, redis, imagick, pdo, pdo_mysql, pdo_pgsql

`; } // ── Settings ─────────────────────────────────────────────────────────────── async function settings() { return `
Panel Settings
`; } // ── Accounts ─────────────────────────────────────────────────────────────── async function accounts() { const res = await Nova.api('accounts', 'list'); const accts = res?.data?.accounts || []; window._adminAccts = accts; return `
All Hosting Accounts
${renderAccountTable(accts)}
`; } function renderAccountTable(accts) { if (!accts.length) return '
No accounts found.
'; return ` ${accts.map(a => ``).join('')}
UsernameDomainResellerPackageDiskStatusCreatedActions
${a.username} ${a.domain} ${a.reseller_username || 'admin'} ${a.package_name || '—'} ${a.disk_usage_mb || 0} MB ${Nova.badge(a.status, a.status==='active'?'green':a.status==='suspended'?'yellow':'red')} ${Nova.relTime(a.created_at)} ${a.status==='active' ? `` : ``}
`; } 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 || []); }; window.adminSuspend = async (id, user) => { Nova.confirm(`Suspend ${user}?`, async () => { const res = await Nova.api('accounts','suspend',{method:'POST',body:{account_id:id}}); if (res?.success) { Nova.toast('Suspended','success'); adminPage('accounts'); } else Nova.toast(res?.message,'error'); }); }; window.adminUnsuspend = async (id) => { const res = await Nova.api('accounts','unsuspend',{method:'POST',body:{account_id:id}}); if (res?.success) { Nova.toast('Unsuspended','success'); adminPage('accounts'); } else Nova.toast(res?.message,'error'); }; window.adminChangePass = (id, user) => { Nova.modal(`Change Password — ${user}`, `
`, ``); }; window.adminTerminate = (id, user) => { Nova.confirm(`TERMINATE ${user}? This permanently deletes all files, DBs, DNS, email. IRREVERSIBLE.`, async () => { const res = await Nova.api('accounts','terminate',{method:'POST',body:{account_id:id}}); if (res?.success) { Nova.toast('Terminated','success'); adminPage('accounts'); } else Nova.toast(res?.message,'error'); }, true); }; // ── Create Account ───────────────────────────────────────────────────────── async function createAccount() { const pkgRes = await Nova.api('packages', 'list'); const pkgOpts = (pkgRes?.data || []).map(p => ``).join(''); return `
Create Hosting Account
`; } window.adminSubmitCreateAccount = async () => { const res = await Nova.api('accounts','create',{method:'POST',body:{ username: document.getElementById('nca-user')?.value, password: document.getElementById('nca-pass')?.value, email: document.getElementById('nca-email')?.value, domain: document.getElementById('nca-domain')?.value, package_id: document.getElementById('nca-pkg')?.value, php_version:document.getElementById('nca-php')?.value, }}); const el = document.getElementById('nca-result'); if (res?.success) { Nova.toast('Account created!','success'); if (el) el.innerHTML = `
Account created successfully! View accounts →
`; } else { Nova.toast(res?.message || 'Failed','error'); if (el) el.innerHTML = `
${res?.message || 'Error creating account'}
`; } }; // ── Resellers ────────────────────────────────────────────────────────────── async function resellers() { const res = await Nova.api('accounts', 'list', { params:{ role: 'reseller' }}); const rows = res?.data?.accounts || []; return `
Reseller Accounts
${rows.length ? ` ${rows.map(r => ``).join('')}
UsernameEmailAccountsStatusActions
${r.username}${r.email||'—'} ${r.account_count||0} ${Nova.badge(r.status,r.status==='active'?'green':'red')}
` : '
No resellers yet.
'}
`; } window.adminAddReseller = () => { Nova.modal('Create Reseller Account', `
`, ``); }; // ── Packages ─────────────────────────────────────────────────────────────── async function packages() { const res = await Nova.api('packages', 'list'); const pkgs = res?.data || []; return `
Hosting Packages
${pkgs.length ? ` ${pkgs.map(p => ``).join('')}
NameDiskBWDBsEmailsPriceAccountsActions
${p.name} ${p.disk_mb > 0 ? p.disk_mb+'MB' : '∞'} ${p.bandwidth_mb > 0 ? p.bandwidth_mb+'MB' : '∞'} ${p.databases||'∞'} ${p.email_accounts||'∞'} ${p.price ? '$'+p.price : 'Free'} ${p.account_count||0}
` : '
No packages yet. Create one to start hosting accounts.
'}
`; } window.adminAddPkg = () => showAdminPkgModal(); window.adminEditPkg = async (id) => { const r = await Nova.api('packages','get',{params:{id}}); if (r?.success) showAdminPkgModal(r.data); }; function showAdminPkgModal(p = {}) { Nova.modal(p.id ? 'Edit Package' : 'Add Package', `
`, ``); } window.adminSavePkg = async (id) => { const body = {name:document.getElementById('ap-name')?.value,disk_mb:+document.getElementById('ap-disk')?.value,bandwidth_mb:+document.getElementById('ap-bw')?.value,databases:+document.getElementById('ap-db')?.value,email_accounts:+document.getElementById('ap-email')?.value,addon_domains:+document.getElementById('ap-dom')?.value,subdomains:+document.getElementById('ap-sub')?.value,ftp_accounts:+document.getElementById('ap-ftp')?.value,price:+document.getElementById('ap-price')?.value}; const res = id ? await Nova.api('packages','update',{method:'POST',body:{...body,id}}) : await Nova.api('packages','create',{method:'POST',body}); if (res?.success) { Nova.toast(id?'Updated':'Created','success'); document.querySelector('.modal-overlay')?.remove(); adminPage('packages'); } else Nova.toast(res?.message,'error'); }; window.adminDelPkg = (id, name) => { Nova.confirm(`Delete package "${name}"?`, async () => { const r = await Nova.api('packages','delete',{method:'POST',body:{id}}); if (r?.success) { Nova.toast('Deleted','success'); adminPage('packages'); } else Nova.toast(r?.message,'error'); }, true); }; // ── DNS Zones ────────────────────────────────────────────────────────────── async function dnsZones() { const res = await Nova.api('dns', 'zones'); const zones = res?.data || []; return `
DNS Zones
${zones.length ? ` ${zones.map(z => ``).join('')}
DomainAccountRecordsActions
${z.domain} ${z.username||'—'} ${z.record_count||0}
` : '
No DNS zones yet.
'}
`; } window.adminAddZone = () => { Nova.modal('Create DNS Zone', `
`, ``); }; window.adminEditZone = async (id, domain) => { const res = await Nova.api('dns', 'records', {params:{zone_id:id}}); if (!res?.success) { Nova.toast('Failed to load records','error'); return; } const rows = res.data.map(r => `${r.name}${Nova.badge(r.type,'default')}${r.value}${r.ttl} `).join(''); Nova.modal(`DNS: ${domain}`, ` ${rows}
NameTypeValueTTL
`); }; window.adminAddRecord = (zoneId, domain) => { Nova.modal('Add Record', `
`, ``); }; window.adminDelRecord = async (id, zoneId, domain) => { Nova.confirm('Delete this record?', async () => { const r = await Nova.api('dns','delete-record',{method:'POST',body:{id}}); if (r?.success) { Nova.toast('Deleted','success'); document.querySelector('.modal-overlay')?.remove(); adminEditZone(zoneId,domain); } else Nova.toast(r?.message,'error'); }, true); }; window.adminDelZone = (id, domain) => { Nova.confirm(`Delete DNS zone for ${domain}?`, async () => { const r = await Nova.api('dns','delete-zone',{method:'POST',body:{zone_id:id}}); if (r?.success) { Nova.toast('Zone deleted','success'); adminPage('dns-zones'); } else Nova.toast(r?.message,'error'); }, true); }; // ── Nameservers ──────────────────────────────────────────────────────────── async function nameservers() { const r = await Nova.api('server_setup','get'); const d = r?.data || {}; return `
Nameserver Configuration
`; } window.adminSaveNS = async () => { const r = await Nova.api('server_setup','nameservers',{method:'POST',body:{ns1:document.getElementById('ns1')?.value,ns2:document.getElementById('ns2')?.value}}); if (r?.success) Nova.toast('Nameservers saved','success'); else Nova.toast(r?.message,'error'); }; window.adminSetHostname = async () => { const r = await Nova.api('server_setup','set-hostname',{method:'POST',body:{hostname:document.getElementById('srvhost')?.value}}); if (r?.success) Nova.toast(`Hostname set to ${r.data?.hostname}`,'success'); else Nova.toast(r?.message,'error'); }; // ── Web Server ──────────────────────────────────────────────────────────── async function webServer() { const r = await Nova.api('system', 'stats'); const svcs = r?.data?.services || {}; const webSvc = Object.keys(svcs).find(k => k.includes('apache') || k.includes('nginx')) || 'apache2'; return `
Web Server Management
${Object.entries(svcs).map(([s,st]) => `
${s}${Nova.badge(st,st==='active'?'green':'red')}
`).join('')}
`; } // ── SSL Manager ──────────────────────────────────────────────────────────── async function sslManager() { const res = await Nova.api('ssl', 'list', {params:{account_id:0}}); const certs = res?.data || []; return `
SSL Certificate Manager
${certs.length ? ` ${certs.map(c => { const days = c.days_remaining; const badge = days !== null ? Nova.badge(days+'d', days<7?'red':days<30?'yellow':'green') : Nova.badge('unknown','muted'); return ``; }).join('')}
DomainAccountTypeExpiresDaysActions
${c.domain} ${c.username||'—'} ${Nova.badge(c.type,'default')} ${c.expires_at||'—'} ${badge}
` : '
No SSL certificates issued yet.
'}
`; } window.adminIssueBulkSSL = async () => { Nova.toast('Queuing SSL for all domains without certificates…','info',6000); // Get all accounts, then issue SSL for each domain const accts = await Nova.api('accounts','list',{params:{limit:1000}}); let count = 0; for (const a of (accts?.data?.accounts || [])) { await Nova.api('ssl','issue',{method:'POST',body:{domain:a.domain}}); count++; } Nova.toast(`SSL issued for ${count} domains`,'success'); adminPage('ssl-manager'); }; window.adminRenewCert = async (id) => { Nova.toast('Renewing…','info'); const r = await Nova.api('ssl','renew',{method:'POST',body:{cert_id:id}}); if (r?.success) { Nova.toast('Renewed','success'); adminPage('ssl-manager'); } else Nova.toast(r?.message,'error'); }; window.adminDelCert = (id, domain) => { Nova.confirm(`Delete SSL cert for ${domain}?`, async () => { const r = await Nova.api('ssl','delete',{method:'POST',body:{cert_id:id}}); if (r?.success) { Nova.toast('Deleted','success'); adminPage('ssl-manager'); } else Nova.toast(r?.message,'error'); }, true); }; // ── Firewall ─────────────────────────────────────────────────────────────── async function firewall() { const ufwRes = await Nova.api('system','service',{method:'POST',body:{service:'ufw',command:'status'}}).catch(()=>null); return `
UFW Firewall
Fail2Ban
`; } window.adminAllowPort = async () => { const port = document.getElementById('fw-port')?.value; if (!port) return; const r = await Nova.api('system','service',{method:'POST',body:{service:'ufw',command:`allow ${port}`}}); Nova.toast(r?.success ? `Allowed ${port}` : r?.message,'success'); }; window.adminBlockPort = async () => { const port = document.getElementById('fw-block')?.value; if (!port) return; const r = await Nova.api('system','service',{method:'POST',body:{service:'ufw',command:`deny ${port}`}}); Nova.toast(r?.success ? `Blocked ${port}` : r?.message,'success'); }; window.adminF2bStatus = async () => { const r = await Nova.api('system','service',{method:'POST',body:{service:'fail2ban-client',command:'status'}}); Nova.modal('Fail2Ban Jails', `
${r?.data?.output || 'No output'}
`); }; window.adminUnban = async () => { const ip = document.getElementById('fw-unban')?.value; if (!ip) return; Nova.toast(`Unbanning ${ip}…`,'info'); // Unban from all jails for (const jail of ['sshd','novacpx-user','novacpx-admin','novacpx-reseller','novacpx-webmail']) { await Nova.api('system','service',{method:'POST',body:{service:`fail2ban-client set ${jail} unbanip`,command:ip}}).catch(()=>{}); } Nova.toast('Unban commands sent','success'); }; // ── MySQL/DB Manager ─────────────────────────────────────────────────────── async function mysqlManager() { const res = await Nova.api('databases','list',{params:{account_id:0}}); const dbs = res?.data || []; return `
Databases
${dbs.length ? ` ${dbs.map(d => ``).join('')}
DatabaseUserTypeAccountSizeActions
${d.db_name} ${d.db_user} ${Nova.badge(d.db_type,'default')} ${d.username||'—'} ${d.size||'—'}
` : '
No databases.
'}
`; } window.adminDropDB = (id, name) => { Nova.confirm(`Drop database ${name}? ALL DATA WILL BE LOST.`, async () => { const r = await Nova.api('databases','drop',{method:'POST',body:{id}}); if (r?.success) { Nova.toast('Dropped','success'); adminPage('mysql-manager'); } else Nova.toast(r?.message,'error'); }, true); }; // ── Mail Server ──────────────────────────────────────────────────────────── async function mailServer() { const r = await Nova.api('system','stats'); const svcs = r?.data?.services || {}; const mailStatus = svcs['postfix'] || 'unknown'; const doveStatus = svcs['dovecot'] || 'unknown'; return `
Mail Services
${[['postfix',mailStatus],['dovecot',doveStatus],['spamassassin','unknown']].map(([s,st]) => `
${s} ${Nova.badge(st,st==='active'?'green':'red')}
`).join('')}
Mail Queue
`; } window.adminViewMailQueue = async () => { const r = await Nova.api('system','service',{method:'POST',body:{service:'mailq',command:'status'}}); Nova.modal('Mail Queue', `
${r?.data?.output || 'Queue is empty'}
`); }; // ── FTP Server ──────────────────────────────────────────────────────────── async function ftpServer() { const r = await Nova.api('system','stats'); const ftpStatus = r?.data?.services?.proftpd || 'unknown'; return `
FTP Server (ProFTPD) ${Nova.badge(ftpStatus, ftpStatus==='active'?'green':'red')}

ProFTPD uses virtual users stored in /etc/proftpd/novacpx-users.passwd

FTP connections use SFTP on port 22 or passive FTP on ports 20/21.

Per-account FTP management is available in each account's FTP page.

`; } // ── Backups ──────────────────────────────────────────────────────────────── async function backups() { const res = await Nova.api('accounts','list',{params:{limit:1000}}); const accts = res?.data?.accounts || []; return `
Backup Manager
${accts.slice(0,20).map(a => ``).join('')}
AccountDomainActions
${a.username} ${a.domain}
`; } window.adminBackupAll = () => Nova.toast('Full backup queued — this may take several minutes.','info',6000); window.adminBackupAccount = (id, user) => Nova.toast(`Backup queued for ${user}…`,'info'); // ── Global action helpers ────────────────────────────────────────────────── window.adminPage = (page) => Nova.loadPage(page, pages); window.applyUpdate = async () => { Nova.confirm('Apply all pending updates? The panel may restart.', async () => { Nova.toast('Applying update…', 'info', 8000); const res = await Nova.api('system', 'apply-update', { method: 'POST' }); if (res?.data?.updated) { Nova.toast(`Updated to ${res.data.to_commit}`, 'success'); Nova.loadPage('updates', pages); } else { Nova.toast(res?.data?.pull_output || 'Already up to date', 'info'); } }); }; window.adminServiceAction = async (svc, 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'); }; window.phpAction = async (ver, cmd) => { const svc = `php${ver}-fpm`; await window.adminServiceAction(svc, 'restart'); }; // ── Check for updates badge ──────────────────────────────────────────────── async function checkUpdates() { const res = await Nova.api('system', 'check-update'); const n = res?.data?.updates_available || 0; const badge = document.getElementById('update-badge'); if (badge && n > 0) { badge.textContent = n; badge.style.display = ''; } } })();