/** * NovaCPX User Panel JS — all pages */ /* ── Auth guard ──────────────────────────────────────────────────────────── */ let _user = null; async function initUser() { const res = await Nova.api('auth', 'me'); if (!res || !res.success) { document.getElementById('auth-check').innerHTML = renderLogin(); document.getElementById('main-layout').style.display = 'none'; return false; } _user = res.data; document.getElementById('user-name').textContent = _user.username || 'User'; return true; } function renderLogin() { return `
User Portal · Port 8880
`; } async function doLogin() { const u = document.getElementById('li-user')?.value; const p = document.getElementById('li-pass')?.value; const err = document.getElementById('li-err'); const res = await Nova.api('auth', 'login', { method: 'POST', body: { username: u, password: p } }); if (res?.success) { if (res.data?.portal_url && !res.data.portal_url.includes(':8880')) { location.href = res.data.portal_url; } else { location.reload(); } } else { if (err) { err.textContent = res?.message || 'Login failed'; err.style.display = 'block'; } } } window.doLogin = doLogin; /* ── Pages ───────────────────────────────────────────────────────────────── */ const userPages = { dashboard, domains, email, databases, ftp, ssl, php: phpPage, cron, files, stats: statsPage, backups, 'change-password': changePasswordPage, }; /* ── Dashboard ───────────────────────────────────────────────────────────── */ async function dashboard(el) { el.innerHTML = `
${['Disk','Bandwidth','Emails','Databases'].map(l => `
${l}
`).join('')}
Quick Access
${[ ['ni-domains','Domains','domains'], ['ni-email','Email','email'], ['ni-databases','Databases','databases'], ['ni-ftp','FTP','ftp'], ['ni-ssl','SSL','ssl'], ['ni-php','PHP','php'], ['ni-cron','Cron Jobs','cron'], ['ni-files','File Manager','files'], ].map(([icon,label,page]) => ` `).join('')}
`; const res = await Nova.api('stats', 'account'); if (res?.success) { const d = res.data; const rings = document.getElementById('dash-rings'); rings.innerHTML = [ { label: 'Disk', used: d.disk_mb, limit: d.disk_limit, unit: 'MB' }, { label: 'Databases', used: d.databases, limit: d.db_limit, unit: '' }, { label: 'Email Accts', used: d.emails, limit: d.email_limit, unit: '' }, { label: 'FTP Accts', used: d.ftp, limit: d.ftp_limit, unit: '' }, ].map(item => { const pct = item.limit > 0 ? Math.min(100, Math.round(item.used / item.limit * 100)) : 0; const r = 26, circ = 2 * Math.PI * r; const dash = circ - (pct / 100) * circ; const color = pct > 85 ? 'var(--red)' : pct > 65 ? 'var(--yellow)' : 'var(--primary)'; return `
${pct}%
${item.label}
${item.used}${item.unit} / ${item.limit > 0 ? item.limit + item.unit : '∞'}
`; }).join(''); } } /* ── Domains ────────────────────────────────────────────────────────────── */ async function domains(el) { el.innerHTML = `
Loading…
`; await loadDomainsList(); } async function loadDomainsList() { const el = document.getElementById('domains-list'); if (!el) return; const res = await Nova.api('domains', 'list'); if (!res?.success) { el.innerHTML = '
No domains
'; return; } const rows = res.data; el.innerHTML = ` ${rows.map(d => ``).join('')}
DomainTypeSSLActions
${d.domain} ${Nova.badge(d.type, d.is_primary ? 'primary' : 'default')} ${d.ssl_enabled ? Nova.badge('SSL','green') : ``} ${!d.is_primary ? `` : ''}
`; } window.loadDomainsList = loadDomainsList; window.addDomain = (type) => { const fields = type === 'subdomain' ? `` : ``; Nova.modal(`Add ${type.charAt(0).toUpperCase()+type.slice(1)}`, `
${fields}
`, `` ); }; window.submitAddDomain = async (type) => { let body = { type }; if (type === 'subdomain') body.subdomain = document.getElementById('md-sub')?.value; else body.domain = document.getElementById('md-domain')?.value; const action = type === 'subdomain' ? 'add-subdomain' : type === 'alias' ? 'add-alias' : 'add-addon'; const res = await Nova.api('domains', action, { method: 'POST', body }); if (res?.success) { Nova.toast(res.message,'success'); document.querySelector('.modal-overlay')?.remove(); loadDomainsList(); } else Nova.toast(res?.message || 'Failed','error'); }; window.removeDomain = (id, domain) => { Nova.confirm(`Remove domain ${domain}? This deletes the vhost and DNS zone.`, async () => { const res = await Nova.api('domains', 'remove', { method: 'POST', body: { id } }); if (res?.success) { Nova.toast('Domain removed','success'); loadDomainsList(); } else Nova.toast(res?.message || 'Failed','error'); }, true); }; window.issueSSL = async (domainId, domain) => { Nova.toast(`Issuing Let's Encrypt SSL for ${domain}…`,'info',6000); const res = await Nova.api('ssl', 'issue', { method: 'POST', body: { domain } }); if (res?.success) { Nova.toast('SSL issued successfully','success'); loadDomainsList(); } else Nova.toast(res?.message || 'SSL failed — check domain DNS','error',6000); }; window.issueSSL = window.issueSSL; /* ── Email ──────────────────────────────────────────────────────────────── */ async function email(el) { el.innerHTML = `
Loading…
Loading…
`; loadEmailList(); loadForwarderList(); } async function loadEmailList() { const el = document.getElementById('email-list'); if (!el) return; const res = await Nova.api('email', 'list'); if (!res?.success || !res.data.length) { el.innerHTML = '
No email accounts yet.
'; return; } el.innerHTML = ` ${res.data.map(a => ``).join('')}
EmailQuotaStatusActions
${a.email} ${a.quota_mb > 0 ? a.quota_mb + 'MB' : 'Unlimited'} ${Nova.badge(a.status, a.status === 'active' ? 'green' : 'yellow')} Webmail
`; } window.loadEmailList = loadEmailList; window.addEmailAccount = () => { Nova.modal('Add Email Account', `
`, `` ); }; window.submitAddEmail = async () => { const res = await Nova.api('email', 'create', { method: 'POST', body: { email: document.getElementById('em-addr')?.value, password: document.getElementById('em-pass')?.value, quota_mb: parseInt(document.getElementById('em-quota')?.value || '0'), }}); if (res?.success) { Nova.toast('Email account created','success'); document.querySelector('.modal-overlay')?.remove(); loadEmailList(); } else Nova.toast(res?.message || 'Failed','error'); }; window.changeEmailPass = (id) => { Nova.modal('Change Email Password', `
`, ``); }; window.submitEmailPass = async (id) => { const res = await Nova.api('email', 'change-password', { method: 'POST', body: { id, password: document.getElementById('ep-pass')?.value }}); if (res?.success) { Nova.toast('Password updated','success'); document.querySelector('.modal-overlay')?.remove(); } else Nova.toast(res?.message || 'Failed','error'); }; window.deleteEmail = (id, addr) => { Nova.confirm(`Delete ${addr}?`, async () => { const res = await Nova.api('email', 'delete', { method: 'POST', body: { id }}); if (res?.success) { Nova.toast('Email deleted','success'); loadEmailList(); } }, true); }; window.openWebmail = (email) => { Nova.api('webmail', 'url').then(res => { if (res?.success) window.open(res.data.url, '_blank'); }); }; async function loadForwarderList() { const el = document.getElementById('forwarder-list'); if (!el) return; const res = await Nova.api('email', 'forwarders'); if (!res?.success || !res.data.length) { el.innerHTML = '
No forwarders yet.
'; return; } el.innerHTML = ` ${res.data.map(f => ``).join('')}
FromTo
${f.source}${f.destination}
`; } window.addForwarder = () => { Nova.modal('Add Forwarder', `
`, ``); }; window.submitFwd = async () => { const res = await Nova.api('email', 'add-forwarder', { method: 'POST', body: { source: document.getElementById('fw-from')?.value, destination: document.getElementById('fw-to')?.value }}); if (res?.success) { Nova.toast('Forwarder added','success'); document.querySelector('.modal-overlay')?.remove(); loadForwarderList(); } else Nova.toast(res?.message || 'Failed','error'); }; window.deleteFwd = async (id) => { const res = await Nova.api('email', 'delete-forwarder', { method: 'POST', body: { id }}); if (res?.success) { Nova.toast('Deleted','success'); loadForwarderList(); } }; /* ── Databases ──────────────────────────────────────────────────────────── */ async function databases(el) { el.innerHTML = `
Loading…
`; loadDBList(); } async function loadDBList() { const el = document.getElementById('db-list'); if (!el) return; const res = await Nova.api('databases', 'list'); if (!res?.success || !res.data.length) { el.innerHTML = '
No databases yet.
'; return; } el.innerHTML = ` ${res.data.map(d => ``).join('')}
DatabaseUserTypeSizeActions
${d.db_name} ${d.db_user} ${Nova.badge(d.db_type,'default')} ${d.size || '—'}
`; } window.loadDBList = loadDBList; window.addDB = (type) => { Nova.modal(`Create ${type.toUpperCase()} Database`, `
`, ``); }; window.submitAddDB = async (type) => { const res = await Nova.api('databases', 'create', { method:'POST', body: { db_type: type, db_name: document.getElementById('dbn-name')?.value, db_user: document.getElementById('dbn-user')?.value, db_pass: document.getElementById('dbn-pass')?.value }}); if (res?.success) { Nova.toast('Database created','success'); document.querySelector('.modal-overlay')?.remove(); loadDBList(); } else Nova.toast(res?.message || 'Failed','error'); }; window.changeDBPass = (id) => { Nova.modal('Change DB Password', `
`, ``); }; window.submitDBPass = async (id) => { const res = await Nova.api('databases', 'change-password', { method:'POST', body:{ id, password: document.getElementById('dbp-pass')?.value }}); if (res?.success) { Nova.toast('Password updated','success'); document.querySelector('.modal-overlay')?.remove(); } else Nova.toast(res?.message,'error'); }; window.dropDB = (id, name) => { Nova.confirm(`Drop database ${name}? All data will be permanently deleted.`, async () => { const res = await Nova.api('databases', 'drop', { method:'POST', body:{ id }}); if (res?.success) { Nova.toast('Database dropped','success'); loadDBList(); } else Nova.toast(res?.message,'error'); }, true); }; /* ── FTP ────────────────────────────────────────────────────────────────── */ async function ftp(el) { el.innerHTML = `
Loading…
`; loadFTPList(); } async function loadFTPList() { const el = document.getElementById('ftp-list'); if (!el) return; const res = await Nova.api('ftp', 'list'); if (!res?.success || !res.data.length) { el.innerHTML = '
No FTP accounts yet.
'; return; } el.innerHTML = ` ${res.data.map(f => ``).join('')}
UsernameDirectoryQuotaActions
${f.username} ${f.home_dir} ${f.quota_mb > 0 ? f.quota_mb+'MB' : 'Unlimited'}
`; } window.loadFTPList = loadFTPList; window.addFTP = () => { Nova.modal('Add FTP Account', `
`, ``); }; window.submitAddFTP = async () => { const res = await Nova.api('ftp', 'create', { method:'POST', body:{ username: document.getElementById('ftp-user')?.value, password: document.getElementById('ftp-pass')?.value, home_dir: document.getElementById('ftp-dir')?.value || null }}); if (res?.success) { Nova.toast('FTP account created','success'); document.querySelector('.modal-overlay')?.remove(); loadFTPList(); } else Nova.toast(res?.message||'Failed','error'); }; window.changeFTPPass = (id) => { Nova.modal('Change FTP Password', `
`, ``); }; window.deleteFTP = (id, user) => { Nova.confirm(`Delete FTP account ${user}?`, async () => { const res = await Nova.api('ftp', 'delete', { method:'POST', body:{id}}); if (res?.success) { Nova.toast('Deleted','success'); loadFTPList(); } }, true); }; /* ── SSL ────────────────────────────────────────────────────────────────── */ async function ssl(el) { el.innerHTML = `
Loading…
`; loadSSLList(); } async function loadSSLList() { const el = document.getElementById('ssl-list'); if (!el) return; const res = await Nova.api('ssl', 'list'); if (!res?.success || !res.data.length) { el.innerHTML = '
No SSL certificates yet.
'; return; } el.innerHTML = ` ${res.data.map(c => { const days = c.days_remaining; const status = !days ? 'unknown' : days < 7 ? 'critical' : days < 30 ? 'warning' : 'ok'; const badge = days !== null ? `${days}d` : c.status; const badgeType = status === 'critical' ? 'red' : status === 'warning' ? 'yellow' : 'green'; return ``; }).join('')}
DomainTypeExpiresStatusActions
${c.domain} ${Nova.badge(c.type,'default')} ${c.expires_at || '—'} ${Nova.badge(badge, badgeType)}
`; } window.loadSSLList = loadSSLList; window.issueNewSSL = () => { Nova.api('domains','list').then(res => { const opts = (res?.data || []).map(d => ``).join(''); Nova.modal("Issue Let's Encrypt SSL", `
`, ``); }); }; window.submitIssueSSL = async () => { const domain = document.getElementById('ssl-dom')?.value; Nova.toast(`Issuing SSL for ${domain}…`, 'info', 8000); document.querySelector('.modal-overlay')?.remove(); const res = await Nova.api('ssl', 'issue', { method:'POST', body:{ domain, email: document.getElementById('ssl-email')?.value }}); if (res?.success) { Nova.toast('SSL issued!','success'); loadSSLList(); } else Nova.toast(res?.message || 'SSL issue failed','error',8000); }; window.renewCert = async (id) => { Nova.toast('Renewing…','info'); const res = await Nova.api('ssl', 'renew', { method:'POST', body:{cert_id:id}}); if (res?.success) { Nova.toast('Renewed','success'); loadSSLList(); } else Nova.toast(res?.message,'error'); }; window.deleteCert = (id, domain) => { Nova.confirm(`Remove SSL cert for ${domain}?`, async () => { const res = await Nova.api('ssl', 'delete', { method:'POST', body:{cert_id:id}}); if (res?.success) { Nova.toast('Removed','success'); loadSSLList(); } }, true); }; /* ── PHP Manager ────────────────────────────────────────────────────────── */ async function phpPage(el) { el.innerHTML = `
PHP Version
Loading…
PHP Settings
Loading…
`; const [versRes, cfgRes] = await Promise.all([ Nova.api('php', 'versions'), Nova.api('php', 'config'), ]); if (versRes?.success) { document.getElementById('php-versions').innerHTML = versRes.data.map(v => `
PHP ${v.version} ${v.is_default ? Nova.badge('default','primary') : ''} ${!v.installed ? Nova.badge('not installed','muted') : ''}
${v.installed ? `` : ''}
`).join(''); } if (cfgRes?.success) { const c = cfgRes.data; document.getElementById('php-settings').innerHTML = `
`; } } window.switchPHP = async (ver) => { const res = await Nova.api('php', 'switch-version', { method:'POST', body:{ version: ver }}); if (res?.success) { Nova.toast(`Switched to PHP ${ver}`,'success'); phpPage(document.getElementById('page-content')); } else Nova.toast(res?.message,'error'); }; window.savePHPSettings = async () => { const res = await Nova.api('php', 'update-config', { method:'POST', body:{ memory_limit: document.getElementById('php-mem')?.value, max_execution_time: document.getElementById('php-exec')?.value, upload_max_filesize: document.getElementById('php-upload')?.value, post_max_size: document.getElementById('php-post')?.value, }}); if (res?.success) Nova.toast('PHP settings saved','success'); else Nova.toast(res?.message,'error'); }; /* ── Cron Jobs ──────────────────────────────────────────────────────────── */ async function cron(el) { el.innerHTML = `
Loading…
`; loadCronList(); } async function loadCronList() { const el = document.getElementById('cron-list'); if (!el) return; const res = await Nova.api('cron', 'list'); if (!res?.success || !res.data.length) { el.innerHTML = '
No cron jobs yet.
'; return; } el.innerHTML = ` ${res.data.map(j => ``).join('')}
ScheduleCommandStatusActions
${j.minute} ${j.hour} ${j.day} ${j.month} ${j.weekday} ${j.command}
`; } window.loadCronList = loadCronList; window.addCron = () => { Nova.modal('Add Cron Job', `
${['minute','hour','day','month','weekday'].map(f => `
`).join('')}
* = every | */5 = every 5 | 0 = midnight/Jan/Mon
`, ``); }; window.submitCron = async () => { const res = await Nova.api('cron', 'create', { method:'POST', body:{ command: document.getElementById('cr-cmd')?.value, minute: document.getElementById('cr-minute')?.value || '*', hour: document.getElementById('cr-hour')?.value || '*', day: document.getElementById('cr-day')?.value || '*', month: document.getElementById('cr-month')?.value || '*', weekday: document.getElementById('cr-weekday')?.value|| '*', }}); if (res?.success) { Nova.toast('Cron job added','success'); document.querySelector('.modal-overlay')?.remove(); loadCronList(); } else Nova.toast(res?.message,'error'); }; window.toggleCron = async (id) => { await Nova.api('cron', 'toggle', { method:'POST', body:{id}}); loadCronList(); }; window.deleteCron = (id) => { Nova.confirm('Delete this cron job?', async () => { const res = await Nova.api('cron', 'delete', { method:'POST', body:{id}}); if (res?.success) { Nova.toast('Deleted','success'); loadCronList(); } }, true); }; /* ── File Manager ───────────────────────────────────────────────────────── */ let _fmPath = '/public_html'; async function files(el) { el.innerHTML = `
${_fmPath}
Loading…
`; loadFMList(_fmPath); } async function loadFMList(path) { _fmPath = path; const pathEl = document.getElementById('fm-path'); if (pathEl) pathEl.textContent = path; const el = document.getElementById('fm-list'); if (!el) return; const res = await Nova.api('files', 'list', { params: { path }}); if (!res?.success) { el.innerHTML = `
${res?.message || 'Error loading directory'}
`; return; } const parentPath = path.includes('/') ? path.replace(/\/[^/]+$/, '') || '/' : '/'; el.innerHTML = ` ${path !== '/' && path !== '/public_html' ? `` : ''} ${res.data.items.map(f => ``).join('')}
NameSizePermsModifiedActions
← ..
${f.type === 'dir' ? `📁 ${f.name}` : `📄 ${f.name}`} ${f.size || '—'} ${f.perms} ${f.modified} ${f.type === 'file' ? `` : ''}
`; } window.fmNav = (p) => loadFMList(p); window.fmEdit = async (path, name) => { const res = await Nova.api('files', 'read', { params: { path }}); if (!res?.success) { Nova.toast(res?.message || 'Cannot read file','error'); return; } const edEl = document.getElementById('fm-editor'); edEl.style.display = 'block'; edEl.innerHTML = `
Editing: ${name}
`; }; window.fmSave = async (path) => { const content = document.getElementById('fm-code')?.value || ''; const res = await Nova.api('files', 'write', { method:'POST', body:{ path, content }}); if (res?.success) Nova.toast('Saved','success'); else Nova.toast(res?.message || 'Save failed','error'); }; window.fmDelete = (path, name) => { Nova.confirm(`Delete ${name}?`, async () => { const res = await Nova.api('files', 'delete', { method:'POST', body:{ path }}); if (res?.success) { Nova.toast('Deleted','success'); loadFMList(_fmPath); } else Nova.toast(res?.message,'error'); }, true); }; window.fmMkdir = () => { Nova.modal('New Folder', `
`, ``); }; window.fmRename = (path, name) => { const dir = path.replace(/\/[^/]+$/, ''); Nova.modal('Rename', `
`, ``); }; window.fmChmod = (path, current) => { Nova.modal('Change Permissions', `
`, ``); }; window.fmUpload = () => { Nova.modal('Upload File', `
`, ``); }; window.submitFMUpload = async () => { const fileInput = document.getElementById('fm-upfile'); if (!fileInput?.files[0]) return; const fd = new FormData(); fd.append('file', fileInput.files[0]); fd.append('path', _fmPath); const res = await fetch(`/api/files/upload?path=${encodeURIComponent(_fmPath)}`, { method:'POST', credentials:'include', body: fd }).then(r => r.json()); if (res?.success) { Nova.toast('Uploaded','success'); document.querySelector('.modal-overlay')?.remove(); loadFMList(_fmPath); } else Nova.toast(res?.message || 'Upload failed','error'); }; /* ── Stats ──────────────────────────────────────────────────────────────── */ async function statsPage(el) { el.innerHTML = `
Loading…
`; const res = await Nova.api('stats', 'account'); if (!res?.success) return; const d = res.data; document.getElementById('stats-grid').innerHTML = [ { label: 'Disk Used', val: d.disk_mb + ' MB', limit: d.disk_limit > 0 ? `/ ${d.disk_limit} MB` : '', pct: d.disk_limit > 0 ? Math.min(100,(d.disk_mb/d.disk_limit*100)) : 0 }, { label: 'Databases', val: d.databases, limit: d.db_limit > 0 ? `/ ${d.db_limit}` : '', pct: d.db_limit > 0 ? Math.min(100,d.databases/d.db_limit*100) : 0 }, { label: 'Email Accounts', val: d.emails, limit: d.email_limit > 0 ? `/ ${d.email_limit}` : '', pct: d.email_limit > 0 ? Math.min(100,d.emails/d.email_limit*100) : 0 }, { label: 'FTP Accounts', val: d.ftp, limit: d.ftp_limit > 0 ? `/ ${d.ftp_limit}` : '', pct: d.ftp_limit > 0 ? Math.min(100,d.ftp/d.ftp_limit*100) : 0 }, { label: 'Domains', val: d.domains, limit: '', pct: 0 }, { label: 'Inodes', val: d.inodes.toLocaleString(), limit: '', pct: 0 }, ].map(item => `
${item.label}
${item.val} ${item.limit}
${item.pct > 0 ? `
${Nova.progressBar(Math.round(item.pct))}
` : ''}
`).join(''); } /* ── Backups ────────────────────────────────────────────────────────────── */ async function backups(el) { el.innerHTML = `
Loading…
`; const res = await Nova.api('system', 'audit-log', { params:{ limit:5 }}); document.getElementById('backup-list').innerHTML = `
Backup management is being configured by your hosting provider.
Contact support to request a manual backup.
`; } window.createBackup = () => Nova.toast('Backup request submitted — you will be notified when ready.','info'); /* ── Navigation ─────────────────────────────────────────────────────────── */ const navItems = [ { id: 'dashboard', label: 'Dashboard', icon: 'ni-dashboard' }, { id: 'domains', label: 'Domains', icon: 'ni-domains' }, { id: 'email', label: 'Email', icon: 'ni-email' }, { id: 'databases', label: 'Databases', icon: 'ni-databases' }, { id: 'ftp', label: 'FTP', icon: 'ni-ftp' }, { id: 'ssl', label: 'SSL / TLS', icon: 'ni-ssl' }, { id: 'php', label: 'PHP', icon: 'ni-php' }, { id: 'cron', label: 'Cron Jobs', icon: 'ni-cron' }, { id: 'files', label: 'File Manager', icon: 'ni-files' }, { id: 'stats', label: 'Statistics', icon: 'ni-stats' }, { id: 'backups', label: 'Backups', icon: 'ni-backups' }, { id: 'change-password', label: 'Change Password', icon: 'ni-lock' }, ]; let _activePage = 'dashboard'; function renderNav() { const nav = document.getElementById('sidebar-nav'); if (!nav) return; nav.innerHTML = navItems.map(n => ` ${n.label} `).join(''); } window.userNav = (page) => { _activePage = page; renderNav(); const content = document.getElementById('page-content'); if (!content) return; content.innerHTML = '
Loading…
'; if (userPages[page]) userPages[page](content); }; /* ── Change Password ─────────────────────────────────────────────────────── */ async function changePasswordPage(el) { el.innerHTML = `
Update Your Password
`; } window.submitChangePassword = async () => { const current = document.getElementById('cp-current')?.value; const newPass = document.getElementById('cp-new')?.value; const confirm = document.getElementById('cp-confirm')?.value; if (!current || !newPass || !confirm) { Nova.toast('All fields required', 'error'); return; } if (newPass !== confirm) { Nova.toast('New passwords do not match', 'error'); return; } const res = await Nova.api('auth', 'change-password', { method: 'POST', body: { current_password: current, new_password: newPass, confirm_password: confirm }, }); if (res?.success) { Nova.toast('Password updated successfully', 'success'); document.getElementById('cp-current').value = ''; document.getElementById('cp-new').value = ''; document.getElementById('cp-confirm').value = ''; } else { Nova.toast(res?.message || 'Failed to update password', 'error'); } }; /* ── Boot ────────────────────────────────────────────────────────────────── */ document.addEventListener('DOMContentLoaded', async () => { const ok = await initUser(); if (!ok) return; renderNav(); window.userNav('dashboard'); });