diff --git a/panel/admin/index.php b/panel/admin/index.php
index 2582926..7ab8c79 100644
--- a/panel/admin/index.php
+++ b/panel/admin/index.php
@@ -1,33 +1,34 @@
'?v=' . @filemtime(dirname(__DIR__) . $f);
+require_once dirname(__DIR__) . '/_branding.php';
?>
+
+
+
NovaCPX Admin
-
-
+
+
+
`;
};
+
+// ══ ADMIN SUBDOMAINS PAGE ═════════════════════════════════════════════════
+window.adminSubdomains = async function() {
+ Nova.loadPage('subdomains', window._novaPages); document.getElementById('page-title').textContent='All Subdomains'; document.getElementById('page-content').innerHTML=`, 'All Subdomains', `
+
+
All subdomains across all hosting accounts.
+
+ `);
+ `;
+ const el = document.getElementById('admin-sub-list');
+ const res = await Nova.api('accounts','list',{params:{per_page:200}});
+ if (!res?.success || !res.data?.length) { el.innerHTML='No accounts
'; return; }
+ const accts = res.data;
+ let rows = [];
+ for (const acct of accts) {
+ const dr = await Nova.api('domains','list',{params:{account_id:acct.id}});
+ if (!dr?.success) continue;
+ dr.data.filter(d=>d.type==='subdomain').forEach(d => rows.push({...d, account_username: acct.username, account_domain: acct.domain}));
+ }
+ if (!rows.length) { el.innerHTML='No subdomains found.
'; return; }
+ el.innerHTML = `| Account | Subdomain | SSL | Created | |
+ ${rows.map(d=>`
+ | ${d.account_username} |
+ ${d.domain} |
+ ${d.ssl_enabled ? 'SSL' : '—'} |
+ ${(d.created_at||'').split('T')[0]} |
+ |
+
`).join('')}
+
`;
+};
+
+// ══ ADMIN PARKED DOMAINS PAGE ═════════════════════════════════════════════
+window.adminParked = async function() {
+ document.querySelectorAll('.sidebar-link').forEach(l=>l.classList.remove('active'));
+ document.querySelector('[data-page="parked-domains"]')?.classList.add('active');
+ document.getElementById('page-title').textContent = 'Parked Domains';
+ document.getElementById('page-content').innerHTML = `
+
+
All parked/alias domains across all accounts.
+
+ `);
+ `;
+ const el = document.getElementById('admin-park-list');
+ const res = await Nova.api('accounts','list',{params:{per_page:200}});
+ if (!res?.success || !res.data?.length) { el.innerHTML='No accounts
'; return; }
+ let rows = [];
+ for (const acct of res.data) {
+ const dr = await Nova.api('domains','list',{params:{account_id:acct.id}});
+ if (!dr?.success) continue;
+ const main = dr.data.find(d=>d.type==='main');
+ dr.data.filter(d=>d.type==='parked'||d.type==='alias').forEach(d => rows.push({...d, account_username: acct.username, main_domain: main?.domain||acct.domain}));
+ }
+ if (!rows.length) { el.innerHTML='No parked domains found.
'; return; }
+ el.innerHTML = `| Account | Parked Domain | Points To | Created |
+ ${rows.map(d=>`
+ | ${d.account_username} |
+ ${d.domain} |
+ ${d.main_domain} |
+ ${(d.created_at||'').split('T')[0]} |
+
`).join('')}
+
`;
+};
diff --git a/panel/public/assets/js/reseller.js b/panel/public/assets/js/reseller.js
index 4800189..76140c2 100644
--- a/panel/public/assets/js/reseller.js
+++ b/panel/public/assets/js/reseller.js
@@ -325,6 +325,11 @@ const rNavGroups = [
{ id: 'packages', label: 'Packages',
svg: '' },
]},
+ { id: 'subdomains', label: 'Subdomains',
+ svg: '' },
+ { id: 'parked-domains', label: 'Parked Domains',
+ svg: '' },
+ ]},
{ label: 'DNS', items: [
{ id: 'dns', label: 'DNS Zones',
svg: '' },
@@ -361,6 +366,8 @@ function renderRNav() {
document.getElementById('sidebar-overlay')?.classList.remove('open');
document.body.style.overflow = '';
}
+ const extras = {'subdomains': window.resellerSubdomains, 'parked-domains': window.resellerParked};
+ if (extras[link.dataset.page]) { extras[link.dataset.page](); _rActivePage = link.dataset.page; return; }
resellerNav(link.dataset.page);
});
});
@@ -699,3 +706,54 @@ window.rWlSave = async () => {
Nova.toast(r?.success ? 'Branding saved — reload to see changes' : (r?.message || 'Save failed'),
r?.success ? 'success' : 'error');
};
+
+// ══ RESELLER SUBDOMAINS PAGE ══════════════════════════════════════════════
+window.resellerSubdomains = async function() {
+ document.getElementById('page-title').textContent = 'Subdomains';
+ document.getElementById('page-content').innerHTML = `
+
+
Subdomains across your customers' accounts.
+
+ `;
+ const el = document.getElementById('rsub-list');
+ const res = await Nova.api('accounts','list',{params:{per_page:200}});
+ if (!res?.success || !res.data?.length) { el.innerHTML='No accounts
'; return; }
+ let rows = [];
+ for (const acct of res.data) {
+ const dr = await Nova.api('domains','list',{params:{account_id:acct.id}});
+ if (!dr?.success) continue;
+ dr.data.filter(d=>d.type==='subdomain').forEach(d=>rows.push({...d,acct_username:acct.username}));
+ }
+ if (!rows.length) { el.innerHTML='No subdomains found.
'; return; }
+ el.innerHTML = `| Account | Subdomain | SSL | Created |
+ ${rows.map(d=>`| ${d.acct_username} | ${d.domain} |
+ ${d.ssl_enabled?'SSL':'—'} |
+ ${(d.created_at||'').split('T')[0]} |
`).join('')}
+
`;
+};
+
+// ══ RESELLER PARKED DOMAINS PAGE ══════════════════════════════════════════
+window.resellerParked = async function() {
+ document.getElementById('page-title').textContent = 'Parked Domains';
+ document.getElementById('page-content').innerHTML = `
+
+
Parked domains across your customers' accounts.
+
+ `;
+ const el = document.getElementById('rpark-list');
+ const res = await Nova.api('accounts','list',{params:{per_page:200}});
+ if (!res?.success || !res.data?.length) { el.innerHTML='No accounts
'; return; }
+ let rows = [];
+ for (const acct of res.data) {
+ const dr = await Nova.api('domains','list',{params:{account_id:acct.id}});
+ if (!dr?.success) continue;
+ const main = dr.data.find(d=>d.type==='main');
+ dr.data.filter(d=>d.type==='parked'||d.type==='alias').forEach(d=>rows.push({...d,acct_username:acct.username,main_domain:main?.domain||acct.domain}));
+ }
+ if (!rows.length) { el.innerHTML='No parked domains found.
'; return; }
+ el.innerHTML = `| Account | Parked Domain | Points To | Created |
+ ${rows.map(d=>`| ${d.acct_username} | ${d.domain} |
+ ${d.main_domain} |
+ ${(d.created_at||'').split('T')[0]} |
`).join('')}
+
`;
+};
diff --git a/panel/public/assets/js/user.js b/panel/public/assets/js/user.js
index 47de85d..ae0d9c2 100644
--- a/panel/public/assets/js/user.js
+++ b/panel/public/assets/js/user.js
@@ -937,8 +937,13 @@ const navGroups = [
svg: '' },
]},
{ label: 'Hosting', items: [
- { id: 'domains', label: 'Domains',
+ { id: 'domains', label: 'Domains',
+
svg: '' },
+ { id: 'subdomains', label: 'Subdomains',
+ svg: '' },
+ { id: 'parked-domains', label: 'Parked Domains',
+ svg: '' },
{ id: 'email', label: 'Email',
svg: '' },
{ id: 'databases', label: 'Databases',
@@ -1003,6 +1008,11 @@ window.userNav = (page) => {
_activePage = page;
renderNav();
const allItems = navGroups.flatMap(g => g.items);
+ // Extra pages not in nav groups
+ const extraPages = {
+ 'subdomains': userSubdomains,
+ 'parked-domains': userParked,
+ };
const item = allItems.find(n => n.id === page);
const titleEl = document.getElementById('page-title');
if (titleEl && item) titleEl.textContent = item.label;
@@ -1275,3 +1285,111 @@ document.addEventListener('DOMContentLoaded', async () => {
renderNav();
window.userNav('dashboard');
});
+
+// ══ SUBDOMAINS PAGE ════════════════════════════════════════════════════════
+async function userSubdomains() {
+ Nova.loadPage('subdomains', 'Subdomains', `
+
+
Create subdomains on your hosted domains.
+
+
+ `);
+ loadSubdomainsList();
+}
+
+async function loadSubdomainsList() {
+ const el = document.getElementById('subdomains-list');
+ if (!el) return;
+ const res = await Nova.api('domains', 'list');
+ if (!res?.success) { el.innerHTML = 'No domains found
'; return; }
+ const rows = res.data.filter(d => d.type === 'subdomain');
+ if (!rows.length) {
+ el.innerHTML = 'No subdomains yet. Click + Add Subdomain to create one.
';
+ return;
+ }
+ el.innerHTML = `
+ | Subdomain | Document Root | SSL | Created | Actions |
+
${rows.map(d => `
+ | ${d.domain} |
+ ${d.document_root||'—'} |
+ ${d.ssl_enabled ? Nova.badge('SSL','green') : 'None'} |
+ ${d.created_at?.split('T')[0]||d.created_at||''} |
+ |
+
`).join('')}
`;
+}
+window.userSubdomains = userSubdomains;
+
+window.addSubdomain = () => {
+ Nova.modal('Add Subdomain', `
+
+
+
+ Creates blog.yourdomain.com
+
`, ``);
+};
+
+window.submitSubdomain = async () => {
+ const sub = document.getElementById('sd-prefix')?.value?.trim();
+ if (!sub) { Nova.toast('Enter a subdomain prefix','error'); return; }
+ const res = await Nova.api('domains', 'add-subdomain', { method: 'POST', body: { subdomain: sub } });
+ if (res?.success) { Nova.toast(res.message,'success'); document.querySelector('.modal-overlay')?.remove(); loadSubdomainsList(); }
+ else Nova.toast(res?.message||'Failed','error');
+};
+
+window.removeDomainEntry = (id, domain) => {
+ Nova.confirm(`Remove ${domain}? This will delete its vhost and directory.`, async () => {
+ const res = await Nova.api('domains','remove',{method:'POST',body:{id}});
+ if (res?.success) { Nova.toast('Removed','success'); loadSubdomainsList(); loadParkedList(); }
+ else Nova.toast(res?.message||'Failed','error');
+ }, true);
+};
+
+// ══ PARKED DOMAINS PAGE ════════════════════════════════════════════════════
+async function userParked() {
+ Nova.loadPage('parked-domains', 'Parked Domains', `
+
+
Parked domains point additional domains to your account's main content.
+
+
+ `);
+ loadParkedList();
+}
+
+async function loadParkedList() {
+ const el = document.getElementById('parked-list');
+ if (!el) return;
+ const res = await Nova.api('domains','list');
+ if (!res?.success) { el.innerHTML = 'No domains
'; return; }
+ const rows = res.data.filter(d => d.type === 'parked' || d.type === 'alias');
+ const main = res.data.find(d => d.type === 'main');
+ if (!rows.length) {
+ el.innerHTML = `No parked domains yet. Click + Park Domain to add one.
`;
+ return;
+ }
+ el.innerHTML = `
+ | Parked Domain | Points To | Created | Actions |
+
${rows.map(d=>`
+ | ${d.domain} |
+ ${main?.domain||'—'} |
+ ${d.created_at?.split('T')[0]||d.created_at||''} |
+ |
+
`).join('')}
`;
+}
+window.userParked = userParked;
+
+window.addParked = () => {
+ Nova.modal('Park a Domain', `
+
+
+
+ This domain will serve the same content as your primary domain.
+
`, ``);
+};
+
+window.submitParked = async () => {
+ const domain = document.getElementById('pd-domain')?.value?.trim();
+ if (!domain) { Nova.toast('Enter a domain','error'); return; }
+ const res = await Nova.api('domains','add-alias',{method:'POST',body:{domain}});
+ if (res?.success) { Nova.toast(res.message,'success'); document.querySelector('.modal-overlay')?.remove(); loadParkedList(); }
+ else Nova.toast(res?.message||'Failed','error');
+};