diff --git a/panel/api/endpoints/email.php b/panel/api/endpoints/email.php index efafe6b..09b99ab 100644 --- a/panel/api/endpoints/email.php +++ b/panel/api/endpoints/email.php @@ -98,5 +98,21 @@ match ($action) { Response::success(null, 'Autoresponder deleted'); })(), + + 'domains' => (function() use ($db) { + Auth::getInstance()->require('admin', 'reseller'); + $user = Auth::getInstance()->user(); + $clause = $user['role'] === 'reseller' ? "AND a.user_id IN (SELECT id FROM users WHERE reseller_id=".(int)$user['uid'].")" : ""; + $rows = $db->fetchAll( + "SELECT d.domain, a.username, a.id as account_id, + (SELECT COUNT(*) FROM email_accounts WHERE account_id = a.id) as email_count + FROM dns_zones d + JOIN accounts a ON a.id = d.account_id + WHERE 1=1 $clause + ORDER BY d.domain" + ); + Response::success($rows); + })(), + default => Response::error("Unknown email action: $action", 404), }; diff --git a/panel/api/endpoints/system.php b/panel/api/endpoints/system.php index 11b93ca..1d1b1a2 100644 --- a/panel/api/endpoints/system.php +++ b/panel/api/endpoints/system.php @@ -1057,5 +1057,22 @@ BASH; exit; })(), - default => Response::error("Unknown system action: $action", 404), + + 'read-log' => (function() { + Auth::getInstance()->require('admin'); + $log = preg_replace('/[^a-z0-9-]/', '', $_GET['log'] ?? 'panel'); + $map = [ + 'panel' => '/var/log/novacpx/panel.log', + 'deploy' => '/var/log/novacpx/deploy.log', + 'nginx-error' => '/var/log/novacpx/nginx-error.log', + 'nginx-access' => '/var/log/novacpx/nginx-access.log', + 'mail' => '/var/log/mail.log', + 'stats' => '/var/log/novacpx/stats-collector.log', + ]; + $path = $map[$log] ?? '/var/log/novacpx/panel.log'; + $raw = file_exists($path) ? trim(shell_exec('tail -100 ' . escapeshellarg($path)) ?: '') : ''; + Response::success(['content' => $raw, 'log' => $log]); + })(), + + default => Response::error("Unknown system action: $action", 404), }; diff --git a/panel/public/assets/js/admin.js b/panel/public/assets/js/admin.js index 8e8e145..542a291 100644 --- a/panel/public/assets/js/admin.js +++ b/panel/public/assets/js/admin.js @@ -1401,26 +1401,68 @@ // ── Web Server ──────────────────────────────────────────────────────────── async function webServer() { - const r = await Nova.api('system', 'stats'); - const svcs = r?.data?.services || {}; - const webSvc = Object.keys(svcs).find(k => k.includes('apache') || k.includes('nginx')) || 'apache2'; + const [statsR, phpR] = await Promise.all([ + Nova.api('system','stats'), + Nova.api('php','global-config'), + ]); + const svcs = statsR?.data?.services || {}; + const uptime = statsR?.data?.uptime || '—'; + const cpu = statsR?.data?.cpu?.pct ?? '—'; + const ram = statsR?.data?.ram?.pct ?? '—'; + const phpCfg = phpR?.data || {}; + + window.wsLoadLog = async (log) => { + const r = await Nova.api('system','read-log',{params:{log}}); + const el = document.getElementById('ws-log-out'); + if (el) { el.textContent = r?.data?.content || '(empty)'; el.scrollTop = el.scrollHeight; } + }; + return ` -
-
Web Server Management
-
-
- ${Object.entries(svcs).map(([s,st]) => `
-
- ${s}${Nova.badge(st,st==='active'?'green':'red')} -
-
- - - -
-
`).join('')} + +
+
CPU
${cpu}%
+
RAM
${ram}%
+
Uptime
${uptime}
+
PHP
${phpCfg.version||'8.3'}
+
+ +
+
+
Services
+
+ ${Object.entries(svcs).map(([s,st]) => ` +
+ ${s} ${Nova.badge(st,st==='active'?'green':'red')} +
+ + + +
+
`).join('')}
+
+
PHP Defaults
+
+ ${[['Version',phpCfg.version||'8.3'],['Memory Limit',phpCfg.memory_limit||'256M'], + ['Upload Max',phpCfg.upload_max_filesize||'64M'],['Max Exec Time',(phpCfg.max_execution_time||30)+'s'], + ['Post Max',phpCfg.post_max_size||'64M']].map(([k,v])=>` +
+ ${k}${v} +
`).join('')} +
+
+
+ +
+
+ Log Viewer +
+ ${[['nginx-error','Nginx Error'],['nginx-access','Nginx Access'],['panel','Panel'],['deploy','Deploy']].map(([l,n])=> + ``).join('')} +
+
+
← Click a log above to view it
`; } @@ -2406,7 +2448,12 @@ ${dbs.map(d=>` ` : '
No databases yet.
'; return ` - + +
+ 🐬 phpMyAdmin (MySQL) + 🗄️ Adminer (MySQL) + 🐘 Adminer (PostgreSQL) +
${engineCard('mysql', 'MySQL', '🐬')} @@ -2433,6 +2480,7 @@ ${dbs.map(d=>`
${toolCard('phpmyadmin', 'phpMyAdmin', '🛢', `http://${location.hostname}/phpmyadmin`)} ${toolCard('pgadmin', 'pgAdmin 4', '🐘', `http://${location.hostname}/pgadmin4`)} + ${toolCard('adminer', 'Adminer', '🗄️', `http://${location.hostname}/adminer.php`)}
@@ -2609,37 +2657,71 @@ ${dbs.map(d=>` // ── Mail Server ──────────────────────────────────────────────────────────── async function mailServer() { - const r = await Nova.api('system','stats'); - const svcs = r?.data?.services || {}; - const mailStatus = svcs['postfix'] || 'unknown'; - const doveStatus = svcs['dovecot'] || 'unknown'; + const [statsR, domainsR] = await Promise.all([ + Nova.api('system','stats'), + Nova.api('email','domains'), + ]); + const svcs = statsR?.data?.services || {}; + const mailSvcs = ['postfix','dovecot','rspamd','opendkim'].filter(s => svcs[s]); + const domains = domainsR?.data || []; + + window.msLoadLog = async () => { + const r = await Nova.api('system','read-log',{params:{log:'mail'}}); + const el = document.getElementById('ms-log-out'); + if (el) { el.textContent = r?.data?.content || '(empty)'; el.scrollTop = el.scrollHeight; } + }; + return ` -
+ +
-
Mail Services
-
- ${[['postfix',mailStatus],['dovecot',doveStatus]].map(([s,st]) => ` -
- ${s} ${Nova.badge(st,st==='active'?'green':'red')} -
- - +
Services
+
+ ${mailSvcs.length ? mailSvcs.map(s => ` +
+ ${s} ${Nova.badge(svcs[s],svcs[s]==='active'?'green':'red')} +
+ +
-
`).join('')} +
`).join('') : '

No mail services detected

'}
-
Mail Queue
-
+
+ Mail Queue + +
+
- +
+
+ +
+
+ Virtual Mail Domains (${domains.length}) +
+ ${domains.length ? `
+ ${domains.map(d=>``).join('')} +
DomainAccountEmail Accounts
${Nova.escHtml(d.domain)}${Nova.escHtml(d.username||'—')}${d.email_count||0}
` : '
No mail domains yet — created automatically when hosting accounts are set up.
'} +
+ +
+
+ Mail Log + +
+
← Click Load Log to view recent mail activity
`; } window.adminViewMailQueue = async () => { const r = await Nova.api('system','service',{method:'POST',body:{service:'mailq',command:'status'}}); - Nova.modal('Mail Queue', `
${r?.data?.output || 'Queue is empty'}
`); + const out = r?.data?.output || 'Queue is empty'; + const el = document.getElementById('ms-queue-out'); + if (el) el.innerHTML = `
${Nova.escHtml(out)}
`; + else Nova.modal('Mail Queue', `
${Nova.escHtml(out)}
`); }; // ── FTP Server ──────────────────────────────────────────────────────────── @@ -2653,23 +2735,36 @@ ${dbs.map(d=>` const svcName = ftpConf === 'vsftpd' ? 'vsftpd' : (ftpConf === 'pureftpd' ? 'pure-ftpd' : 'proftpd'); const label = ftpConf === 'vsftpd' ? 'vsftpd' : (ftpConf === 'pureftpd' ? 'Pure-FTPd' : 'ProFTPD'); const status = svcs[svcName] || 'unknown'; + const ftpR = await Nova.api('ftp','list',{params:{account_id:0}}); + const ftpAccts = ftpR?.data || []; + return ` -
+ +
- FTP Server (${label}) - ${Nova.badge(status, status==='active'?'green':'red')} -
- - + ${label} + ${Nova.badge(status, status==='active'?'green':'red')} +
+ +
-
-
-

Active FTP server: ${label} — change in Server Options.

-

FTP connections use SFTP on port 22 or passive FTP on ports 20/21.

-

Per-account FTP management is available in each account's FTP page.

-
+
+ Active server: ${label} — change in Server Options. + Passive FTP ports 20/21 · SFTP on port 22.
+
+
+
All FTP Accounts (${ftpAccts.length})
+ ${ftpAccts.length ? `
+ ${ftpAccts.map(f=>` + + + + + `).join('')} +
UsernameAccountDirectoryPermissions
${Nova.escHtml(f.username)}${Nova.escHtml(f.account_domain||String(f.account_id)||'—')}${Nova.escHtml(f.home_dir||'—')}${Nova.badge(f.permissions||'rw','blue')}
` + : '
No FTP accounts yet — created from each account's FTP page.
'}
`; }