/** * 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'; document.getElementById('auth-check').style.display = 'none'; document.getElementById('main-layout').style.display = ''; // Show impersonation banner if an admin/reseller is acting as this user if (_user.impersonated_by) { const imp = _user.impersonated_by; const returnUrl = imp.role === 'reseller' ? location.href.replace(/:\d+/, ':8881') : location.href.replace(/:\d+/, ':8882'); const banner = document.createElement('div'); banner.id = 'impersonation-banner'; banner.style.cssText = [ 'position:fixed;top:0;left:0;right:0;z-index:99998', 'background:linear-gradient(135deg,#f59e0b,#d97706)', 'color:#fff;font-size:.82rem;font-weight:600', 'display:flex;align-items:center;justify-content:center;gap:1rem', 'padding:.45rem 1rem', 'box-shadow:0 2px 8px rgba(0,0,0,.25)', ].join(';'); banner.innerHTML = ` Acting as ${Nova.escHtml(_user.username)} — logged in as ${Nova.escHtml(imp.username)} (${imp.role}) `; document.body.prepend(banner); // Push content down so the fixed banner doesn't overlap const layout = document.getElementById('main-layout'); if (layout) layout.style.marginTop = '36px'; } return true; } window.exitImpersonation = async () => { Nova.loading('Returning…'); const res = await Nova.api('auth', 'unimpersonate', { method: 'POST' }); Nova.loadingDone(); if (res?.success && res.data?.portal_url) { window.location.href = res.data.portal_url; } else { Nova.toast(res?.message || 'Could not return', 'error'); } }; 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'); Nova.loading('Signing in…'); const res = await Nova.api('auth', 'login', { method: 'POST', body: { username: u, password: p } }); Nova.loadingDone(); 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, docker: dockerPage, 'change-password': changePasswordPage, }; /* ── Dashboard ───────────────────────────────────────────────────────────── */ const _quickIcons = { domains: '', email: '', databases: '', ftp: '', ssl: '', php: '', cron: '', files: '', }; async function dashboard(el) { el.innerHTML = `
${['Disk','Databases','Email Accts','FTP Accts'].map(l => `
${l}
`).join('')}
Quick Access
${[ ['domains','Domains'],['email','Email'],['databases','Databases'],['ftp','FTP'], ['ssl','SSL'],['php','PHP'],['cron','Cron Jobs'],['files','File Manager'], ].map(([page, label]) => ` `).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); }; function _sslStream(params, onSuccess) { const termId = 'ssl-term-' + Date.now(); Nova.modal(`SSL: ${params.domain}`, `
Requesting certificate…\n
`, ``); const term = document.getElementById(termId); const append = t => { term.textContent += t; term.scrollTop = term.scrollHeight; }; fetch('/api/ssl/issue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), credentials: 'same-origin', }).then(resp => { if (!resp.ok) { append(`\nHTTP error ${resp.status}`); return; } const reader = resp.body.getReader(); const dec = new TextDecoder(); let buf = ''; const read = () => reader.read().then(({ done, value }) => { if (done) { append('\n[done]'); return; } buf += dec.decode(value, { stream: true }); const parts = buf.split('\n\n'); buf = parts.pop(); for (const part of parts) { const m = part.match(/^data: (.+)$/m); if (!m) continue; try { const obj = JSON.parse(m[1]); if (obj.line) { append(obj.line); } else if (obj.done) { const btn = document.getElementById('ssl-term-close'); if (btn) { btn.textContent = obj.success ? 'Done ✓' : 'Close'; btn.className = obj.success ? 'btn btn-primary' : 'btn btn-ghost'; if (obj.success) btn.onclick = () => { document.querySelector('.modal-overlay')?.remove(); if (onSuccess) onSuccess(); }; } } } catch(e) {} } read(); }).catch(err => append(`\n[error: ${err.message}]`)); read(); }).catch(err => append(`\n[error: ${err.message}]`)); } window.issueSSL = (domainId, domain) => _sslStream({ domain }, () => loadDomainsList()); 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 = () => { const domain = document.getElementById('ssl-dom')?.value; const email = document.getElementById('ssl-email')?.value; document.querySelector('.modal-overlay')?.remove(); _sslStream({ domain, email }, () => loadSSLList()); }; 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?.versions || []).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) => { Nova.loading(`Switching to PHP ${ver}…`); const res = await Nova.api('php', 'switch-version', { method:'POST', body:{ version: ver }}); Nova.loadingDone(); if (res?.success) { Nova.toast(`Switched to PHP ${ver}`,'success'); phpPage(document.getElementById('page-content')); } else Nova.toast(res?.message,'error'); }; window.savePHPSettings = async () => { Nova.loading('Saving PHP settings…'); 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, }}); Nova.loadingDone(); 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…
`; await loadBackupList(); } async function loadBackupList() { const el = document.getElementById('backup-list'); if (!el) return; const res = await Nova.api('backup', 'list'); const list = res?.data?.backups || []; if (!list.length) { el.innerHTML = `
No backups yet.
Click + Create Backup to create your first backup.
`; return; } el.innerHTML = `
${list.map(b => ``).join('')}
DateTypeSizeStatusActions
${Nova.relTime(b.created_at)} ${Nova.badge(b.type, 'blue')} ${b.size ? Nova.bytes(parseInt(b.size)) : '—'} ${Nova.badge(b.status, b.status==='complete'?'green':b.status==='running'?'yellow':'red')} ${b.status === 'complete' ? `Download` : ''}
`; } window.createBackup = () => { Nova.modal('Create Backup', `

Backups run on the server and may take a few minutes for large accounts.

`, ` ` ); }; window.submitCreateBackup = async () => { const type = document.getElementById('bk-type')?.value || 'full'; document.querySelector('.modal-overlay')?.remove(); Nova.loading('Creating backup… this may take a few minutes'); const res = await Nova.api('backup', 'create', { method: 'POST', body: { type } }); Nova.loadingDone(); if (res?.success) { Nova.toast('Backup created successfully', 'success'); loadBackupList(); } else { Nova.toast(res?.message || 'Backup failed', 'error'); } }; /* ── Navigation ─────────────────────────────────────────────────────────── */ const navGroups = [ { label: 'Overview', items: [ { id: 'dashboard', label: 'Dashboard', svg: '' }, ]}, { label: 'Hosting', items: [ { id: 'domains', label: 'Domains', svg: '' }, { id: 'email', label: 'Email', svg: '' }, { id: 'databases', label: 'Databases', svg: '' }, { id: 'ftp', label: 'FTP', svg: '' }, { id: 'ssl', label: 'SSL / TLS', svg: '' }, ]}, { label: 'Management', items: [ { id: 'php', label: 'PHP', svg: '' }, { id: 'cron', label: 'Cron Jobs', svg: '' }, { id: 'files', label: 'File Manager', svg: '' }, { id: 'stats', label: 'Statistics', svg: '' }, ]}, { label: 'Tools', items: [ { id: 'backups', label: 'Backups', svg: '' }, { id: 'docker', label: 'Docker', svg: '' }, ]}, { label: 'Account', items: [ { id: 'change-password', label: 'Change Password', svg: '' }, ]}, ]; let _activePage = 'dashboard'; function renderNav() { const nav = document.getElementById('sidebar-nav'); if (!nav) return; nav.innerHTML = navGroups.map(g => ` `).join(''); nav.querySelectorAll('[data-page]').forEach(link => { link.addEventListener('click', e => { e.preventDefault(); if (window.innerWidth <= 768) { document.getElementById('sidebar')?.classList.remove('open'); document.getElementById('sidebar-overlay')?.classList.remove('open'); document.body.style.overflow = ''; } userNav(link.dataset.page); }); }); } window.userNav = (page) => { _activePage = page; renderNav(); const allItems = navGroups.flatMap(g => g.items); const item = allItems.find(n => n.id === page); const titleEl = document.getElementById('page-title'); if (titleEl && item) titleEl.textContent = item.label; 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'); } }; /* ── Docker (#34) ────────────────────────────────────────────────────────── */ async function dockerPage(el) { el.innerHTML = '
Loading Docker…
'; const [contRes, quotaRes, catRes] = await Promise.all([ Nova.api('docker', 'containers'), Nova.api('docker', 'quota-get'), Nova.api('docker', 'catalog'), ]); const containers = contRes?.data?.containers || []; const quota = quotaRes?.data?.quota || { max_containers: 2, max_memory_mb: 512, max_cpus: 1.0 }; const catalog = catRes?.data?.catalog || {}; const used = containers.length; el.innerHTML = `
Containers Used
${used} / ${quota.max_containers}
${Nova.progressBar(Math.round(used/Math.max(quota.max_containers,1)*100))}
Max Memory / Container
${quota.max_memory_mb} MB
Max CPUs / Container
${quota.max_cpus}
Loading…
`; window._uDockerContainers = containers; window._uDockerQuota = quota; window._uDockerCatalog = catalog; window._uDockerTab = window._uDockerTab || 'my-containers'; window.uDockerTab = async (tab) => { window._uDockerTab = tab; document.querySelectorAll('[onclick^="uDockerTab"]').forEach(b => { const t = b.getAttribute('onclick').match(/'([^']+)'/)?.[1]; b.className = 'btn btn-sm ' + (t === tab ? 'btn-primary' : 'btn-ghost'); }); uDockerLoadTab(tab); }; uDockerLoadTab(window._uDockerTab); } window._uDockerTab = 'my-containers'; function uDockerLoadTab(tab) { const tc = document.getElementById('udocker-content'); if (!tc) return; const containers = window._uDockerContainers || []; const catalog = window._uDockerCatalog || {}; const quota = window._uDockerQuota || {}; if (tab === 'my-containers') { tc.innerHTML = `
${containers.length} container${containers.length===1?'':'s'}
${containers.length === 0 ? `
🐳

No containers yet. Launch an app from the catalog!

` : `
${containers.map(c=>``).join('')}
NameAppStatusActions
${Nova.escHtml(c.name)} ${Nova.escHtml(c.app_key||c.image||'—')} ${Nova.badge(c.status, c.status==='running'?'green':c.status==='stopped'?'red':'yellow')} ${c.status==='running' ? ` ` : ``}
`}`; } else if (tab === 'catalog') { tc.innerHTML = `

One-click app deployment. Each app runs as an isolated Docker container.

${Object.entries(catalog).map(([key,app])=>`
${Nova.escHtml(app.icon)}
${Nova.escHtml(app.name)}
${Nova.escHtml(app.description)}
`).join('')}
`; } } window.uDockerAct = async (cid, action) => { Nova.loading(`${action.charAt(0).toUpperCase()+action.slice(1)}ing container…`); const r = await Nova.api('docker', 'container-action', { method: 'POST', body: { container_id: cid, action } }); Nova.loadingDone(); Nova.toast(r?.success ? `Container ${action}ed` : (r?.message||'Failed'), r?.success?'success':'error'); if (r?.success) { const c = (window._uDockerContainers||[]).find(x=>x.container_id===cid); if (c) c.status = action==='stop'?'stopped':'running'; uDockerLoadTab('my-containers'); } }; window.uDockerLogs = async (cid, name) => { const r = await Nova.api('docker', 'container-logs', { params: { container_id: cid, lines: 100 } }); Nova.modal(`Logs: ${name}`, `
${Nova.escHtml(r?.data?.logs||'No logs available')}
`); }; window.uDockerLaunchModal = () => uDockerLaunchApp(null); window.uDockerLaunchApp = async (preselect) => { const catalog = window._uDockerCatalog || {}; const entries = Object.entries(catalog); const appOpts = entries.map(([k,a])=>``).join(''); window.uDockerUpdateParams = (key) => { const app = catalog[key]; if (!app) return; const tc = document.getElementById('ul-params'); if (!tc) return; tc.innerHTML = (app.params||[]).map(p=>`
`).join(''); }; const ov = Nova.modal('Launch App', `
`, ` ` ); const initialKey = preselect || entries[0]?.[0]; if (initialKey) uDockerUpdateParams(initialKey); window.uDockerLaunchSubmit = async () => { const key = document.getElementById('ul-app')?.value; const app = catalog[key]; if (!app) return; const params = {}; (app.params||[]).forEach(p => { params[p.key] = document.getElementById('ul-'+p.key)?.value||''; }); const missing = (app.params||[]).filter(p=>p.required && !params[p.key]); if (missing.length) { Nova.toast(`Required: ${missing.map(p=>p.label).join(', ')}`, 'error'); return; } ov.remove(); Nova.loading(`Launching ${app.name}… this may take a minute`); const r = await Nova.api('docker', 'launch', { method: 'POST', body: { app_key: key, params } }); Nova.loadingDone(); Nova.toast(r?.success ? `${app.name} launched!` : (r?.message||'Launch failed'), r?.success?'success':'error'); if (r?.success) { const cr = await Nova.api('docker', 'containers'); window._uDockerContainers = cr?.data?.containers || []; uDockerTab('my-containers'); } }; }; /* ── Boot ────────────────────────────────────────────────────────────────── */ document.addEventListener('DOMContentLoaded', async () => { const ok = await initUser(); if (!ok) return; document.getElementById('logout-btn')?.addEventListener('click', async e => { e.preventDefault(); await Nova.api('auth', 'logout', { method: 'POST' }); location.href = '/'; }); renderNav(); window.userNav('dashboard'); });