/** * NovaCPX Admin Panel — page controllers */ (async () => { // ── Auth guard ───────────────────────────────────────────────────────────── // Inline login handler on port 8882 let _loginCredentials = null; 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; err.style.display = 'none'; // Step 2: TOTP code entry const totpInput = document.getElementById('l-totp'); if (totpInput && _loginCredentials) { btn.textContent = 'Verifying…'; const res = await Nova.api('auth', 'login', { method: 'POST', body: { ..._loginCredentials, totp_code: totpInput.value.trim() } }); if (res?.success && res.data?.user?.role === 'admin') { location.reload(); } else { err.textContent = res?.message || 'Invalid 2FA code'; err.style.display = ''; btn.disabled = false; btn.textContent = 'Verify'; } return; } // Step 1: username + password btn.textContent = 'Signing in…'; const creds = { username: document.getElementById('l-user').value, password: document.getElementById('l-pass').value }; const res = await Nova.api('auth', 'login', { method: 'POST', body: creds }); if (res?.success && res.data?.user?.role === 'admin') { location.reload(); } else if (res?.totp_required) { // Show TOTP step _loginCredentials = creds; document.getElementById('l-user').closest('.form-group').style.display = 'none'; document.getElementById('l-pass').closest('.form-group').style.display = 'none'; const totpGroup = document.createElement('div'); totpGroup.className = 'form-group'; totpGroup.innerHTML = ''; loginForm.insertBefore(totpGroup, btn.parentNode || btn); btn.textContent = 'Verify'; btn.disabled = false; } 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, 'nginx-proxy': nginxProxy, wordpress, 'ssl-manager': sslManager, firewall, 'audit-log': auditLog, twofa, updates, backups, cloudflare, settings, }; window._novaPages = pages; 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, ncpxCheck, osCheck] = await Promise.all([ Nova.api('system', 'version'), Nova.api('system', 'check-novacpx-update'), Nova.api('system', 'check-os-update'), ]); const v = ver?.data || {}; const ncpx = ncpxCheck?.data || {}; const os = osCheck?.data || {}; const ncpxCount = ncpx.updates_available || 0; const osCount = os.upgradable || 0; return `
NovaCPX Panel ${ncpxCount > 0 ? Nova.badge(ncpxCount + ' commit' + (ncpxCount > 1 ? 's' : '') + ' available', 'yellow') : Nova.badge('Up to date', 'green')}

Installed

${v.installed_version || '—'}

Commit

${ncpx.current_commit || v.git_commit || '—'}

Branch

${ncpx.branch || 'main'}

PHP

${v.php_version || '—'}
${ncpxCount > 0 ? `
Pending Commits
${ncpx.commits?.map(c => `
${Nova.escHtml(c)}
`).join('') || 'None'}

PHP syntax is validated before deploy. If the panel goes down after update, it will automatically restore from backup.

` : `

NovaCPX is up to date.

`}
Operating System Packages ${os.security_updates > 0 ? Nova.badge(os.security_updates + ' security', 'red') : ''} ${osCount > 0 ? Nova.badge(osCount + ' upgradable', 'yellow') : Nova.badge('All current', 'green')}
${osCount > 0 ? `
${os.packages?.map(p => ``).join('') || ''}
PackageFromTo
${Nova.escHtml(p.name)} ${Nova.escHtml(p.from || '(new)')} ${Nova.escHtml(p.to)}

Services are automatically restarted if an upgrade stops them. The NovaCPX web root is backed up before upgrade and restored if panel ports go down.

` : `

All OS packages are current.

`}
`; } // ── 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 ─────────────────────────────────────────────────────────────── // ── Firewall ─────────────────────────────────────────────────────────────── async function firewall() { const [fwRes, f2bRes, ipRes, ignoreipRes] = await Promise.all([ Nova.api('firewall','status'), Nova.api('firewall','f2b-status'), Nova.api('firewall','ip-lists'), Nova.api('firewall','f2b-ignoreip-list'), ]); const fw = fwRes?.data || {}; const jails = f2bRes?.data?.jails || []; const trusted = ipRes?.data?.trusted || []; const blocked = ipRes?.data?.blocked || []; const fwIgnoreips = ignoreipRes?.data?.ignoreip || ignoreipRes?.data?.detected || []; const rules = fw.rules || []; const active = fw.active; const totalBanned = jails.reduce((s,j) => s + (j.currently_banned||0), 0); return `
Default Policies

Incoming

Outgoing

Fail2Ban ${totalBanned > 0 ? Nova.badge(totalBanned + ' banned', 'red') : Nova.badge('0 banned','green')}

Active Jails

${jails.length}

Currently Banned

${totalBanned}

UFW Rules ${rules.length} rule${rules.length!==1?'s':''}
${rules.length ? `
${rules.map(r => ``).join('')}
#To / PortActionFrom
${r.num} ${Nova.escHtml(r.to)} ${fwActionBadge(r.action)} ${Nova.escHtml(r.from)}
` : `

No rules defined.

`}
Quick Rule
Trusted IPs ${trusted.length} IPs
${trusted.length ? `
${trusted.map(ip => `${Nova.escHtml(ip)} ×`).join('')}
` : `

No trusted IPs.

`}
Blocked IPs ${blocked.length} IPs
${blocked.length ? `
${blocked.map(ip => `${Nova.escHtml(ip)} ×`).join('')}
` : `

No blocked IPs.

`}
Fail2Ban Jails
${jails.length ? `
${jails.map(j => ``).join('')}
JailCurrently BannedTotal BannedFailedActions
${Nova.escHtml(j.jail)} ${j.currently_banned > 0 ? `${j.currently_banned}` : '0'} ${j.total_banned} ${j.currently_failed} ${j.currently_banned > 0 ? `` : ''}
` : `

Fail2Ban not running or no jails configured.

`}
Fail2Ban Whitelist IPs that will never be banned
${(fwIgnoreips||[]).map(ip => fwIgnoreipChip(ip)).join('')}

Loopback (127.0.0.0/8, ::1) and the server's own LAN IPs are added automatically. Add your home/office IP or subnet here so you never lock yourself out.

UFW Logging
Logs at /var/log/ufw.log
`; } function fwActionBadge(action) { const a = (action||'').toLowerCase(); if (a.includes('allow')) return Nova.badge('ALLOW','green'); if (a.includes('deny')) return Nova.badge('DENY','red'); if (a.includes('reject'))return Nova.badge('REJECT','red'); if (a.includes('limit')) return Nova.badge('LIMIT','yellow'); return `${Nova.escHtml(action)}`; } window.fwToggle = async (enable) => { const label = enable ? 'Enable' : 'Disable'; Nova.confirm(`${label} UFW firewall?`, async () => { const r = await Nova.api('firewall', enable ? 'enable' : 'disable', {method:'POST'}); Nova.toast(r?.message || label + 'd', r?.success ? 'success' : 'error'); adminPage('firewall'); }, !enable); }; window.fwSavePolicies = async () => { const inc = document.getElementById('pol-incoming')?.value; const out = document.getElementById('pol-outgoing')?.value; await Nova.api('firewall','default-policy',{method:'POST',body:{direction:'incoming',policy:inc}}); const r2 = await Nova.api('firewall','default-policy',{method:'POST',body:{direction:'outgoing',policy:out}}); Nova.toast(r2?.success ? 'Policies saved' : r2?.message, r2?.success ? 'success' : 'error'); adminPage('firewall'); }; window.fwDeleteRule = (num) => { Nova.confirm(`Delete rule #${num}? This cannot be undone.`, async () => { const r = await Nova.api('firewall','delete-rule',{method:'POST',body:{num}}); Nova.toast(r?.message || 'Deleted', r?.success ? 'success' : 'error'); adminPage('firewall'); }, true); }; window.fwResetModal = () => { Nova.confirm('Reset ALL firewall rules to NovaCPX defaults? SSH, HTTP, HTTPS, and panel ports will be re-allowed automatically.', async () => { Nova.toast('Resetting firewall…','info',5000); const r = await Nova.api('firewall','reset',{method:'POST'}); Nova.toast(r?.message || 'Reset complete','success'); adminPage('firewall'); }, true); }; window.fwQuickRule = async () => { const body = { action: document.getElementById('qr-action')?.value, direction: document.getElementById('qr-dir')?.value, port: document.getElementById('qr-port')?.value, proto: document.getElementById('qr-proto')?.value, from_ip: document.getElementById('qr-from')?.value || 'any', comment: document.getElementById('qr-comment')?.value, }; if (!body.port) { Nova.toast('Port/service is required','error'); return; } const r = await Nova.api('firewall','add-rule',{method:'POST',body}); Nova.toast(r?.message || (r?.success ? 'Rule added' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) adminPage('firewall'); }; window.fwAddRuleModal = () => { Nova.modal('Add Firewall Rule',`
`,` `); }; window.fwSubmitAddRule = async () => { const body = { action: document.getElementById('m-action')?.value, direction: document.getElementById('m-dir')?.value, port: document.getElementById('m-port')?.value, proto: document.getElementById('m-proto')?.value, from_ip: document.getElementById('m-from')?.value || 'any', to_ip: document.getElementById('m-to')?.value || 'any', comment: document.getElementById('m-comment')?.value, }; if (!body.port) { Nova.toast('Port is required','error'); return; } document.querySelector('.modal-overlay')?.remove(); const r = await Nova.api('firewall','add-rule',{method:'POST',body}); Nova.toast(r?.message || (r?.success ? 'Rule added' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) adminPage('firewall'); }; window.fwAllowIp = async () => { const ip = document.getElementById('fw-trust-ip')?.value?.trim(); if (!ip) { Nova.toast('Enter an IP or CIDR','error'); return; } const r = await Nova.api('firewall','allow-ip',{method:'POST',body:{ip}}); Nova.toast(r?.message || (r?.success ? 'IP allowed' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) adminPage('firewall'); }; window.fwBlockIp = async () => { const ip = document.getElementById('fw-block-ip')?.value?.trim(); if (!ip) { Nova.toast('Enter an IP or CIDR','error'); return; } Nova.confirm(`Block ${ip}? This will deny all incoming traffic from this address.`, async () => { const r = await Nova.api('firewall','block-ip',{method:'POST',body:{ip}}); Nova.toast(r?.message || (r?.success ? 'IP blocked' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) adminPage('firewall'); }, true); }; window.fwRemoveIp = (ip, action) => { Nova.confirm(`Remove ${action} rule for ${ip}?`, async () => { const r = await Nova.api('firewall','remove-ip',{method:'POST',body:{ip,action}}); Nova.toast(r?.message || 'Removed', r?.success ? 'success' : 'error'); if (r?.success) adminPage('firewall'); }, true); }; window.fwJailDetail = async (jail) => { const r = await Nova.api('firewall','f2b-jail',{method:'POST',body:{jail}}); const d = r?.data || {}; const ips = d.banned_ips || []; Nova.modal(`Fail2Ban: ${jail}`,`

Currently Banned

${d.currently_banned}

Total Banned

${d.total_banned}

${ips.length ? ` ${ips.map(ip=>``).join('')}
Banned IP
${Nova.escHtml(ip)}
` : '

No IPs currently banned in this jail.

'}`); }; window.fwUnbanIp = async (ip, jail, btn) => { if (btn) btn.disabled = true; const r = await Nova.api('firewall','f2b-unban',{method:'POST',body:{ip,jail}}); Nova.toast(r?.message || 'Unbanned', r?.success ? 'success' : 'error'); if (r?.success && btn) btn.closest('tr')?.remove(); }; window.fwManualBanModal = (jail) => { Nova.modal(`Manual Ban — ${jail}`,`
`,` `); }; window.fwSubmitManualBan = async (jail) => { const ip = document.getElementById('mb-ip')?.value?.trim(); if (!ip) { Nova.toast('Enter an IP','error'); return; } document.querySelector('.modal-overlay')?.remove(); const r = await Nova.api('firewall','f2b-ban',{method:'POST',body:{ip,jail}}); Nova.toast(r?.message || (r?.success ? 'Banned' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) adminPage('firewall'); }; window.fwF2bReload = async () => { const r = await Nova.api('firewall','f2b-reload',{method:'POST'}); Nova.toast(r?.message || 'Reloaded', r?.success ? 'success' : 'error'); }; window.fwF2bRestart = async () => { Nova.confirm('Restart Fail2Ban? Active bans will be preserved.', async () => { const r = await Nova.api('firewall','f2b-restart',{method:'POST'}); Nova.toast(r?.message || 'Restarted', r?.success ? 'success' : 'error'); adminPage('firewall'); }); }; window.fwSetLogging = async () => { const level = document.getElementById('fw-log-level')?.value; const r = await Nova.api('firewall','set-logging',{method:'POST',body:{level}}); Nova.toast(r?.message || 'Logging updated', r?.success ? 'success' : 'error'); }; function fwIgnoreipChip(ip) { const isLoopback = ip === '127.0.0.0/8' || ip === '127.0.0.1' || ip === '::1'; return ` ${Nova.escHtml(ip)}${isLoopback ? ' 🔒' : ' ×'} `; } window.fwIgnoreipAdd = async () => { const ip = document.getElementById('fw-ignoreip-input')?.value?.trim(); if (!ip) { Nova.toast('Enter an IP address or CIDR range', 'error'); return; } const r = await Nova.api('firewall','f2b-ignoreip-add',{method:'POST',body:{ip}}); Nova.toast(r?.message || (r?.success ? 'Added' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) { const chips = document.getElementById('ignoreip-chips'); if (chips) chips.innerHTML = (r.data?.ignoreip || []).map(fwIgnoreipChip).join(''); const inp = document.getElementById('fw-ignoreip-input'); if (inp) inp.value = ''; } }; window.fwIgnoreipRemove = async (ip) => { Nova.confirm(`Remove ${ip} from Fail2Ban whitelist? They could get banned if they fail too many login attempts.`, async () => { const r = await Nova.api('firewall','f2b-ignoreip-remove',{method:'POST',body:{ip}}); Nova.toast(r?.message || (r?.success ? 'Removed' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) { const chips = document.getElementById('ignoreip-chips'); if (chips) chips.innerHTML = (r.data?.ignoreip || []).map(fwIgnoreipChip).join(''); } }, true); }; window.fwIgnoreipReset = () => { Nova.confirm('Reset Fail2Ban whitelist to server defaults (loopback + local IPs)?', async () => { const r = await Nova.api('firewall','f2b-ignoreip-reset',{method:'POST'}); Nova.toast(r?.message || 'Reset', r?.success ? 'success' : 'error'); if (r?.success) adminPage('firewall'); }); }; // ── 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 — delegates to backupsFull() defined in additions ───────────── async function backups() { return backupsFull(); } // ── Stubs for new pages — implementations in additions block below ───────── async function wordpress() { return `

Loading…

`; } async function cloudflare() { return `

Loading…

`; } async function twofa() { return `

Loading…

`; } async function nginxProxy() { return `

Loading…

`; } // ── Global action helpers ────────────────────────────────────────────────── window.adminPage = (page) => Nova.loadPage(page, pages); 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 () => { const btn = document.getElementById('ncpx-update-btn'); 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' }); 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); if (btn) { btn.disabled = false; btn.textContent = 'Update NovaCPX'; } } else { Nova.toast('Already up to date.', 'info'); if (btn) { btn.disabled = false; btn.textContent = 'Update NovaCPX'; } } }); }; 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 () => { const btn = document.getElementById('os-update-btn'); 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 }); 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); if (btn) { btn.disabled = false; btn.textContent = 'Apply OS Upgrade'; } } }); }; // keep old alias for any lingering references window.applyUpdate = window.applyNovaCPXUpdate; 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 [ncpx, os] = await Promise.all([ Nova.api('system', 'check-novacpx-update'), Nova.api('system', 'check-os-update'), ]); const ncpxN = ncpx?.data?.updates_available || 0; const osN = os?.data?.upgradable || 0; const total = ncpxN + osN; const badge = document.getElementById('update-badge'); if (badge && total > 0) { badge.textContent = total; badge.style.display = ''; } } })(); // ── ADDITIONS: appended by features #14-17 ──────────────────────────────── // ── WordPress Manager (#14) ──────────────────────────────────────────────── async function wordpress() { const [acctRes, wpRes] = await Promise.all([ Nova.api('accounts','list',{params:{limit:500}}), Nova.api('wordpress','list'), ]); const accts = acctRes?.data?.accounts || []; const installs = wpRes?.data?.installs || []; window._adminAcctsWP = accts; return `
WordPress Installs ${installs.length} install${installs.length!==1?'s':''}
${installs.length ? `
${installs.map(w => ``).join('')}
DomainPathAccountVersionStatusActions
${Nova.escHtml(w.domain)} ${Nova.escHtml(w.path||'/')} ${Nova.escHtml(w.username||'—')} ${w.wp_version ? `${Nova.escHtml(w.wp_version)}` : '—'} ${Nova.badge(w.status||'active', w.status==='active'?'green':w.status==='updating'?'yellow':'red')} ${!w.staging_of ? `` : `staging`}
` : `
No WordPress installs yet. Click "Install WordPress" to get started.
`}
`; } window.wpInstallModal = () => { const accts = window._adminAcctsWP || []; const opts = accts.map(a => ``).join(''); Nova.modal('Install WordPress', `

wp-cli will be downloaded automatically if not installed. This may take 1-2 minutes.

`, ` `); }; window.wpSubmitInstall = async () => { const btn = document.getElementById('wp-install-btn'); if (btn) { btn.disabled = true; btn.textContent = 'Installing…'; } Nova.toast('Installing WordPress — this may take 1-2 minutes…', 'info', 90000); const res = await Nova.api('wordpress','install',{method:'POST',body:{ account_id: +document.getElementById('wp-acct')?.value, domain: document.getElementById('wp-domain')?.value, path: document.getElementById('wp-path')?.value || '/', site_title: document.getElementById('wp-title')?.value, admin_user: document.getElementById('wp-admin')?.value, admin_pass: document.getElementById('wp-adminpass')?.value, admin_email:document.getElementById('wp-email')?.value, }}); document.querySelector('.modal-overlay')?.remove(); if (res?.success) { Nova.toast('WordPress installed!','success'); adminPage('wordpress'); } else Nova.toast(res?.message || 'Install failed','error'); }; window.wpUpdate = async (id, type) => { const action = type === 'core' ? 'update-core' : type === 'plugins' ? 'update-plugins' : 'update-themes'; Nova.toast(`Updating ${type}…`,'info',15000); const r = await Nova.api('wordpress', action, {method:'POST',body:{install_id:id}}); Nova.toast(r?.message || (r?.success ? 'Updated' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) adminPage('wordpress'); }; window.wpInfo = async (id, domain) => { Nova.toast('Loading info…','info',5000); const r = await Nova.api('wordpress','info',{params:{install_id:id}}); if (!r?.success) { Nova.toast(r?.message,'error'); return; } const d = r.data || {}; const plugins = (d.plugins||[]).map(p => `${Nova.escHtml(p.name)}${Nova.escHtml(p.version||'')}${Nova.badge(p.status||'inactive',p.status==='active'?'green':'muted')}`).join(''); const themes = (d.themes||[]).map(t => `${Nova.escHtml(t.name)}${Nova.escHtml(t.version||'')}${Nova.badge(t.status||'inactive',t.status==='active'?'green':'muted')}`).join(''); Nova.modal(`WordPress: ${domain}`,`

Core Version

${Nova.escHtml(d.version||'—')}

Site URL

${Nova.escHtml(d.siteurl||'—')}

Plugins (${(d.plugins||[]).length})

${plugins ? `${plugins}
PluginVersionStatus
` : '

None

'}

Themes (${(d.themes||[]).length})

${themes ? `${themes}
ThemeVersionStatus
` : '

None

'}`); }; window.wpCloneStaging = (id, domain) => { Nova.confirm(`Clone ${domain} to a staging environment? This copies all files and the database.`, async () => { Nova.toast('Cloning to staging…','info',30000); const r = await Nova.api('wordpress','clone-staging',{method:'POST',body:{install_id:id}}); Nova.toast(r?.message || (r?.success ? 'Staging created' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) adminPage('wordpress'); }); }; window.wpDelete = (id, domain) => { Nova.confirm(`DELETE WordPress install on ${domain}? This removes all files AND drops the database. IRREVERSIBLE.`, async () => { const r = await Nova.api('wordpress','delete',{method:'POST',body:{install_id:id}}); Nova.toast(r?.message || (r?.success ? 'Deleted' : 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) adminPage('wordpress'); }, true); }; // ── Backup Manager — full implementation (#15) ───────────────────────────── async function backupsFull() { const [acctRes, bkRes] = await Promise.all([ Nova.api('accounts','list',{params:{limit:500}}), Nova.api('backup','list'), ]); const accts = acctRes?.data?.accounts || []; const backupList = bkRes?.data?.backups || []; const diskUsed = bkRes?.data?.disk_used || 0; window._adminAcctsBK = accts; return `
Total Backups
${backupList.length}
Disk Used
${Nova.bytes(diskUsed)}
Accounts
${accts.length}
Backup Schedules

Set per-account backup schedules. Cron runs backups automatically based on the configured frequency.

${accts.slice(0,8).map(a => ``).join('')} ${accts.length>8?`+${accts.length-8} more`:''}
All Backups
${backupList.length ? `
${backupList.map(b => ``).join('')}
AccountTypeSizeStatusStorageCreatedActions
${Nova.escHtml(b.username||b.account_id||'—')} ${Nova.badge(b.type,'default')} ${Nova.bytes(b.size||0)} ${Nova.badge(b.status, b.status==='complete'?'green':b.status==='failed'?'red':'yellow')} ${b.remote_path ? Nova.badge('remote','blue') : Nova.badge('local','muted')} ${Nova.relTime(b.created_at)} ${b.status==='complete'?`Download`:''}
` : `
No backups yet.
`}
`; } window.bkCreateModal = () => { const accts = window._adminAcctsBK || []; const opts = accts.map(a => ``).join(''); Nova.modal('Create Backup', `
`, ` `); }; window.bkSubmitCreate = async () => { const id = +document.getElementById('bk-acct')?.value; const type = document.getElementById('bk-type')?.value; document.querySelector('.modal-overlay')?.remove(); Nova.toast('Creating backup…','info',30000); const r = await Nova.api('backup','create',{method:'POST',body:{account_id:id,type}}); Nova.toast(r?.message||(r?.success?'Backup complete':'Failed'), r?.success?'success':'error'); if (r?.success) adminPage('backups'); }; window.bkRestore = (id) => { Nova.confirm('Restore this backup? Current files and databases will be overwritten. IRREVERSIBLE.', async () => { Nova.toast('Restoring…','info',30000); const r = await Nova.api('backup','restore',{method:'POST',body:{id}}); Nova.toast(r?.message||(r?.success?'Restored':'Failed'), r?.success?'success':'error'); }, true); }; window.bkDelete = (id) => { Nova.confirm('Delete this backup?', async () => { const r = await Nova.api('backup','delete',{method:'POST',body:{id}}); Nova.toast(r?.message||(r?.success?'Deleted':'Failed'), r?.success?'success':'error'); if (r?.success) adminPage('backups'); }, true); }; window.bkScheduleModal = () => { const accts = window._adminAcctsBK || []; const opts = accts.map(a => ``).join(''); Nova.modal('Configure Backup Schedule', `
`, ` `); }; window.bkScheduleForAccount = async (id, user) => { const r = await Nova.api('backup','get-schedule',{params:{account_id:id}}); const s = r?.data || {}; Nova.modal(`Schedule: ${user}`, `
`, ` `); }; window.bkSaveSchedule = async () => { const id = +document.getElementById('bks-acct')?.value; await bkSaveScheduleFor(id); }; window.bkSaveScheduleFor = async (id) => { const r = await Nova.api('backup','schedule',{method:'POST',body:{ account_id: id, frequency: document.getElementById('bks-freq')?.value, type: document.getElementById('bks-type')?.value, retain: +document.getElementById('bks-retain')?.value, }}); document.querySelector('.modal-overlay')?.remove(); Nova.toast(r?.message||(r?.success?'Schedule saved':'Failed'), r?.success?'success':'error'); }; // ── Cloudflare Integration (#16) ────────────────────────────────────────── async function cloudflare() { const acctRes = await Nova.api('accounts','list',{params:{limit:500}}); const accts = acctRes?.data?.accounts || []; window._adminAcctsCF = accts; return `
Account Credentials

Select an account to configure or view its Cloudflare API key.

`; } window.cfLoadAccount = async (id) => { if (!id) { document.getElementById('cf-acct-panel').innerHTML=''; return; } const r = await Nova.api('cloudflare','get-credentials',{params:{account_id:id}}); const c = r?.data || {}; document.getElementById('cf-acct-panel').innerHTML = `
${c.cf_api_key ? `

Key on file: ${Nova.escHtml(c.cf_api_key)}

` : ''}`; document.getElementById('cf-zones-panel').style.display = ''; window._cfCurrentAcct = id; }; window.cfSaveCredentials = async (id) => { const r = await Nova.api('cloudflare','save-credentials',{method:'POST',body:{ account_id: id, api_key: document.getElementById('cf-apikey')?.value, email: document.getElementById('cf-email')?.value, }}); Nova.toast(r?.message||(r?.success?'Saved':'Failed'), r?.success?'success':'error'); }; window.cfTestKey = async (id) => { const r = await Nova.api('cloudflare','test-key',{method:'POST',body:{ account_id: id, api_key: document.getElementById('cf-apikey')?.value, email: document.getElementById('cf-email')?.value, }}); Nova.toast(r?.message||(r?.data?.valid?'API key is valid':'Invalid key'), r?.data?.valid?'success':'error'); }; window.cfRefreshZones = async () => { const id = window._cfCurrentAcct; if (!id) { Nova.toast('Select an account first','error'); return; } const r = await Nova.api('cloudflare','list-zones',{params:{account_id:id}}); const zones = r?.data?.zones || r?.data || []; const body = document.getElementById('cf-zones-body'); if (!body) return; if (!r?.success) { body.innerHTML=`

${Nova.escHtml(r?.message||'Failed to load zones')}

`; return; } if (!zones.length) { body.innerHTML='

No zones found for these credentials.

'; return; } body.innerHTML = ` ${zones.map(z=>``).join('')}
ZoneStatusPlanActions
${Nova.escHtml(z.name)}
${Nova.escHtml(z.id)}
${Nova.badge(z.status,z.status==='active'?'green':'yellow')} ${Nova.escHtml(z.plan?.name||'—')}
`; }; window.cfViewRecords = async (zoneId, domain, acctId) => { const r = await Nova.api('cloudflare','list-records',{method:'POST',body:{zone_id:zoneId,account_id:acctId}}); const records = r?.data?.records || r?.data || []; Nova.modal(`CF DNS: ${domain}`, !records.length ? '

No records.

' : ` ${records.map(rec=>``).join('')}
NameTypeValueProxy
${Nova.escHtml(rec.name)} ${Nova.badge(rec.type,'default')} ${Nova.escHtml(rec.content)}
`); }; window.cfToggleProxy = async (zoneId, recordId, proxied, acctId) => { const r = await Nova.api('cloudflare','toggle-proxy',{method:'POST',body:{zone_id:zoneId,record_id:recordId,proxied,account_id:acctId}}); Nova.toast(r?.message||(r?.success?'Updated':'Failed'), r?.success?'success':'error'); }; window.cfSync = async (zoneId, domain, dir, acctId) => { const action = dir==='to' ? 'sync-to-cf' : 'sync-from-cf'; const label = dir==='to' ? 'Pushing to Cloudflare' : 'Pulling from Cloudflare'; Nova.toast(`${label}…`,'info',10000); const r = await Nova.api('cloudflare',action,{method:'POST',body:{zone_id:zoneId,domain,account_id:acctId}}); Nova.toast(r?.message||(r?.success?'Done':'Failed'), r?.success?'success':'error'); }; window.cfPurge = async (zoneId, acctId) => { Nova.confirm('Purge all Cloudflare cache for this zone?', async () => { const r = await Nova.api('cloudflare','purge-cache',{method:'POST',body:{zone_id:zoneId,account_id:acctId}}); Nova.toast(r?.message||(r?.success?'Cache purged':'Failed'), r?.success?'success':'error'); }); }; // ── TOTP / 2FA Admin (#17) ──────────────────────────────────────────────── async function twofa() { const res = await Nova.api('accounts','list',{params:{limit:500}}); const users = res?.data?.accounts || []; return `
User 2FA Status
${users.map(u=>``).join('')}
UsernameEmailRole2FA StatusActions
${Nova.escHtml(u.username)} ${Nova.escHtml(u.email||'—')} ${Nova.badge(u.role||'user','default')}
`; } window.totpCheckStatus = async (userId) => { const r = await Nova.api('totp','admin-status',{method:'POST',body:{user_id:userId}}); const el = document.getElementById(`totp-status-${userId}`); if (!el) return; const enabled = r?.data?.totp_enabled; el.innerHTML = enabled ? Nova.badge('Enabled','green') : Nova.badge('Disabled','muted'); }; window.totpAdminDisable = (userId, username) => { Nova.confirm(`Force-disable 2FA for ${username}? Use only for account recovery when user cannot log in.`, async () => { const r = await Nova.api('totp','admin-disable',{method:'POST',body:{user_id:userId}}); Nova.toast(r?.message||(r?.success?'2FA disabled':'Failed'), r?.success?'success':'error'); if (r?.success) { const el = document.getElementById(`totp-status-${userId}`); if (el) el.innerHTML = Nova.badge('Disabled','muted'); } }, true); }; // ── Nginx Proxy Manager ─────────────────────────────────────────────────────── async function nginxProxy() { const [statusR, hostsR] = await Promise.all([ Nova.api('proxy', 'status'), Nova.api('proxy', 'hosts'), ]); const s = statusR?.data || {}; const hosts = hostsR?.data || (Array.isArray(hostsR) ? hostsR : []); const run = s.running; const inst = s.installed; return `
Nginx Status
${inst ? (run ? 'Running' : 'Stopped') : 'Not Installed'}
${s.version || (inst ? 'nginx' : 'click Install to set up')}
Proxy Hosts
${hosts.length}
${hosts.filter(h => h.enabled).length} active
SSL Enabled
${hosts.filter(h => h.ssl_enabled).length}
of ${hosts.length} hosts
${!inst ? `

Nginx Not Installed

Install Nginx on this VM to use it as a reverse proxy in front of Apache, or use a separate proxy VM (see Setup Guide).

` : `

Service Controls

Proxy Hosts

${hosts.length} total
${hosts.length === 0 ? `
No proxy hosts yet. Click Sync Accounts to auto-add all hosted domains, or + Add Host to add manually.
` : `
${hosts.map(h => ` `).join('')}
Domain Upstream SSL Status Actions
${Nova.escHtml(h.domain)} ${Nova.escHtml(h.upstream)} ${h.ssl_enabled ? Nova.badge('SSL','green') : Nova.badge('HTTP','muted')} ${h.enabled ? Nova.badge('Active','green') : Nova.badge('Disabled','red')}
`}
`}`; } window.proxyInstall = async () => { if (!confirm('Install Nginx on this VM? This will run apt-get install nginx.')) return; Nova.toast('Installing nginx...', 'info'); const r = await Nova.api('proxy', 'install', { method: 'POST' }); Nova.toast(r?.data?.result || r?.message || 'Done', r?.data?.result === 'installed' ? 'success' : 'info'); Nova.loadPage('nginx-proxy', window._novaPages); }; window.proxyControl = async (action) => { const r = await Nova.api('proxy', 'control', { method: 'POST', body: { action } }); Nova.toast(r?.data?.result || r?.message || action + ' done', 'success'); setTimeout(() => Nova.loadPage('nginx-proxy', window._novaPages), 800); }; window.proxySync = async () => { const r = await Nova.api('proxy', 'sync', { method: 'POST' }); Nova.toast(`Synced: ${r?.data?.added ?? 0} new hosts added`, 'success'); Nova.loadPage('nginx-proxy', window._novaPages); }; window.proxyAddHost = () => { Nova.modal('Add Proxy Host', `
e.g. http://127.0.0.1:80 or http://10.0.0.2:8080
`, async () => { const domain = document.getElementById('ph-domain')?.value?.trim(); const upstream = document.getElementById('ph-upstream')?.value?.trim(); if (!domain || !upstream) { Nova.toast('Domain and upstream required', 'error'); return; } const r = await Nova.api('proxy', 'hosts', { method: 'POST', body: { domain, upstream, ssl_enabled: document.getElementById('ph-ssl')?.checked ? 1 : 0 } }); Nova.toast(r?.success ? 'Host added' : (r?.message || 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) Nova.loadPage('nginx-proxy', window._novaPages); }); }; window.proxyEditHost = async (id) => { const hostsR = await Nova.api('proxy', 'hosts'); const hosts = hostsR?.data || (Array.isArray(hostsR) ? hostsR : []); const h = hosts.find(x => x.id == id); if (!h) return; Nova.modal('Edit Proxy Host', `
Leave blank to use auto-generated config
`, async () => { const r = await Nova.api('proxy', 'host', { method: 'PUT', body: { id, domain: document.getElementById('phe-domain')?.value?.trim(), upstream: document.getElementById('phe-upstream')?.value?.trim(), ssl_enabled: document.getElementById('phe-ssl')?.checked ? 1 : 0, custom_config: document.getElementById('phe-custom')?.value?.trim() || null, } }); Nova.toast(r?.success ? 'Updated' : (r?.message || 'Failed'), r?.success ? 'success' : 'error'); if (r?.success) Nova.loadPage('nginx-proxy', window._novaPages); }); }; window.proxyToggle = async (id, enable) => { const r = await Nova.api('proxy', 'toggle', { method: 'POST', body: { id, enabled: enable } }); Nova.toast(r?.success ? (enable ? 'Enabled' : 'Disabled') : 'Failed', r?.success ? 'success' : 'error'); if (r?.success) Nova.loadPage('nginx-proxy', window._novaPages); }; window.proxyDeleteHost = (id, domain) => { Nova.confirm(`Delete proxy host for ${domain}?`, async () => { const r = await Nova.api('proxy', 'host', { method: 'DELETE', body: { id } }); Nova.toast(r?.success ? 'Deleted' : 'Failed', r?.success ? 'success' : 'error'); if (r?.success) Nova.loadPage('nginx-proxy', window._novaPages); }, true); }; window.proxySetupInstructions = async () => { const scriptUrl = '/api/proxy/setup-script'; Nova.modal('Nginx Proxy Setup Guide', `

Option A — Local (Nginx on this VM)

Install Nginx alongside Apache on this VM. Nginx listens on ports 80/443 and forwards to Apache. Best for SSL termination and caching.

  1. Click Install Nginx Locally on the main Nginx Proxy page
  2. Move Apache to port 8080: edit /etc/apache2/ports.conf → change Listen 80 to Listen 8080
  3. Update upstream in all proxy hosts to http://127.0.0.1:8080
  4. Click Sync Accounts to auto-populate proxy hosts from your hosted accounts
  5. Click Reload Config to apply changes

Option B — Remote Proxy VM (Recommended for production)

Run a dedicated Nginx proxy VM in front of this NovaCPX VM. Traffic flows: Internet → FortiGate → Nginx Proxy VM → NovaCPX VM (Apache).

  1. Create a new VM on Proxmox (Ubuntu 22.04, 1 vCPU, 1GB RAM)
  2. Run the setup script below on the new VM as root
  3. Point FortiGate VIPs to the proxy VM IP (ports 80/443)
  4. Set the proxy upstream to this NovaCPX VM IP (http://10.48.200.110:80)
  5. Add proxy hosts for each domain from your NovaCPX admin panel

Automated Setup Script

Run this on the target VM (local or remote) as root:

curl -sk https://YOUR_NOVACPX_IP:8882/api/proxy/setup-script | bash

Or download and review before running:

curl -sk https://YOUR_NOVACPX_IP:8882/api/proxy/setup-script -o proxy-setup.sh
cat proxy-setup.sh # review
bash proxy-setup.sh

Integration with VirtualHost Manager

When proxy mode is active, NovaCPX automatically:

`, null, { cancelLabel: 'Close', showConfirm: false }); };