feat: server monitoring charts, package limits, WHMCS bridge, server options (#19-22)

#19 Server monitoring charts:
- server_stats table (migration 007) + collect-stats.php cron script
- serverStatus() page rebuilt with Chart.js line charts (CPU/RAM/disk)
- Chart.js lazy-loaded from CDN; history shown for last 24h

#20 Cron job manager: already complete in prior session

#21 Package limits enforcement:
- email.php: checks max_email before creating email account
- databases.php: checks max_databases before creating database
- ftp.php: checks max_ftp before creating FTP account
- stats.php: fixed column names (max_email/max_ftp/max_databases vs old aliases)

#22b WHMCS billing bridge:
- whmcs.php endpoint: create/suspend/unsuspend/terminate/changepackage/info
- Auth via X-WHMCS-Key header; enabled/key stored in settings table

#22a/c/d/e Server options admin page:
- Web/mail/FTP/DNS server selection with settings persistence
- Server switch triggers /opt/novacpx/bin/switch-*.sh scripts (if present)
- NS health checker: live dig lookup of all zones vs configured NS1/NS2
- system.php: server-options + save-option actions
- dns.php: ns-health action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 03:00:09 +00:00
parent 7c17e3696d
commit c0c9865653
11 changed files with 558 additions and 30 deletions
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env php
<?php
/**
* NovaCPX stats collector — runs every 5 minutes via cron
* Cron: *\/5 * * * * root /usr/bin/php /opt/novacpx/bin/collect-stats.php
*/
define('NOVACPX_ROOT', '/srv/novacpx/public');
define('NOVACPX_LIB', NOVACPX_ROOT . '/lib');
require NOVACPX_ROOT . '/lib/Core.php';
require NOVACPX_ROOT . '/lib/DB.php';
// CPU usage (idle from /proc/stat)
$stat1 = file_get_contents('/proc/stat');
usleep(200000); // 200ms sample
$stat2 = file_get_contents('/proc/stat');
$line1 = explode(' ', trim(explode("\n", $stat1)[0]));
$line2 = explode(' ', trim(explode("\n", $stat2)[0]));
array_shift($line1); array_shift($line2);
$line1 = array_filter($line1, 'strlen'); $line2 = array_filter($line2, 'strlen');
$line1 = array_values($line1); $line2 = array_values($line2);
$total1 = array_sum($line1); $total2 = array_sum($line2);
$idle1 = (int)($line1[3] ?? 0); $idle2 = (int)($line2[3] ?? 0);
$cpuPct = $total1 === $total2 ? 0 : round((1 - ($idle2 - $idle1) / ($total2 - $total1)) * 100, 2);
// RAM
$meminfo = [];
foreach (file('/proc/meminfo') as $line) {
if (preg_match('/^(\w+):\s+(\d+)/', $line, $m)) $meminfo[$m[1]] = (int)$m[2];
}
$ramPct = isset($meminfo['MemTotal'], $meminfo['MemAvailable'])
? round((1 - $meminfo['MemAvailable'] / $meminfo['MemTotal']) * 100, 2)
: 0;
// Disk
$dfLine = trim(shell_exec("df / | tail -1") ?: '');
$dfParts = preg_split('/\s+/', $dfLine);
$diskPct = isset($dfParts[4]) ? (float) rtrim($dfParts[4], '%') : 0;
// Load
$load = sys_getloadavg();
// Network I/O (eth0 or first interface)
$netIn = 0; $netOut = 0;
$netData = @file_get_contents('/proc/net/dev');
if ($netData) {
foreach (explode("\n", $netData) as $line) {
if (preg_match('/^\s*(eth0|ens\w+|enp\w+|eno\w+):\s*(\d+)(?:\s+\d+){7}\s+(\d+)/', $line, $m)) {
$netIn = (int)($m[2] / 1024);
$netOut = (int)($m[3] / 1024);
break;
}
}
}
// Insert
$db = DB::getInstance();
$db->execute(
"INSERT INTO server_stats (cpu_usage, ram_usage, disk_usage, load_avg, net_in_kb, net_out_kb)
VALUES (?,?,?,?,?,?)",
[$cpuPct, $ramPct, $diskPct, $load[0], $netIn, $netOut]
);
// Prune rows older than 30 days
$db->execute("DELETE FROM server_stats WHERE recorded_at < DATE_SUB(NOW(), INTERVAL 30 DAY)");