/**
* NovaCPX Reseller Panel JS
*/
let _rUser = null;
async function initReseller() {
const res = await Nova.api('auth', 'me');
if (!res?.success || !['admin','reseller'].includes(res.data?.role)) {
document.getElementById('auth-check').innerHTML = renderLogin();
document.getElementById('main-layout').style.display = 'none';
return false;
}
_rUser = res.data;
document.getElementById('user-name').textContent = _rUser.username || 'Reseller';
return true;
}
function renderLogin() {
return `
`;
}
async function doLogin() {
const res = await Nova.api('auth', 'login', { method: 'POST', body: { username: document.getElementById('li-user')?.value, password: document.getElementById('li-pass')?.value }});
if (res?.success) {
if (res.data?.portal_url && !res.data.portal_url.includes(':8881')) location.href = res.data.portal_url;
else location.reload();
} else {
const err = document.getElementById('li-err');
if (err) { err.textContent = res?.message || 'Login failed'; err.style.display = 'block'; }
}
}
window.doLogin = doLogin;
/* ── Pages ─────────────────────────────────────────────────────────────── */
async function rDashboard(el) {
el.innerHTML = `
`;
const res = await Nova.api('accounts', 'list', { params:{ limit:5 }});
const accts = res?.data?.accounts || [];
document.getElementById('r-stats').innerHTML = [
{ label: 'Total Accounts', val: res?.data?.total || 0, icon: 'ni-accounts' },
{ label: 'Active', val: accts.filter(a=>a.status==='active').length, icon: 'ni-stats' },
{ label: 'Suspended', val: accts.filter(a=>a.status==='suspended').length, icon: 'ni-suspend' },
].map(s => ``).join('');
document.getElementById('r-recent').innerHTML = accts.length
? `| Username | Domain | Package | Status |
${accts.map(a => `
| ${a.username} | ${a.domain} | ${a.package_name||'—'} |
${Nova.badge(a.status, a.status==='active'?'green':'yellow')} |
`).join('')}
`
: 'No accounts yet.
';
}
async function rAccounts(el) {
el.innerHTML = `
`;
loadRAccounts();
}
async function loadRAccounts(search = '') {
const el = document.getElementById('r-accounts-list');
if (!el) return;
const res = await Nova.api('accounts', 'list', { params: search ? { search } : {}});
if (!res?.success || !res.data.accounts.length) { el.innerHTML = 'No accounts found.
'; return; }
el.innerHTML = `| Username | Domain | Package | Disk | Status | Actions |
${res.data.accounts.map(a => `
| ${a.username} |
${a.domain} |
${a.package_name || '—'} |
${a.disk_usage_mb || 0} MB |
${Nova.badge(a.status, a.status==='active'?'green':a.status==='suspended'?'yellow':'red')} |
${a.status === 'active'
? ``
: ``}
|
`).join('')}
`;
}
window.loadRAccounts = loadRAccounts;
window.rSearchAccounts = (v) => loadRAccounts(v);
window.rSuspend = async (id, user) => {
Nova.confirm(`Suspend account ${user}? Their website will show a suspension page.`, async () => {
const res = await Nova.api('accounts', 'suspend', { method:'POST', body:{ account_id: id }});
if (res?.success) { Nova.toast('Account suspended','success'); loadRAccounts(); }
else Nova.toast(res?.message,'error');
});
};
window.rUnsuspend = async (id, user) => {
const res = await Nova.api('accounts', 'unsuspend', { method:'POST', body:{ account_id: id }});
if (res?.success) { Nova.toast('Account unsuspended','success'); loadRAccounts(); }
else Nova.toast(res?.message,'error');
};
window.rTerminate = (id, user) => {
Nova.confirm(`TERMINATE ${user}? This permanently deletes all files, databases, DNS, and email. THIS CANNOT BE UNDONE.`, async () => {
const res = await Nova.api('accounts', 'terminate', { method:'POST', body:{ account_id: id }});
if (res?.success) { Nova.toast('Account terminated','success'); loadRAccounts(); }
else Nova.toast(res?.message,'error');
}, true);
};
window.rChangePass = (id, user) => {
Nova.modal(`Change Password — ${user}`, ``,
``);
};
async function rCreateAccount(el) {
el.innerHTML = `
`;
Nova.api('packages', 'list').then(res => {
const sel = document.getElementById('ca-pkg');
if (sel && res?.success) {
sel.innerHTML = res.data.map(p => ``).join('');
}
});
}
window.submitCreateAccount = async () => {
const btn = document.querySelector('#ca-result');
if (btn) btn.textContent = '';
const res = await Nova.api('accounts', 'create', { method:'POST', body:{
username: document.getElementById('ca-user')?.value,
password: document.getElementById('ca-pass')?.value,
email: document.getElementById('ca-email')?.value,
domain: document.getElementById('ca-domain')?.value,
package_id: document.getElementById('ca-pkg')?.value,
}});
if (res?.success) {
Nova.toast('Account created successfully!','success');
if (btn) btn.innerHTML = ``;
} else {
Nova.toast(res?.message || 'Failed to create account','error');
if (btn) btn.innerHTML = `${res?.message || 'Error'}
`;
}
};
async function rPackages(el) {
el.innerHTML = `
`;
const res = await Nova.api('packages', 'list');
const plist = document.getElementById('pkg-list');
if (!res?.success || !res.data.length) { plist.innerHTML = 'No packages yet.
'; return; }
plist.innerHTML = `| Name | Disk | BW | DBs | Emails | Domains | Price | Actions |
${res.data.map(p => `
| ${p.name} |
${p.disk_mb > 0 ? p.disk_mb+'MB' : '∞'} |
${p.bandwidth_mb > 0 ? p.bandwidth_mb+'MB' : '∞'} |
${p.databases || '∞'} |
${p.email_accounts || '∞'} |
${p.addon_domains || '∞'} |
${p.price ? '$'+p.price : 'Free'} |
|
`).join('')}
`;
}
window.rAddPackage = () => showPackageModal();
window.rEditPackage = async (id) => {
const res = await Nova.api('packages', 'get', { params:{ id }});
if (res?.success) showPackageModal(res.data);
};
function showPackageModal(pkg = null) {
const p = pkg || {};
Nova.modal(pkg ? 'Edit Package' : 'Add Package', `
`,
``);
}
window.submitPackage = async (id) => {
const body = { name:document.getElementById('pk-name')?.value, disk_mb:parseInt(document.getElementById('pk-disk')?.value), bandwidth_mb:parseInt(document.getElementById('pk-bw')?.value), databases:parseInt(document.getElementById('pk-db')?.value), email_accounts:parseInt(document.getElementById('pk-email')?.value), addon_domains:parseInt(document.getElementById('pk-adom')?.value), subdomains:parseInt(document.getElementById('pk-sub')?.value), ftp_accounts:parseInt(document.getElementById('pk-ftp')?.value), price:parseFloat(document.getElementById('pk-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 ? 'Package updated' : 'Package created','success'); document.querySelector('.modal-overlay')?.remove(); rPackages(document.getElementById('page-content')); }
else Nova.toast(res?.message,'error');
};
window.rDeletePackage = (id, name) => {
Nova.confirm(`Delete package "${name}"? Cannot delete if accounts are using it.`, async () => {
const res = await Nova.api('packages','delete',{method:'POST',body:{id}});
if (res?.success) { Nova.toast('Deleted','success'); rPackages(document.getElementById('page-content')); }
else Nova.toast(res?.message,'error');
}, true);
};
async function rDNS(el) {
el.innerHTML = `
`;
const res = await Nova.api('dns', 'zones');
const list = document.getElementById('r-dns-list');
if (!res?.success || !res.data.length) { list.innerHTML = 'No DNS zones.
'; return; }
list.innerHTML = `| Domain | Account | Records | Actions |
${res.data.map(z => `
| ${z.domain} |
${z.username||'—'} |
${z.record_count||0} |
|
`).join('')}
`;
}
window.rViewZone = async (zoneId, domain) => {
const res = await Nova.api('dns', 'records', { params:{ zone_id: zoneId }});
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 Records — ${domain}`,
`
`);
};
window.rAddRecord = (zoneId, domain) => {
Nova.modal('Add DNS Record', `
`,
``);
};
window.rDeleteRecord = async (id, zoneId, domain) => {
Nova.confirm('Delete this DNS record?', async () => {
const res = await Nova.api('dns', 'delete-record', { method:'POST', body:{id, zone_id: zoneId }});
if (res?.success) { Nova.toast('Deleted','success'); document.querySelector('.modal-overlay')?.remove(); rViewZone(zoneId, domain); }
else Nova.toast(res?.message,'error');
}, true);
};
/* ── Nav ────────────────────────────────────────────────────────────────── */
const rNavItems = [
{ id:'dashboard', label:'Dashboard', icon:'ni-dashboard' },
{ id:'accounts', label:'Accounts', icon:'ni-accounts' },
{ id:'createAccount', label:'New Account', icon:'ni-add' },
{ id:'packages', label:'Packages', icon:'ni-packages' },
{ id:'dns', label:'DNS Zones', icon:'ni-dns' },
{ id:'docker', label:'Docker', icon:'ni-docker' },
];
const rPages = { dashboard: rDashboard, accounts: rAccounts, createAccount: rCreateAccount, packages: rPackages, dns: rDNS, docker: rDocker };
let _rActivePage = 'dashboard';
function renderRNav() {
const nav = document.getElementById('sidebar-nav');
if (!nav) return;
nav.innerHTML = rNavItems.map(n => `
${n.label}
`).join('');
}
window.resellerNav = (page) => {
_rActivePage = page;
renderRNav();
const content = document.getElementById('page-content');
if (!content) return;
content.innerHTML = 'Loading…
';
if (rPages[page]) rPages[page](content);
};
document.addEventListener('DOMContentLoaded', async () => {
const ok = await initReseller();
if (!ok) return;
renderRNav();
window.resellerNav('dashboard');
});
/* ── Docker (Reseller #33) ────────────────────────────────────────────────── */
async function rDocker(el) {
el.innerHTML = 'Loading…
';
const [stRes, acctRes] = await Promise.all([
Nova.api('docker', 'stacks'),
Nova.api('accounts', 'list', { params: { limit: 200 } }),
]);
const stacks = stRes?.data?.stacks || [];
const accts = acctRes?.data?.accounts || [];
el.innerHTML = `
Manage Docker containers and quotas for your customers. Contact the server admin to change your own Docker allocation.
`;
window._rDockerAccts = accts;
window._rDockerTab = window._rDockerTab || 'containers';
window.rDockerTab = async (tab) => {
window._rDockerTab = tab;
document.querySelectorAll('[onclick^="rDockerTab"]').forEach(b => {
const t = b.getAttribute('onclick').match(/'([^']+)'/)?.[1];
b.className = 'btn btn-sm ' + (t === tab ? 'btn-primary' : 'btn-ghost');
});
await rDockerLoadTab(tab);
};
await rDockerLoadTab(window._rDockerTab);
}
window._rDockerTab = 'containers';
async function rDockerLoadTab(tab) {
const tc = document.getElementById('rdocker-content');
if (!tc) return;
tc.innerHTML = 'Loading…
';
if (tab === 'containers') {
const r = await Nova.api('docker', 'containers');
const rows = r?.data?.containers || [];
tc.innerHTML = rows.length === 0
? 'No containers for your accounts
'
: `| Name | Image | Status | Account | Actions |
${rows.map(c=>`
| ${Nova.escHtml(c.name)} |
${Nova.escHtml(c.image)} |
${Nova.badge(c.status,c.status==='running'?'green':'red')} |
${c.account_id||'—'} |
${c.status==='running'
? ``
: ``}
|
`).join('')}
`;
} else if (tab === 'quotas') {
const accts = window._rDockerAccts || [];
tc.innerHTML = accts.length === 0
? 'No accounts
'
: `Set Docker limits for each of your customers.
| Username | Max Containers | Max Memory | Max CPUs | Actions |
${accts.map(u=>`
| ${Nova.escHtml(u.username)} |
2 | 512 MB | 1.0 |
|
`).join('')}
`;
} else if (tab === 'catalog') {
const r = await Nova.api('docker', 'catalog');
const catalog = r?.data?.catalog || {};
const accts = window._rDockerAccts || [];
tc.innerHTML = `
Pre-install app stacks for your customers.
${Object.entries(catalog).map(([key,app])=>`
${Nova.escHtml(app.icon)}
${Nova.escHtml(app.name)}
${Nova.escHtml(app.description)}
`).join('')}
`;
}
}
window.rDockerAct = async (cid, action) => {
const r = await Nova.api('docker', 'container-action', { method: 'POST', body: { container_id: cid, action } });
Nova.toast(r?.success ? `Container ${action}ed` : (r?.message||'Failed'), r?.success?'success':'error');
if (r?.success) rDockerLoadTab('containers');
};
window.rDockerLogs = 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||'')}`);
};
window.rDockerQuotaModal = (userId, username) => {
const ov = Nova.modal(`Docker Quota: ${username}`,
`
`,
`
`
);
window.rDockerQuotaSubmit = async (uid) => {
ov.remove();
const r = await Nova.api('docker', 'quota-set', { method:'POST', body:{
user_id: uid,
max_containers: parseInt(document.getElementById('rdq-cnt').value)||2,
max_memory_mb: parseInt(document.getElementById('rdq-mem').value)||512,
max_cpus: parseFloat(document.getElementById('rdq-cpus').value)||1.0,
}});
Nova.toast(r?.success?'Quota saved':(r?.message||'Failed'),r?.success?'success':'error');
};
};
window.rDockerLaunchModal = async (appKey, appName) => {
const catRes = await Nova.api('docker', 'catalog');
const app = catRes?.data?.catalog?.[appKey];
if (!app) return;
const accts = window._rDockerAccts || [];
const acctOpts = accts.map(a=>``).join('');
const paramFields = (app.params||[]).map(p=>`
`).join('');
const ov = Nova.modal(`Deploy ${appName}`,
`${paramFields}`,
`
`
);
window.rDockerLaunchSubmit = async (key) => {
const acctId = parseInt(document.getElementById('rl-acct').value)||0;
if (!acctId) { Nova.toast('Select an account','error'); return; }
const params = {};
(app.params||[]).forEach(p => { params[p.key] = document.getElementById('rl-'+p.key)?.value||''; });
ov.remove();
Nova.toast('Deploying…', 'info', 10000);
const r = await Nova.api('docker', 'launch', { method:'POST', body:{ account_id: acctId, app_key: key, params }});
Nova.toast(r?.success?`${appName} deployed!`:(r?.message||'Deploy failed'), r?.success?'success':'error');
if (r?.success) rDockerLoadTab('containers');
};
};