ghostai1 commited on
Commit
ceead6b
·
verified ·
1 Parent(s): 6e4276d

Upload index.php

Browse files

DASH for server admin overview ops

Files changed (1) hide show
  1. 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>