/**
* 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 `
`;
}
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 => `
`).join('')}
${[
['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 `
${item.label}
${item.used}${item.unit} / ${item.limit > 0 ? item.limit + item.unit : '∞'}
`;
}).join('');
}
}
/* ── Domains ────────────────────────────────────────────────────────────── */
async function domains(el) {
el.innerHTML = `
`;
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 = `| Domain | Type | SSL | Actions |
${rows.map(d => `
| ${d.domain} |
${Nova.badge(d.type, d.is_primary ? 'primary' : 'default')} |
${d.ssl_enabled ? Nova.badge('SSL','green') : ``} |
${!d.is_primary ? `` : ''}
|
`).join('')}
`;
}
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 = `
`;
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 = `| Email | Quota | Status | Actions |
${res.data.map(a => `
| ${a.email} |
${a.quota_mb > 0 ? a.quota_mb + 'MB' : 'Unlimited'} |
${Nova.badge(a.status, a.status === 'active' ? 'green' : 'yellow')} |
Webmail
|
`).join('')}
`;
}
window.loadEmailList = loadEmailList;
window.addEmailAccount = async () => {
const dr = await Nova.api('domains', 'list');
const domains = (dr?.data || []).map(d => d.domain).filter(Boolean);
const domainOpts = domains.length
? domains.map(d => ``).join('')
: '';
Nova.modal('Add Email Account', `
`,
``
);
};
window.submitAddEmail = async () => {
const local = (document.getElementById('em-local')?.value || '').trim();
const domain = document.getElementById('em-domain')?.value || '';
if (!local) { Nova.toast('Enter a username', 'error'); return; }
if (!domain) { Nova.toast('Select a domain', 'error'); return; }
const res = await Nova.api('email', 'create', { method: 'POST', body: {
email: `${local}@${domain}`,
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 = `| From | To | |
${res.data.map(f => `| ${f.source} | ${f.destination} |
|
`).join('')}
`;
}
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 = `
`;
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 = `| Database | User | Type | Size | Actions |
${res.data.map(d => `
| ${d.db_name} |
${d.db_user} |
${Nova.badge(d.db_type,'default')} |
${d.size || '—'} |
|
`).join('')}
`;
}
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 = `
`;
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 = `| Username | Directory | Quota | Actions |
${res.data.map(f => `
| ${f.username} |
${f.home_dir} |
${f.quota_mb > 0 ? f.quota_mb+'MB' : 'Unlimited'} |
|
`).join('')}
`;
}
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 = `
`;
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 = `| Domain | Type | Expires | Status | Actions |
${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 `
| ${c.domain} |
${Nova.badge(c.type,'default')} |
${c.expires_at || '—'} |
${Nova.badge(badge, badgeType)} |
|
`;
}).join('')}
`;
}
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 = `
`;
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 = `
`;
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 = ``;
}
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 = `
`;
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 = `| Name | Size | Perms | Modified | Actions |
${path !== '/' && path !== '/public_html' ? `| ← .. |
` : ''}
${res.data.items.map(f => `
|
${f.type === 'dir'
? `📁 ${f.name}`
: `📄 ${f.name}`}
|
${f.size || '—'} |
${f.perms} |
${f.modified} |
${f.type === 'file' ? `` : ''}
|
`).join('')}
`;
}
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 = `
`;
};
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 = `
`;
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 = `
`;
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 = `
| Date | Type | Size | Status | Actions |
${list.map(b => `
| ${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`
: ''}
|
`).join('')}
`;
}
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 = `
`;
}
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}
`;
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!
`
: `| Name | App | Status | Actions |
${containers.map(c=>`
| ${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'
? `
`
: ``}
|
`).join('')}
`}`;
} 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');
});