Upload index.php
Browse filesDASH for server admin overview ops
- public/index.php +470 -0
public/index.php
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
// index.php - GhostAI Server Dashboard (Dark Bootstrap, High-Contrast, ADA-friendly)
|
| 3 |
+
|
| 4 |
+
// ---------- Helpers ----------
|
| 5 |
+
function run_cmd($cmd, $timeout = 3) {
|
| 6 |
+
// Basic guard: strip dangerous chars (still assume trusted admin usage)
|
| 7 |
+
$cmd = trim($cmd);
|
| 8 |
+
if ($cmd === '') return ['code' => 1, 'out' => 'N/A'];
|
| 9 |
+
// Timeout wrapper (Linux 'timeout' if available)
|
| 10 |
+
$timeout_bin = trim(shell_exec('command -v timeout 2>/dev/null')) ?: '';
|
| 11 |
+
$safe = $timeout_bin ? escapeshellcmd($timeout_bin) . " " . intval($timeout) . "s " . $cmd : $cmd;
|
| 12 |
+
$out = [];
|
| 13 |
+
$code = 0;
|
| 14 |
+
@exec($safe . ' 2>&1', $out, $code);
|
| 15 |
+
return ['code' => $code, 'out' => implode("\n", $out)];
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function human_bytes($bytes) {
|
| 19 |
+
$u = ['B','KB','MB','GB','TB','PB'];
|
| 20 |
+
$i = 0;
|
| 21 |
+
while ($bytes >= 1024 && $i < count($u)-1) { $bytes /= 1024; $i++; }
|
| 22 |
+
return sprintf('%.1f %s', $bytes, $u[$i]);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function status_badge($status) {
|
| 26 |
+
$s = strtolower(trim($status));
|
| 27 |
+
if (in_array($s, ['online','active','up','running','listening'])) return ['success','Online'];
|
| 28 |
+
if (in_array($s, ['stopped','inactive','exited'])) return ['secondary','Stopped'];
|
| 29 |
+
if (in_array($s, ['errored','failed','dead'])) return ['danger','Error'];
|
| 30 |
+
if (in_array($s, ['unknown','n/a','not found'])) return ['dark','Unknown'];
|
| 31 |
+
return ['warning', ucfirst($status ?: 'Unknown')];
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function parse_pm2() {
|
| 35 |
+
// Prefer JSON output
|
| 36 |
+
$res = run_cmd('pm2 jlist');
|
| 37 |
+
if ($res['code'] === 0 && strlen($res['out']) > 2) {
|
| 38 |
+
$json = json_decode($res['out'], true);
|
| 39 |
+
if (is_array($json)) return $json;
|
| 40 |
+
}
|
| 41 |
+
// Fallback: pm2 status tabular (parse minimally)
|
| 42 |
+
$res2 = run_cmd('pm2 list --no-color');
|
| 43 |
+
$apps = [];
|
| 44 |
+
if ($res2['code'] === 0) {
|
| 45 |
+
$lines = explode("\n", $res2['out']);
|
| 46 |
+
foreach ($lines as $ln) {
|
| 47 |
+
if (preg_match('/^\s*\|\s*(\S+)\s*\|\s*(\d+)\s*\|\s*([A-Z]+)\s*\|\s*(\d+)\s*\|\s*([\dms:\.]+)\s*\|\s*(\S*)/i', $ln, $m)) {
|
| 48 |
+
$apps[] = [
|
| 49 |
+
'name' => $m[1],
|
| 50 |
+
'pm_id' => $m[2],
|
| 51 |
+
'status' => strtolower($m[3]),
|
| 52 |
+
'cpu' => null,
|
| 53 |
+
'mem' => null,
|
| 54 |
+
'uptime' => $m[5],
|
| 55 |
+
'mode' => $m[6] ?? ''
|
| 56 |
+
];
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
return $apps;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function get_cpu_info() {
|
| 64 |
+
$model = trim(run_cmd("awk -F': ' '/model name/ {print \$2; exit}' /proc/cpuinfo")['out']);
|
| 65 |
+
$cores_p = intval(trim(run_cmd("getconf _NPROCESSORS_ONLN")['out'])) ?: 0;
|
| 66 |
+
$loadavg = trim(run_cmd("cut -d' ' -f1-3 /proc/loadavg")['out']);
|
| 67 |
+
return [$model ?: 'N/A', $cores_p, $loadavg ?: 'N/A'];
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function get_mem_info() {
|
| 71 |
+
// /proc/meminfo (bytes -> kB)
|
| 72 |
+
$mem_total_kb = intval(trim(run_cmd("awk '/MemTotal/ {print \$2}' /proc/meminfo")['out']));
|
| 73 |
+
$mem_avail_kb = intval(trim(run_cmd("awk '/MemAvailable/ {print \$2}' /proc/meminfo")['out']));
|
| 74 |
+
$mem_used_kb = max(0, $mem_total_kb - $mem_avail_kb);
|
| 75 |
+
return [
|
| 76 |
+
'total' => human_bytes($mem_total_kb * 1024),
|
| 77 |
+
'used' => human_bytes($mem_used_kb * 1024),
|
| 78 |
+
'free' => human_bytes($mem_avail_kb * 1024),
|
| 79 |
+
'pct' => $mem_total_kb ? round(($mem_used_kb / $mem_total_kb) * 100) : 0
|
| 80 |
+
];
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
function get_disk_info() {
|
| 84 |
+
$res = run_cmd("df -h --output=target,size,used,avail,pcent -x tmpfs -x devtmpfs");
|
| 85 |
+
$rows = [];
|
| 86 |
+
if ($res['code'] === 0) {
|
| 87 |
+
$lines = explode("\n", trim($res['out']));
|
| 88 |
+
foreach (array_slice($lines, 1) as $l) {
|
| 89 |
+
$l = preg_replace('/\s+/', ' ', trim($l));
|
| 90 |
+
if (!$l) continue;
|
| 91 |
+
[$mount, $size, $used, $avail, $pct] = array_pad(explode(' ', $l), 5, '');
|
| 92 |
+
$rows[] = compact('mount','size','used','avail','pct');
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
return $rows;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
function get_gpu_info() {
|
| 99 |
+
$nvsmi = trim(run_cmd('command -v nvidia-smi')['out']);
|
| 100 |
+
if (!$nvsmi) return [];
|
| 101 |
+
$q = '--query-gpu=name,driver_version,pstate,temperature.gpu,memory.total,memory.used,pcie.link.gen.current,pcie.link.width.current --format=csv,noheader';
|
| 102 |
+
$res = run_cmd("nvidia-smi $q", 4);
|
| 103 |
+
$out = [];
|
| 104 |
+
if ($res['code'] === 0) {
|
| 105 |
+
foreach (explode("\n", trim($res['out'])) as $line) {
|
| 106 |
+
if (!$line) continue;
|
| 107 |
+
$parts = array_map('trim', explode(',', $line));
|
| 108 |
+
$out[] = [
|
| 109 |
+
'name' => $parts[0] ?? 'NVIDIA GPU',
|
| 110 |
+
'driver' => $parts[1] ?? 'N/A',
|
| 111 |
+
'pstate' => $parts[2] ?? 'N/A',
|
| 112 |
+
'tempC' => $parts[3] ?? 'N/A',
|
| 113 |
+
'mem_total' => $parts[4] ?? 'N/A',
|
| 114 |
+
'mem_used' => $parts[5] ?? 'N/A',
|
| 115 |
+
'pcie_gen' => $parts[6] ?? 'N/A',
|
| 116 |
+
'pcie_w' => $parts[7] ?? 'N/A',
|
| 117 |
+
];
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
return $out;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
function get_network_info() {
|
| 124 |
+
$hostname = trim(run_cmd('hostname')['out']);
|
| 125 |
+
$ips = trim(run_cmd("hostname -I")['out']);
|
| 126 |
+
$gateway = trim(run_cmd("ip route | awk '/default/ {print \$3; exit}'")['out']);
|
| 127 |
+
return [$hostname ?: 'N/A', $ips ?: 'N/A', $gateway ?: 'N/A'];
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
function get_os_info() {
|
| 131 |
+
$pretty = trim(run_cmd("awk -F'=' '/^PRETTY_NAME/{gsub(/\"/ ,\"\",\$2); print \$2}' /etc/os-release")['out']);
|
| 132 |
+
$kernel = trim(run_cmd('uname -r')['out']);
|
| 133 |
+
$arch = trim(run_cmd('uname -m')['out']);
|
| 134 |
+
$uptime = trim(run_cmd('uptime -p')['out']);
|
| 135 |
+
$boot = trim(run_cmd('who -b | awk \'{print $3, $4}\'')['out']);
|
| 136 |
+
$phpv = PHP_VERSION;
|
| 137 |
+
$nginx = trim(run_cmd('nginx -v 2>&1')['out']);
|
| 138 |
+
$nginx = $nginx ?: 'nginx not found';
|
| 139 |
+
return [$pretty ?: 'Linux', $kernel ?: 'N/A', $arch ?: 'N/A', $uptime ?: 'N/A', $boot ?: 'N/A', $phpv, $nginx];
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
function get_services_status($services = ['nginx','php-fpm','pm2']) {
|
| 143 |
+
$data = [];
|
| 144 |
+
foreach ($services as $svc) {
|
| 145 |
+
$cmd = "systemctl is-active $svc";
|
| 146 |
+
$r = run_cmd($cmd);
|
| 147 |
+
$status = $r['code'] === 0 ? trim($r['out']) : 'unknown';
|
| 148 |
+
$data[] = ['name' => $svc, 'status' => $status];
|
| 149 |
+
}
|
| 150 |
+
return $data;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
function get_top_procs($limit = 8) {
|
| 154 |
+
$r = run_cmd("ps -eo pid,comm,%cpu,%mem --sort=-%cpu | head -n " . intval($limit + 1));
|
| 155 |
+
$rows = [];
|
| 156 |
+
if ($r['code'] === 0) {
|
| 157 |
+
$lines = explode("\n", trim($r['out']));
|
| 158 |
+
foreach (array_slice($lines, 1) as $ln) {
|
| 159 |
+
$ln = preg_replace('/\s+/', ' ', trim($ln));
|
| 160 |
+
if (!$ln) continue;
|
| 161 |
+
[$pid,$comm,$cpu,$mem] = array_pad(explode(' ', $ln), 4, '');
|
| 162 |
+
$rows[] = compact('pid','comm','cpu','mem');
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
return $rows;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
function get_log_tail($path, $lines = 80) {
|
| 169 |
+
if (!is_readable($path)) return "Log not found or unreadable: $path";
|
| 170 |
+
$r = run_cmd("tail -n " . intval($lines) . " " . escapeshellarg($path));
|
| 171 |
+
return $r['code'] === 0 ? $r['out'] : "Unable to read log.";
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// ---------- Page Settings ----------
|
| 175 |
+
$auto_refresh = isset($_GET['autorefresh']) ? (int)$_GET['autorefresh'] : 0;
|
| 176 |
+
$refresh_secs = ($auto_refresh > 0 && $auto_refresh <= 600) ? $auto_refresh : 0;
|
| 177 |
+
$custom_log = isset($_GET['log']) ? $_GET['log'] : '';
|
| 178 |
+
$log_path = $custom_log ?: '/var/log/nginx/access.log';
|
| 179 |
+
|
| 180 |
+
// ---------- Data ----------
|
| 181 |
+
[$os_name,$kernel,$arch,$uptime,$booted,$phpv,$nginxv] = get_os_info();
|
| 182 |
+
[$hostname,$ips,$gw] = get_network_info();
|
| 183 |
+
[$cpu_model,$cores,$load] = get_cpu_info();
|
| 184 |
+
$mem = get_mem_info();
|
| 185 |
+
$disks = get_disk_info();
|
| 186 |
+
$gpus = get_gpu_info();
|
| 187 |
+
$pm2 = parse_pm2();
|
| 188 |
+
$services = get_services_status(['nginx','php-fpm','pm2']);
|
| 189 |
+
$procs = get_top_procs(8);
|
| 190 |
+
$log_tail = get_log_tail($log_path, 100);
|
| 191 |
+
?>
|
| 192 |
+
<!doctype html>
|
| 193 |
+
<html lang="en" data-bs-theme="dark">
|
| 194 |
+
<head>
|
| 195 |
+
<meta charset="utf-8">
|
| 196 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 197 |
+
<?php if ($refresh_secs): ?>
|
| 198 |
+
<meta http-equiv="refresh" content="<?php echo htmlspecialchars($refresh_secs); ?>">
|
| 199 |
+
<?php endif; ?>
|
| 200 |
+
<title>🖥️ GhostAI Server Dashboard</title>
|
| 201 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 202 |
+
<style>
|
| 203 |
+
:root {
|
| 204 |
+
color-scheme: dark;
|
| 205 |
+
}
|
| 206 |
+
body {
|
| 207 |
+
background: #0E1014 !important;
|
| 208 |
+
color: #EAECEF !important;
|
| 209 |
+
font-size: 1.05rem;
|
| 210 |
+
}
|
| 211 |
+
* { color: #EAECEF !important; }
|
| 212 |
+
.card { background: #111520; border: 1px solid #243049; border-radius: 16px; }
|
| 213 |
+
.badge { font-size: 0.9rem; }
|
| 214 |
+
.table > :not(caption) > * > * { background: transparent !important; color: #EAECEF !important; }
|
| 215 |
+
.nav-link { color: #EAECEF !important; }
|
| 216 |
+
.form-select, .form-control { background: #151a24; color: #EAECEF; border: 1px solid #2A3142; }
|
| 217 |
+
.skip-link {
|
| 218 |
+
position: absolute; left: -10000px; top: auto; width: 1px; height: 1px; overflow: hidden;
|
| 219 |
+
}
|
| 220 |
+
.skip-link:focus { position: static; width: auto; height: auto; margin: 8px; padding: 8px; background: #1f2937; border-radius: 8px; }
|
| 221 |
+
.kpi { font-weight: 700; font-size: 1.25rem; }
|
| 222 |
+
.progress { background-color: #1b2231; height: 12px; border-radius: 999px; }
|
| 223 |
+
.emoji { font-size: 1.4rem; margin-right: .35rem; }
|
| 224 |
+
.footer { color: #9aa4b2 !important; }
|
| 225 |
+
</style>
|
| 226 |
+
</head>
|
| 227 |
+
<body>
|
| 228 |
+
<a href="#main" class="skip-link" aria-label="Skip to content">Skip to content</a>
|
| 229 |
+
|
| 230 |
+
<nav class="navbar navbar-expand-lg border-bottom" aria-label="Primary">
|
| 231 |
+
<div class="container-fluid">
|
| 232 |
+
<span class="navbar-brand fw-bold">🖥️ GhostAI Dashboard</span>
|
| 233 |
+
<div class="d-flex align-items-center gap-2">
|
| 234 |
+
<form method="get" class="d-flex" role="search" aria-label="Auto refresh interval">
|
| 235 |
+
<label class="me-2" for="autorefresh">⏱️ Auto-refresh</label>
|
| 236 |
+
<select class="form-select form-select-sm me-2" id="autorefresh" name="autorefresh" aria-label="Auto refresh interval select">
|
| 237 |
+
<?php foreach ([0,5,10,15,30,60,120,300,600] as $sec): ?>
|
| 238 |
+
<option value="<?= $sec ?>" <?= $sec===$auto_refresh?'selected':''; ?>><?= $sec ?>s</option>
|
| 239 |
+
<?php endforeach; ?>
|
| 240 |
+
</select>
|
| 241 |
+
<button class="btn btn-sm btn-primary" type="submit" aria-label="Apply refresh interval">Apply</button>
|
| 242 |
+
</form>
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
</nav>
|
| 246 |
+
|
| 247 |
+
<main id="main" class="container py-4" aria-live="polite">
|
| 248 |
+
<div class="row g-3">
|
| 249 |
+
<div class="col-12 col-xl-6">
|
| 250 |
+
<div class="card h-100">
|
| 251 |
+
<div class="card-body">
|
| 252 |
+
<h5 class="card-title"><span class="emoji">💻</span>System Overview</h5>
|
| 253 |
+
<div class="row g-3">
|
| 254 |
+
<div class="col-12">
|
| 255 |
+
<div class="kpi">Host: <span class="text-info-emphasis"><?= htmlspecialchars($hostname) ?></span></div>
|
| 256 |
+
<div><?= htmlspecialchars($os_name) ?> • Kernel <?= htmlspecialchars($kernel) ?> • <?= htmlspecialchars($arch) ?></div>
|
| 257 |
+
<div>Uptime: <?= htmlspecialchars($uptime) ?> • Boot: <?= htmlspecialchars($booted) ?></div>
|
| 258 |
+
<div>PHP: <?= htmlspecialchars($phpv) ?> • <?= htmlspecialchars($nginxv) ?></div>
|
| 259 |
+
<div>IP(s): <?= htmlspecialchars($ips) ?> • GW: <?= htmlspecialchars($gw) ?></div>
|
| 260 |
+
</div>
|
| 261 |
+
<div class="col-md-6">
|
| 262 |
+
<div class="card p-3">
|
| 263 |
+
<div class="fw-semibold"><span class="emoji">🧠</span>CPU</div>
|
| 264 |
+
<div class="small text-secondary">Model</div>
|
| 265 |
+
<div class="kpi"><?= htmlspecialchars($cpu_model) ?></div>
|
| 266 |
+
<div class="small">Cores: <span class="fw-semibold"><?= intval($cores) ?></span></div>
|
| 267 |
+
<div class="small">Load: <span class="fw-semibold"><?= htmlspecialchars($load) ?></span></div>
|
| 268 |
+
</div>
|
| 269 |
+
</div>
|
| 270 |
+
<div class="col-md-6">
|
| 271 |
+
<div class="card p-3">
|
| 272 |
+
<div class="fw-semibold"><span class="emoji">📈</span>Memory</div>
|
| 273 |
+
<div class="small text-secondary">Usage</div>
|
| 274 |
+
<div class="kpi"><?= htmlspecialchars($mem['used']) ?> / <?= htmlspecialchars($mem['total']) ?></div>
|
| 275 |
+
<div class="progress mt-2" role="progressbar" aria-valuenow="<?= intval($mem['pct']) ?>" aria-valuemin="0" aria-valuemax="100" aria-label="Memory usage">
|
| 276 |
+
<div class="progress-bar bg-warning" style="width: <?= intval($mem['pct']) ?>%"></div>
|
| 277 |
+
</div>
|
| 278 |
+
<div class="small mt-1">Free: <?= htmlspecialchars($mem['free']) ?> (<?= intval($mem['pct']) ?>%)</div>
|
| 279 |
+
</div>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
<?php if (!empty($gpus)): ?>
|
| 283 |
+
<div class="mt-3">
|
| 284 |
+
<div class="fw-semibold mb-2"><span class="emoji">🎮</span>GPU</div>
|
| 285 |
+
<div class="table-responsive">
|
| 286 |
+
<table class="table table-sm align-middle">
|
| 287 |
+
<thead><tr><th>Name</th><th>Driver</th><th>Temp</th><th>VRAM</th><th>PCIe</th><th>P-State</th></tr></thead>
|
| 288 |
+
<tbody>
|
| 289 |
+
<?php foreach ($gpus as $g): ?>
|
| 290 |
+
<tr>
|
| 291 |
+
<td><?= htmlspecialchars($g['name']) ?></td>
|
| 292 |
+
<td><?= htmlspecialchars($g['driver']) ?></td>
|
| 293 |
+
<td><?= htmlspecialchars($g['tempC']) ?></td>
|
| 294 |
+
<td><?= htmlspecialchars($g['mem_used']) ?> / <?= htmlspecialchars($g['mem_total']) ?></td>
|
| 295 |
+
<td>Gen <?= htmlspecialchars($g['pcie_gen']) ?> ×<?= htmlspecialchars($g['pcie_w']) ?></td>
|
| 296 |
+
<td><?= htmlspecialchars($g['pstate']) ?></td>
|
| 297 |
+
</tr>
|
| 298 |
+
<?php endforeach; ?>
|
| 299 |
+
</tbody>
|
| 300 |
+
</table>
|
| 301 |
+
</div>
|
| 302 |
+
</div>
|
| 303 |
+
<?php else: ?>
|
| 304 |
+
<div class="mt-3 small text-secondary">No NVIDIA GPU info (nvidia-smi not found or no GPU).</div>
|
| 305 |
+
<?php endif; ?>
|
| 306 |
+
</div>
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
|
| 310 |
+
<div class="col-12 col-xl-6">
|
| 311 |
+
<div class="card h-100">
|
| 312 |
+
<div class="card-body">
|
| 313 |
+
<h5 class="card-title"><span class="emoji">🗄️</span>Disks</h5>
|
| 314 |
+
<div class="table-responsive">
|
| 315 |
+
<table class="table table-sm align-middle">
|
| 316 |
+
<thead><tr><th>Mount</th><th>Size</th><th>Used</th><th>Avail</th><th>Use%</th></tr></thead>
|
| 317 |
+
<tbody>
|
| 318 |
+
<?php foreach ($disks as $d): ?>
|
| 319 |
+
<tr>
|
| 320 |
+
<td><?= htmlspecialchars($d['mount']) ?></td>
|
| 321 |
+
<td><?= htmlspecialchars($d['size']) ?></td>
|
| 322 |
+
<td><?= htmlspecialchars($d['used']) ?></td>
|
| 323 |
+
<td><?= htmlspecialchars($d['avail']) ?></td>
|
| 324 |
+
<td><span class="badge bg-<?= (intval(rtrim($d['pct'],'%')) >= 85 ? 'danger' : (intval(rtrim($d['pct'],'%')) >= 70 ? 'warning' : 'success')) ?>"><?= htmlspecialchars($d['pct']) ?></span></td>
|
| 325 |
+
</tr>
|
| 326 |
+
<?php endforeach; ?>
|
| 327 |
+
</tbody>
|
| 328 |
+
</table>
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
<div class="mt-4">
|
| 332 |
+
<h5 class="card-title"><span class="emoji">���</span>Services</h5>
|
| 333 |
+
<div class="row g-2">
|
| 334 |
+
<?php foreach ($services as $s): $b = status_badge($s['status']); ?>
|
| 335 |
+
<div class="col-auto">
|
| 336 |
+
<span class="badge bg-<?= $b[0] ?>" aria-label="<?= htmlspecialchars($s['name']) ?> status"><?= htmlspecialchars($s['name']) ?>: <?= htmlspecialchars($b[1]) ?></span>
|
| 337 |
+
</div>
|
| 338 |
+
<?php endforeach; ?>
|
| 339 |
+
</div>
|
| 340 |
+
</div>
|
| 341 |
+
|
| 342 |
+
<div class="mt-4">
|
| 343 |
+
<h5 class="card-title"><span class="emoji">📊</span>Top Processes (CPU)</h5>
|
| 344 |
+
<div class="table-responsive">
|
| 345 |
+
<table class="table table-sm align-middle">
|
| 346 |
+
<thead><tr><th>PID</th><th>Command</th><th>CPU%</th><th>MEM%</th></tr></thead>
|
| 347 |
+
<tbody>
|
| 348 |
+
<?php foreach ($procs as $p): ?>
|
| 349 |
+
<tr>
|
| 350 |
+
<td><?= htmlspecialchars($p['pid']) ?></td>
|
| 351 |
+
<td><?= htmlspecialchars($p['comm']) ?></td>
|
| 352 |
+
<td><?= htmlspecialchars($p['cpu']) ?></td>
|
| 353 |
+
<td><?= htmlspecialchars($p['mem']) ?></td>
|
| 354 |
+
</tr>
|
| 355 |
+
<?php endforeach; ?>
|
| 356 |
+
</tbody>
|
| 357 |
+
</table>
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
+
|
| 361 |
+
</div>
|
| 362 |
+
</div>
|
| 363 |
+
</div>
|
| 364 |
+
|
| 365 |
+
<div class="col-12">
|
| 366 |
+
<div class="card">
|
| 367 |
+
<div class="card-body">
|
| 368 |
+
<h5 class="card-title"><span class="emoji">🎛️</span>PM2 Apps</h5>
|
| 369 |
+
<?php if (!empty($pm2) && is_array($pm2) && isset($pm2[0]['name']) || (isset($pm2[0]['name']) || (is_array($pm2) && count($pm2)>0))): ?>
|
| 370 |
+
<div class="table-responsive">
|
| 371 |
+
<table class="table table-sm align-middle">
|
| 372 |
+
<thead>
|
| 373 |
+
<tr>
|
| 374 |
+
<th>Name</th><th>ID</th><th>Status</th><th>Mode</th><th>CPU</th><th>Mem</th><th>Uptime</th>
|
| 375 |
+
</tr>
|
| 376 |
+
</thead>
|
| 377 |
+
<tbody>
|
| 378 |
+
<?php
|
| 379 |
+
// Two shapes: JSON from jlist, or parsed table rows
|
| 380 |
+
if (!empty($pm2) && isset($pm2[0]['pm2_env'])) {
|
| 381 |
+
foreach ($pm2 as $app) {
|
| 382 |
+
$name = $app['name'] ?? ($app['pm2_env']['name'] ?? 'app');
|
| 383 |
+
$pm_id = $app['pm_id'] ?? ($app['pm2_env']['pm_id'] ?? '');
|
| 384 |
+
$st = strtolower($app['pm2_env']['status'] ?? 'unknown');
|
| 385 |
+
$mode = $app['pm2_env']['exec_mode'] ?? '';
|
| 386 |
+
$cpu = $app['monit']['cpu'] ?? null;
|
| 387 |
+
$mem_b = $app['monit']['memory'] ?? null;
|
| 388 |
+
$memh = $mem_b ? human_bytes($mem_b) : 'N/A';
|
| 389 |
+
$upt = $app['pm2_env']['pm_uptime'] ?? 0;
|
| 390 |
+
$upt_h = $upt ? (floor((time() - intval($upt/1000)) / 3600) . 'h') : 'N/A';
|
| 391 |
+
$b = status_badge($st);
|
| 392 |
+
echo "<tr>
|
| 393 |
+
<td>".htmlspecialchars($name)."</td>
|
| 394 |
+
<td>".htmlspecialchars($pm_id)."</td>
|
| 395 |
+
<td><span class='badge bg-{$b[0]}'>".htmlspecialchars($b[1])."</span></td>
|
| 396 |
+
<td>".htmlspecialchars($mode)."</td>
|
| 397 |
+
<td>".($cpu !== null ? htmlspecialchars($cpu).'%' : 'N/A')."</td>
|
| 398 |
+
<td>".htmlspecialchars($memh)."</td>
|
| 399 |
+
<td>".htmlspecialchars($upt_h)."</td>
|
| 400 |
+
</tr>";
|
| 401 |
+
}
|
| 402 |
+
} else {
|
| 403 |
+
foreach ($pm2 as $app) {
|
| 404 |
+
$b = status_badge($app['status'] ?? 'unknown');
|
| 405 |
+
echo "<tr>
|
| 406 |
+
<td>".htmlspecialchars($app['name'] ?? 'app')."</td>
|
| 407 |
+
<td>".htmlspecialchars($app['pm_id'] ?? '')."</td>
|
| 408 |
+
<td><span class='badge bg-{$b[0]}'>".htmlspecialchars($b[1])."</span></td>
|
| 409 |
+
<td>".htmlspecialchars($app['mode'] ?? '')."</td>
|
| 410 |
+
<td>".htmlspecialchars($app['cpu'] ?? 'N/A')."</td>
|
| 411 |
+
<td>".htmlspecialchars($app['mem'] ?? 'N/A')."</td>
|
| 412 |
+
<td>".htmlspecialchars($app['uptime'] ?? 'N/A')."</td>
|
| 413 |
+
</tr>";
|
| 414 |
+
}
|
| 415 |
+
}
|
| 416 |
+
?>
|
| 417 |
+
</tbody>
|
| 418 |
+
</table>
|
| 419 |
+
</div>
|
| 420 |
+
<?php else: ?>
|
| 421 |
+
<div class="small text-secondary">PM2 not detected or no apps configured.</div>
|
| 422 |
+
<?php endif; ?>
|
| 423 |
+
</div>
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
|
| 427 |
+
<div class="col-12">
|
| 428 |
+
<div class="card">
|
| 429 |
+
<div class="card-body">
|
| 430 |
+
<h5 class="card-title"><span class="emoji">🪵</span>Logs</h5>
|
| 431 |
+
<form method="get" class="row g-2 mb-3" aria-label="Log selection">
|
| 432 |
+
<div class="col-md-6">
|
| 433 |
+
<label for="log" class="form-label">Log file path</label>
|
| 434 |
+
<input type="text" class="form-control" id="log" name="log" value="<?= htmlspecialchars($log_path) ?>" placeholder="/var/log/nginx/access.log" aria-describedby="logHelp">
|
| 435 |
+
<div id="logHelp" class="form-text">Enter a readable log file path to tail.</div>
|
| 436 |
+
</div>
|
| 437 |
+
<div class="col-md-3">
|
| 438 |
+
<label for="autorefresh2" class="form-label">Auto-refresh</label>
|
| 439 |
+
<select class="form-select" id="autorefresh2" name="autorefresh">
|
| 440 |
+
<?php foreach ([0,5,10,15,30,60,120,300,600] as $sec): ?>
|
| 441 |
+
<option value="<?= $sec ?>" <?= $sec===$auto_refresh?'selected':''; ?>><?= $sec ?>s</option>
|
| 442 |
+
<?php endforeach; ?>
|
| 443 |
+
</select>
|
| 444 |
+
</div>
|
| 445 |
+
<div class="col-md-3 d-flex align-items-end">
|
| 446 |
+
<button class="btn btn-success w-100" type="submit">Update</button>
|
| 447 |
+
</div>
|
| 448 |
+
</form>
|
| 449 |
+
<pre class="p-3" style="background:#0b0f17; border-radius:12px; max-height:380px; overflow:auto; white-space:pre-wrap;"><?= htmlspecialchars($log_tail) ?></pre>
|
| 450 |
+
</div>
|
| 451 |
+
</div>
|
| 452 |
+
</div>
|
| 453 |
+
|
| 454 |
+
</div>
|
| 455 |
+
</main>
|
| 456 |
+
|
| 457 |
+
<footer class="container py-4 footer">
|
| 458 |
+
<div class="d-flex flex-wrap justify-content-between">
|
| 459 |
+
<div>© <?= date('Y') ?> GhostAI • High-contrast, keyboard-navigable UI</div>
|
| 460 |
+
<div>
|
| 461 |
+
<span class="me-2">Contrast: AAA</span>
|
| 462 |
+
<span class="me-2">ARIA Labels Enabled</span>
|
| 463 |
+
<span>Keyboard Shortcuts: <kbd>Tab</kbd> to navigate</span>
|
| 464 |
+
</div>
|
| 465 |
+
</div>
|
| 466 |
+
</footer>
|
| 467 |
+
|
| 468 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" defer></script>
|
| 469 |
+
</body>
|
| 470 |
+
</html>
|