Última actividad 1 month ago

Revisión 8a37141c1ac6820918f245f8272fc266e52bbebf

beszel_dashboard_simple.php Sin formato
1/**
2 * Dashboard Beszel Simple - Version sans graphiques avec rafraîchissement automatique
3 * Fonctionnalités :
4 * - Données en temps réel sans graphiques
5 * - Rafraîchissement automatique configurable
6 * - Options persistantes
7 * - Interface simple et rapide
8 */
9
10// === CONFIGURATION ===
11$baseUrl = 'URL';
12$username = 'mail';
13$password = 'rpass';
14
15// === CONFIGURATION DES COLLECTIONS ===
16$collections = [
17 'metrics' => 'system_stats',
18 'alerts' => 'alerts',
19 'systems' => 'systems'
20];
21
22// === RÉCUPÉRATION DES PRÉFÉRENCES ===
23$refreshInterval = $_GET['refresh'] ?? $_COOKIE['beszel_refresh'] ?? '30';
24$autoRefresh = $_GET['auto'] ?? $_COOKIE['beszel_auto'] ?? 'true';
25$theme = $_GET['theme'] ?? $_COOKIE['beszel_theme'] ?? 'light';
26$viewMode = $_GET['view'] ?? $_COOKIE['beszel_view'] ?? 'cards';
27
28// Forcer le rafraîchissement automatique si pas défini
29if (!isset($_COOKIE['beszel_auto'])) {
30 $autoRefresh = 'true';
31}
32
33// Sauvegarder les préférences
34if (isset($_GET['refresh'])) setcookie('beszel_refresh', $refreshInterval, time() + (365 * 24 * 60 * 60));
35if (isset($_GET['auto'])) setcookie('beszel_auto', $autoRefresh, time() + (365 * 24 * 60 * 60));
36if (isset($_GET['theme'])) setcookie('beszel_theme', $theme, time() + (365 * 24 * 60 * 60));
37if (isset($_GET['view'])) setcookie('beszel_view', $viewMode, time() + (365 * 24 * 60 * 60));
38
39// === FONCTIONS UTILITAIRES ===
40function getToken() {
41 global $baseUrl, $username, $password;
42 $ch = curl_init($baseUrl . '/api/collections/users/auth-with-password');
43 curl_setopt_array($ch, [
44 CURLOPT_RETURNTRANSFER => true,
45 CURLOPT_POST => true,
46 CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
47 CURLOPT_POSTFIELDS => json_encode([
48 'identity' => $username,
49 'password' => $password
50 ])
51 ]);
52 $resp = curl_exec($ch);
53 $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
54 curl_close($ch);
55 if ($code !== 200) die("Erreur auth ($code): $resp");
56 $data = json_decode($resp, true);
57 return $data['token'] ?? null;
58}
59
60function apiGet($endpoint, $token) {
61 global $baseUrl;
62 $ch = curl_init($baseUrl . $endpoint);
63 curl_setopt_array($ch, [
64 CURLOPT_RETURNTRANSFER => true,
65 CURLOPT_HTTPHEADER => [
66 "Authorization: Bearer $token",
67 "Content-Type: application/json"
68 ]
69 ]);
70 $resp = curl_exec($ch);
71 $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
72 curl_close($ch);
73 if ($code !== 200) {
74 error_log("Erreur API ($endpoint) : $code\n$resp");
75 return ['items' => []];
76 }
77 return json_decode($resp, true);
78}
79
80function formatBytes($bytes, $precision = 2) {
81 $units = array('B', 'KB', 'MB', 'GB', 'TB');
82 for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
83 $bytes /= 1024;
84 }
85 return round($bytes, $precision) . ' ' . $units[$i];
86}
87
88function getStatusColor($status) {
89 switch ($status) {
90 case 'up': return '#10b981';
91 case 'down': return '#ef4444';
92 case 'paused': return '#f59e0b';
93 default: return '#6b7280';
94 }
95}
96
97function getStatusIcon($status) {
98 switch ($status) {
99 case 'up': return '🟢';
100 case 'down': return '🔴';
101 case 'paused': return '⏸️';
102 default: return '⚪';
103 }
104}
105
106function getCpuColorClass($cpu) {
107 if ($cpu < 30) return 'cpu-low';
108 if ($cpu < 70) return 'cpu-medium';
109 return 'cpu-high';
110}
111
112function getLoadColorClass($load) {
113 if ($load < 1) return 'load-low';
114 if ($load < 3) return 'load-medium';
115 return 'load-high';
116}
117
118function getTempColorClass($temp) {
119 if ($temp < 40) return 'temp-low';
120 if ($temp < 70) return 'temp-medium';
121 return 'temp-high';
122}
123
124function getDataFreshnessClass($lastUpdate) {
125 $now = time();
126 $dataTime = strtotime($lastUpdate);
127 $age = $now - $dataTime;
128
129 if ($age < 60) return 'data-fresh'; // < 1 minute
130 if ($age < 300) return 'data-recent'; // < 5 minutes
131 return 'data-old'; // > 5 minutes
132}
133
134function getProgressColorClass($value, $type) {
135 switch ($type) {
136 case 'cpu':
137 if ($value < 30) return 'cpu-low';
138 if ($value < 70) return 'cpu-medium';
139 return 'cpu-high';
140 case 'memory':
141 if ($value < 30) return 'memory-low';
142 if ($value < 70) return 'memory-medium';
143 return 'memory-high';
144 case 'load':
145 if ($value < 1) return 'load-low';
146 if ($value < 3) return 'load-medium';
147 return 'load-high';
148 case 'temp':
149 if ($value < 40) return 'temp-low';
150 if ($value < 70) return 'temp-medium';
151 return 'temp-high';
152 case 'disk':
153 if ($value < 30) return 'disk-low';
154 if ($value < 70) return 'disk-medium';
155 return 'disk-high';
156 case 'gpu':
157 if ($value < 30) return 'gpu-low';
158 if ($value < 70) return 'gpu-medium';
159 return 'gpu-high';
160 default:
161 return 'cpu-low';
162 }
163}
164
165function getMetricColorClass($value, $type) {
166 switch ($type) {
167 case 'cpu':
168 if ($value < 30) return 'cpu-low';
169 if ($value < 70) return 'cpu-medium';
170 return 'cpu-high';
171 case 'memory':
172 if ($value < 30) return 'memory-low';
173 if ($value < 70) return 'memory-medium';
174 return 'memory-high';
175 case 'load':
176 if ($value < 1) return 'load-low';
177 if ($value < 3) return 'load-medium';
178 return 'load-high';
179 case 'temp':
180 if ($value < 40) return 'temp-low';
181 if ($value < 70) return 'temp-medium';
182 return 'temp-high';
183 case 'disk':
184 if ($value < 30) return 'disk-low';
185 if ($value < 70) return 'disk-medium';
186 return 'disk-high';
187 case 'gpu':
188 if ($value < 30) return 'gpu-low';
189 if ($value < 70) return 'gpu-medium';
190 return 'gpu-high';
191 default:
192 return 'cpu-low';
193 }
194}
195
196// === RÉCUPÉRATION DES DONNÉES ===
197$token = getToken();
198if (!$token) {
199 die("❌ Impossible d'obtenir le token d'authentification.");
200}
201
202$data = apiGet('/api/collections/systems/records?perPage=50&expand=latest_metrics', $token);
203$systems = $data['items'] ?? [];
204
205// Récupération des alertes
206$alertsData = apiGet('/api/collections/alerts/records?sort=-created&perPage=20', $token);
207$alerts = $alertsData['items'] ?? [];
208
209// === ENVOI MQTT VERS HOME ASSISTANT ===
210$mqttHost = "192.168.1.200"; // IP de ton Home Assistant
211$mqttUser = "mqtt32mqtt32"; // identifiants MQTT
212$mqttPass = "mqtt32";
213
214// Boucle sur chaque système
215foreach ($systems as $sys) {
216 $name = preg_replace('/[^a-zA-Z0-9_-]/', '_', strtolower($sys['name'] ?? 'unknown'));
217 $info = $sys['info'] ?? [];
218 $payload = [
219 'cpu' => $info['cpu'] ?? 0,
220 'ram' => $info['mp'] ?? 0,
221 'load' => $info['la'][0] ?? 0,
222 'temp' => $info['t'] ?? 0,
223 'disk' => $info['dp'] ?? 0,
224 'gpu' => $info['g'] ?? 0,
225 'status' => $sys['status'] ?? 'unknown',
226 'time' => date('c')
227 ];
228
229 $json = json_encode($payload);
230 // Envoi sur un topic dédié au système
231 exec("mosquitto_pub -h $mqttHost -u $mqttUser -P $mqttPass -t beszel/$name -m '$json'");
232}
233
234
235?>
236<!DOCTYPE html>
237<html lang="fr">
238<head>
239 <meta charset="utf-8">
240 <meta name="viewport" content="width=device-width, initial-scale=1.0">
241 <title>Dashboard32 Beszel Simple</title>
242 <style>
243 :root {
244 --bg: #f8fafc;
245 --card-bg: #ffffff;
246 --text: #1e293b;
247 --sub: #64748b;
248 --border: #e2e8f0;
249 --primary: #3b82f6;
250 --success: #10b981;
251 --warning: #f59e0b;
252 --danger: #ef4444;
253 --info: #06b6d4;
254 --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
255 --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
256 }
257
258 body.dark {
259 --bg: #0f172a;
260 --card-bg: #1e293b;
261 --text: #f1f5f9;
262 --sub: #94a3b8;
263 --border: #334155;
264 --primary: #60a5fa;
265 --success: #34d399;
266 --warning: #fbbf24;
267 --danger: #f87171;
268 --info: #22d3ee;
269 }
270
271 * {
272 margin: 0;
273 padding: 0;
274 box-sizing: border-box;
275 }
276
277 body {
278 background: var(--bg);
279 color: var(--text);
280 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
281 line-height: 1.6;
282 transition: all 0.3s ease;
283 }
284
285 .header {
286 background: var(--card-bg);
287 border-bottom: 1px solid var(--border);
288 padding: 1rem 2rem;
289 display: flex;
290 justify-content: space-between;
291 align-items: center;
292 box-shadow: var(--shadow);
293 position: sticky;
294 top: 0;
295 z-index: 100;
296 }
297
298 .header h1 {
299 font-size: 1.5rem;
300 font-weight: 700;
301 color: var(--text);
302 }
303
304 .controls {
305 display: flex;
306 gap: 1rem;
307 align-items: center;
308 }
309
310 .btn {
311 background: var(--primary);
312 color: white;
313 border: none;
314 border-radius: 8px;
315 padding: 0.5rem 1rem;
316 font-size: 0.875rem;
317 font-weight: 600;
318 cursor: pointer;
319 transition: all 0.2s;
320 text-decoration: none;
321 display: inline-flex;
322 align-items: center;
323 gap: 0.5rem;
324 }
325
326 .btn:hover {
327 background: #2563eb;
328 transform: translateY(-1px);
329 }
330
331 .btn-secondary {
332 background: var(--border);
333 color: var(--text);
334 }
335
336 .btn-secondary:hover {
337 background: var(--sub);
338 }
339
340 .btn-success {
341 background: var(--success);
342 }
343
344 .btn-warning {
345 background: var(--warning);
346 }
347
348 .btn-danger {
349 background: var(--danger);
350 }
351
352 .settings-panel {
353 position: fixed;
354 top: 0;
355 right: -400px;
356 width: 400px;
357 height: 100vh;
358 background: var(--card-bg);
359 border-left: 1px solid var(--border);
360 padding: 2rem;
361 transition: right 0.3s ease;
362 z-index: 1000;
363 overflow-y: auto;
364 }
365
366 .settings-panel.open {
367 right: 0;
368 }
369
370 .settings-overlay {
371 position: fixed;
372 top: 0;
373 left: 0;
374 width: 100%;
375 height: 100%;
376 background: rgba(0, 0, 0, 0.5);
377 z-index: 999;
378 opacity: 0;
379 visibility: hidden;
380 transition: all 0.3s ease;
381 }
382
383 .settings-overlay.open {
384 opacity: 1;
385 visibility: visible;
386 }
387
388 .container {
389 max-width: 100%;
390 margin: 0;
391 padding: 1rem;
392 }
393
394 .stats-grid {
395 display: grid;
396 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
397 gap: 0.75rem;
398 margin-bottom: 1.5rem;
399 }
400
401 .stat-card {
402 background: var(--card-bg);
403 border-radius: 8px;
404 padding: 1rem;
405 box-shadow: var(--shadow);
406 border: 1px solid var(--border);
407 text-align: center;
408 }
409
410 .stat-value {
411 font-size: 2rem;
412 font-weight: 700;
413 color: var(--text);
414 margin-bottom: 0.5rem;
415 }
416
417 .stat-label {
418 font-size: 0.875rem;
419 color: var(--sub);
420 text-transform: uppercase;
421 letter-spacing: 0.05em;
422 }
423
424 .systems-grid {
425 display: grid;
426 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
427 gap: 1rem;
428 width: 100%;
429 align-items: stretch;
430 }
431
432 .system-card {
433 background: var(--card-bg);
434 border-radius: 8px;
435 box-shadow: var(--shadow);
436 border: 1px solid var(--border);
437 overflow: hidden;
438 transition: all 0.3s ease;
439 display: flex;
440 flex-direction: column;
441 height: 100%;
442 }
443
444 .system-card:hover {
445 transform: translateY(-2px);
446 box-shadow: var(--shadow-lg);
447 }
448
449 .card-header {
450 padding: 1rem;
451 border-bottom: 1px solid var(--border);
452 display: flex;
453 justify-content: space-between;
454 align-items: center;
455 min-height: 60px;
456 flex-shrink: 0;
457 }
458
459 .card-title {
460 font-size: 1.25rem;
461 font-weight: 600;
462 color: var(--text);
463 }
464
465 .status {
466 display: inline-flex;
467 align-items: center;
468 gap: 0.5rem;
469 padding: 0.25rem 0.75rem;
470 border-radius: 20px;
471 font-size: 0.75rem;
472 font-weight: 600;
473 text-transform: uppercase;
474 }
475
476 .status.up {
477 background: rgba(16, 185, 129, 0.1);
478 color: var(--success);
479 }
480
481 .status.down {
482 background: rgba(239, 68, 68, 0.1);
483 color: var(--danger);
484 }
485
486 .status.paused {
487 background: rgba(245, 158, 11, 0.1);
488 color: var(--warning);
489 }
490
491 .status.unknown {
492 background: rgba(107, 114, 128, 0.1);
493 color: var(--sub);
494 }
495
496 .card-content {
497 padding: 1rem;
498 }
499
500 .metrics-grid {
501 display: grid;
502 grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
503 gap: 0.5rem;
504 margin-bottom: 1rem;
505 }
506
507 .metric {
508 text-align: center;
509 padding: 0.75rem;
510 background: var(--bg);
511 border-radius: 6px;
512 border: 1px solid var(--border);
513 transition: all 0.2s;
514 }
515
516 /* Couleurs pour les mini-cadres des métriques */
517 .metric.cpu-low { background: rgba(16, 185, 129, 0.1); border-color: #10b981; }
518 .metric.cpu-medium { background: rgba(245, 158, 11, 0.1); border-color: #f59e0b; }
519 .metric.cpu-high { background: rgba(239, 68, 68, 0.1); border-color: #ef4444; }
520
521 .metric.memory-low { background: rgba(16, 185, 129, 0.1); border-color: #10b981; }
522 .metric.memory-medium { background: rgba(245, 158, 11, 0.1); border-color: #f59e0b; }
523 .metric.memory-high { background: rgba(239, 68, 68, 0.1); border-color: #ef4444; }
524
525 .metric.load-low { background: rgba(16, 185, 129, 0.1); border-color: #10b981; }
526 .metric.load-medium { background: rgba(245, 158, 11, 0.1); border-color: #f59e0b; }
527 .metric.load-high { background: rgba(239, 68, 68, 0.1); border-color: #ef4444; }
528
529 .metric.temp-low { background: rgba(6, 182, 212, 0.1); border-color: #06b6d4; }
530 .metric.temp-medium { background: rgba(245, 158, 11, 0.1); border-color: #f59e0b; }
531 .metric.temp-high { background: rgba(239, 68, 68, 0.1); border-color: #ef4444; }
532
533 .metric.disk-low { background: rgba(16, 185, 129, 0.1); border-color: #10b981; }
534 .metric.disk-medium { background: rgba(245, 158, 11, 0.1); border-color: #f59e0b; }
535 .metric.disk-high { background: rgba(239, 68, 68, 0.1); border-color: #ef4444; }
536
537 .metric.gpu-low { background: rgba(16, 185, 129, 0.1); border-color: #10b981; }
538 .metric.gpu-medium { background: rgba(245, 158, 11, 0.1); border-color: #f59e0b; }
539 .metric.gpu-high { background: rgba(239, 68, 68, 0.1); border-color: #ef4444; }
540
541 .metric:hover {
542 background: var(--border);
543 }
544
545 .metric-value {
546 font-size: 1rem;
547 font-weight: 700;
548 color: var(--text);
549 margin-bottom: 0.25rem;
550 }
551
552 /* Couleurs selon les valeurs */
553 .metric.cpu-low { background: rgba(16, 185, 129, 0.1); border-color: #10b981; }
554 .metric.cpu-medium { background: rgba(245, 158, 11, 0.1); border-color: #f59e0b; }
555 .metric.cpu-high { background: rgba(239, 68, 68, 0.1); border-color: #ef4444; }
556
557 .metric.load-low { background: rgba(16, 185, 129, 0.1); border-color: #10b981; }
558 .metric.load-medium { background: rgba(245, 158, 11, 0.1); border-color: #f59e0b; }
559 .metric.load-high { background: rgba(239, 68, 68, 0.1); border-color: #ef4444; }
560
561 .metric.temp-low { background: rgba(6, 182, 212, 0.1); border-color: #06b6d4; }
562 .metric.temp-medium { background: rgba(245, 158, 11, 0.1); border-color: #f59e0b; }
563 .metric.temp-high { background: rgba(239, 68, 68, 0.1); border-color: #ef4444; }
564
565 /* Couleurs pour la fraîcheur des données */
566 .data-fresh { color: #10b981; }
567 .data-recent { color: #f59e0b; }
568 .data-old { color: #ef4444; }
569
570 /* Badges pour IP et Port */
571 .badge {
572 display: inline-flex;
573 align-items: center;
574 gap: 0.2rem;
575 padding: 0.15rem 0.3rem;
576 border-radius: 5px;
577 font-size: 0.6rem;
578 font-weight: 600;
579 text-transform: uppercase;
580 letter-spacing: 0.05em;
581 margin: 0.05rem;
582 transition: all 0.2s;
583 white-space: nowrap;
584 }
585
586 .badge:hover {
587 transform: translateY(-1px);
588 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
589 }
590
591 .badge-ip {
592 background: rgba(59, 130, 246, 0.1);
593 color: #3b82f6;
594 border: 1px solid #3b82f6;
595 }
596
597 .badge-port {
598 background: rgba(16, 185, 129, 0.1);
599 color: #10b981;
600 border: 1px solid #10b981;
601 }
602
603 .badge-id {
604 background: rgba(107, 114, 128, 0.1);
605 color: #6b7280;
606 border: 1px solid #6b7280;
607 }
608
609 .badge-time {
610 background: rgba(6, 182, 212, 0.1);
611 color: #06b6d4;
612 border: 1px solid #06b6d4;
613 }
614
615 /* Styles pour l'affichage en liste */
616 .list-view {
617 display: none;
618 }
619
620 .list-view.active {
621 display: block;
622 }
623
624 .cards-view {
625 /* display: block;*/
626 }
627
628 .cards-view.hidden {
629 display: none;
630 }
631
632 .list-item {
633 background: var(--card-bg);
634 border-radius: 8px;
635 box-shadow: var(--shadow);
636 border: 1px solid var(--border);
637 margin-bottom: 0.5rem;
638 padding: 0.75rem;
639 /*display: flex;*/
640 align-items: center;
641 justify-content: space-between;
642 transition: all 0.3s ease;
643 min-height: 80px;
644 }
645
646 .list-item:hover {
647 transform: translateY(-1px);
648 box-shadow: var(--shadow-lg);
649 }
650
651 .list-item-left {
652 display: flex;
653 align-items: center;
654 gap: 1rem;
655 flex: 0 0 250px;
656 min-width: 250px;
657 }
658
659 .list-item-right {
660 display: flex;
661 align-items: center;
662 gap: 0.5rem;
663 /* flex: 1;*/
664
665 justify-content: space-between;
666 }
667 /*
668 .list-system-name {
669 font-size: 1rem;
670 font-weight: 600;
671 color: var(--text);
672 white-space: nowrap;
673 overflow: hidden;
674 text-overflow: ellipsis;
675 flex: 1;
676 min-width: 200px;
677 }*/
678 .list-system-name {
679 font-size: 1rem;
680 font-weight: 600;
681 color: #3bb2e1;
682 white-space: nowrap;
683 overflow: hidden;
684 text-overflow: ellipsis;
685 flex: 1;
686 min-width: 200px;
687 }
688
689 .list-metrics {
690 display: grid;
691 grid-template-columns: repeat(6, 1fr);
692 gap: 0.25rem;
693 align-items: center;
694 flex: 1;
695 margin-right: 0.5rem;
696 min-width: 500px;
697 }
698
699 .list-metric {
700 text-align: center;
701 padding: 0.15rem;
702 display: flex;
703 flex-direction: column;
704 align-items: center;
705 min-width: 85px;
706 max-width: 85px;
707 }
708
709 .list-metric-value {
710 font-size: 0.8rem;
711 font-weight: 700;
712 color: var(--text);
713 margin-bottom: 0.1rem;
714 min-height: 1em;
715 }
716
717 .list-metric-label {
718 font-size: 0.65rem;
719 color: var(--sub);
720 text-transform: uppercase;
721 letter-spacing: 0.05em;
722 margin-bottom: 0.08rem;
723 min-height: 0.8em;
724 }
725
726 /* Barres de progression pour la vue liste */
727 .progress-bar {
728 width: 100%;
729 height: 4px;
730 background: var(--border);
731 border-radius: 2px;
732 overflow: hidden;
733 margin-top: 0.05rem;
734 min-width: 50px;
735 }
736
737 .progress-fill {
738 height: 100%;
739 border-radius: 2px;
740 transition: width 0.3s ease;
741 }
742
743 .progress-fill.cpu-low { background: #10b981; }
744 .progress-fill.cpu-medium { background: #f59e0b; }
745 .progress-fill.cpu-high { background: #ef4444; }
746
747 .progress-fill.memory-low { background: #10b981; }
748 .progress-fill.memory-medium { background: #f59e0b; }
749 .progress-fill.memory-high { background: #ef4444; }
750
751 .progress-fill.load-low { background: #10b981; }
752 .progress-fill.load-medium { background: #f59e0b; }
753 .progress-fill.load-high { background: #ef4444; }
754
755 .progress-fill.temp-low { background: #06b6d4; }
756 .progress-fill.temp-medium { background: #f59e0b; }
757 .progress-fill.temp-high { background: #ef4444; }
758
759 .progress-fill.disk-low { background: #10b981; }
760 .progress-fill.disk-medium { background: #f59e0b; }
761 .progress-fill.disk-high { background: #ef4444; }
762
763 /* Badges dans la vue liste */
764 .list-badges {
765 display: flex;
766 flex-wrap: nowrap;
767 gap: 0.2rem;
768 align-items: center;
769 flex-shrink: 0;
770 min-width: 320px;
771 }
772
773 .metric-label {
774 font-size: 0.75rem;
775 color: var(--sub);
776 text-transform: uppercase;
777 letter-spacing: 0.05em;
778 }
779
780 .system-info {
781 margin-top: auto;
782 padding-top: 0.75rem;
783 border-top: 1px solid var(--border);
784 font-size: 0.75rem;
785 color: var(--sub);
786 flex-shrink: 0;
787 }
788
789 .refresh-indicator {
790 position: fixed;
791 bottom: 2rem;
792 right: 2rem;
793 background: var(--card-bg);
794 border: 1px solid var(--border);
795 border-radius: 8px;
796 padding: 0.75rem 1rem;
797 box-shadow: var(--shadow-lg);
798 font-size: 0.875rem;
799 color: var(--sub);
800 }
801
802 @keyframes spin {
803 from { transform: rotate(0deg); }
804 to { transform: rotate(360deg); }
805 }
806
807 .form-group {
808 margin-bottom: 1rem;
809 }
810
811 .form-label {
812 display: block;
813 margin-bottom: 0.5rem;
814 font-weight: 600;
815 color: var(--text);
816 }
817
818 .form-select, .form-input {
819 width: 100%;
820 padding: 0.75rem;
821 border: 1px solid var(--border);
822 border-radius: 8px;
823 background: var(--card-bg);
824 color: var(--text);
825 font-size: 0.875rem;
826 }
827
828 .form-select:focus, .form-input:focus {
829 outline: none;
830 border-color: var(--primary);
831 box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
832 }
833
834 .checkbox-group {
835 display: flex;
836 align-items: center;
837 gap: 0.5rem;
838 }
839
840 .checkbox-group input[type="checkbox"] {
841 width: 16px;
842 height: 16px;
843 }
844
845 .checkbox-group label {
846 font-size: 0.875rem;
847 color: var(--text);
848 cursor: pointer;
849 }
850
851 .alerts-section {
852 margin-top: 2rem;
853 }
854
855 .alert-item {
856 display: flex;
857 align-items: center;
858 gap: 0.75rem;
859 padding: 0.75rem;
860 background: var(--bg);
861 border-radius: 8px;
862 margin-bottom: 0.5rem;
863 font-size: 0.875rem;
864 border-left: 4px solid var(--warning);
865 }
866
867 .alert-critical {
868 border-left-color: var(--danger);
869 }
870
871 .alert-warning {
872 border-left-color: var(--warning);
873 }
874
875 .alert-info {
876 border-left-color: var(--info);
877 }
878
879 @media (max-width: 768px) {
880 .header {
881 padding: 1rem;
882 flex-direction: column;
883 gap: 1rem;
884 }
885
886 .container {
887 padding: 0.5rem;
888 }
889
890 .systems-grid {
891 grid-template-columns: 1fr;
892 gap: 0.5rem;
893 }
894
895 .metrics-grid {
896 grid-template-columns: repeat(auto-fit, minmax(70px, 1fr));
897 }
898
899 .badge {
900 font-size: 0.65rem;
901 padding: 0.2rem 0.4rem;
902 }
903
904 .list-item {
905 flex-direction: column;
906 align-items: flex-start;
907 gap: 1rem;
908 }
909
910 .list-item-left {
911 width: 100%;
912 justify-content: space-between;
913 }
914
915 .list-item-right {
916 width: 100%;
917 flex-direction: column;
918 gap: 0.5rem;
919 }
920
921 .list-metrics {
922 grid-template-columns: repeat(3, 1fr);
923 gap: 0.15rem;
924 min-width: 280px;
925 }
926
927 .list-badges {
928 min-width: 200px;
929 flex-wrap: wrap;
930 }
931
932 .list-item {
933 flex-direction: column;
934 align-items: flex-start;
935 gap: 0.5rem;
936 min-height: auto;
937 }
938
939 .list-item-left {
940 flex: 1;
941 min-width: auto;
942 width: 100%;
943 }
944
945 .list-item-right {
946 width: 100%;
947 flex-direction: column;
948 gap: 0.5rem;
949 }
950
951 .list-system-name {
952 min-width: auto;
953 }
954
955 .settings-panel {
956 width: 100%;
957 right: -100%;
958 }
959 }
960 </style>
961</head>
962<body class="<?= $theme === 'dark' ? 'dark' : '' ?>">
963 <div class="header">
964 <h1>🚀 Dashboard32 Beszel Simple</h1>
965 <div class="controls">
966 <div style="font-size: 0.75rem; color: var(--sub); margin-right: 1rem;">
967 🕒 Données: <?= date('H:i:s') ?> (temps réel)
968 </div>
969 <button class="btn btn-secondary" onclick="toggleView()"><?= $viewMode === 'cards' ? '📋 Liste' : '🃏 Cartes' ?></button>
970 <button class="btn btn-secondary" onclick="toggleSettings()">⚙️ Paramètres</button>
971 <button class="btn btn-secondary" onclick="toggleLegend()">🎨 Légende</button>
972 <button class="btn btn-secondary" onclick="toggleTheme()">🌗 Thème</button>
973 <button class="btn btn-secondary" onclick="location.reload()">🔄 Actualiser</button>
974 </div>
975 </div>
976
977 <div class="settings-overlay" id="settingsOverlay" onclick="closeSettings()"></div>
978 <div class="settings-panel" id="settingsPanel">
979 <h2>Paramètres du Dashboard</h2>
980
981 <form method="GET" action="">
982 <div class="form-group">
983 <label class="form-label">Rafraîchissement automatique (secondes)</label>
984 <select class="form-select" name="refresh">
985 <option value="10" <?= $refreshInterval === '10' ? 'selected' : '' ?>>10 secondes</option>
986 <option value="30" <?= $refreshInterval === '30' ? 'selected' : '' ?>>30 secondes</option>
987 <option value="60" <?= $refreshInterval === '60' ? 'selected' : '' ?>>1 minute</option>
988 <option value="120" <?= $refreshInterval === '120' ? 'selected' : '' ?>>2 minutes</option>
989 <option value="300" <?= $refreshInterval === '300' ? 'selected' : '' ?>>5 minutes</option>
990 <option value="0" <?= $refreshInterval === '0' ? 'selected' : '' ?>>Désactivé</option>
991 </select>
992 </div>
993
994 <div class="form-group">
995 <label class="form-label">Thème</label>
996 <select class="form-select" name="theme">
997 <option value="light" <?= $theme === 'light' ? 'selected' : '' ?>>Clair</option>
998 <option value="dark" <?= $theme === 'dark' ? 'selected' : '' ?>>Sombre</option>
999 </select>
1000 </div>
1001
1002 <div class="form-group">
1003 <div class="checkbox-group">
1004 <input type="checkbox" id="autoRefresh" name="auto" value="true" <?= $autoRefresh === 'true' ? 'checked' : '' ?>>
1005 <label for="autoRefresh">Rafraîchissement automatique</label>
1006 </div>
1007 </div>
1008
1009 <button type="submit" class="btn">Appliquer</button>
1010 <button type="button" class="btn btn-secondary" onclick="closeSettings()">Fermer</button>
1011 </form>
1012 </div>
1013
1014 <!-- Fenêtre modale pour la légende -->
1015 <div class="settings-overlay" id="legendOverlay" onclick="closeLegend()"></div>
1016 <div class="settings-panel" id="legendPanel">
1017 <h2>🎨 Légende des couleurs</h2>
1018
1019 <div style="margin-bottom: 1.5rem;">
1020 <h3 style="margin-bottom: 1rem; color: var(--text);">Seuils de couleur :</h3>
1021 <div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
1022 <span style="color: #10b981; font-weight: 600;">🟢 Faible</span>
1023 <span style="color: #f59e0b; font-weight: 600;">🟡 Moyen</span>
1024 <span style="color: #ef4444; font-weight: 600;">🔴 Élevé</span>
1025 </div>
1026 </div>
1027
1028 <div style="margin-bottom: 1.5rem;">
1029 <h4 style="margin-bottom: 0.5rem; color: var(--text);">CPU (Processeur) :</h4>
1030 <div style="font-size: 0.875rem; color: var(--sub);">
1031 <span style="color: #10b981;">&lt; 30%</span> |
1032 <span style="color: #f59e0b;">30-70%</span> |
1033 <span style="color: #ef4444;">&gt; 70%</span>
1034 </div>
1035 </div>
1036
1037 <div style="margin-bottom: 1.5rem;">
1038 <h4 style="margin-bottom: 0.5rem; color: var(--text);">Load (Charge système) :</h4>
1039 <div style="font-size: 0.875rem; color: var(--sub);">
1040 <span style="color: #10b981;">&lt; 1</span> |
1041 <span style="color: #f59e0b;">1-3</span> |
1042 <span style="color: #ef4444;">&gt; 3</span>
1043 </div>
1044 </div>
1045
1046 <div style="margin-bottom: 1.5rem;">
1047 <h4 style="margin-bottom: 0.5rem; color: var(--text);">Température :</h4>
1048 <div style="font-size: 0.875rem; color: var(--sub);">
1049 <span style="color: #06b6d4;">&lt; 40°C</span> |
1050 <span style="color: #f59e0b;">40-70°C</span> |
1051 <span style="color: #ef4444;">&gt; 70°C</span>
1052 </div>
1053 </div>
1054
1055 <button type="button" class="btn btn-secondary" onclick="closeLegend()">Fermer</button>
1056 </div>
1057
1058 <div class="container">
1059
1060 <!-- Statistiques globales -->
1061 <div class="stats-grid">
1062 <div class="stat-card">
1063 <div class="stat-value"><?= count($systems) ?></div>
1064 <div class="stat-label">Systèmes</div>
1065 </div>
1066 <div class="stat-card">
1067 <div class="stat-value"><?= count(array_filter($systems, fn($s) => $s['status'] === 'up')) ?></div>
1068 <div class="stat-label">En ligne</div>
1069 </div>
1070 <div class="stat-card">
1071 <div class="stat-value"><?= count(array_filter($systems, fn($s) => $s['status'] === 'down')) ?></div>
1072 <div class="stat-label">Hors ligne</div>
1073 </div>
1074 <div class="stat-card">
1075 <div class="stat-value"><?= count(array_filter($systems, fn($s) => $s['status'] === 'paused')) ?></div>
1076 <div class="stat-label">En pause</div>
1077 </div>
1078 <div class="stat-card">
1079 <div class="stat-value"><?= count($alerts) ?></div>
1080 <div class="stat-label">Alertes</div>
1081 </div>
1082 </div>
1083
1084 <!-- Systèmes - Vue Cartes -->
1085 <div class="systems-grid cards-view <?= $viewMode === 'cards' ? '' : 'hidden' ?>">
1086 <?php foreach ($systems as $sys):
1087 $name = htmlspecialchars($sys['name'] ?? 'Système inconnu');
1088 $status = htmlspecialchars($sys['status'] ?? 'unknown');
1089 // Utiliser l'heure actuelle comme dernière mise à jour des données
1090 $lastUpdate = date('Y-m-d H:i:s');
1091 $systemId = $sys['id'] ?? '';
1092
1093 // Récupération des métriques actuelles
1094 $info = $sys['info'] ?? [];
1095 $cpu = $info['cpu'] ?? 0;
1096 $memory = $info['mp'] ?? 0;
1097 $load = $info['la'][0] ?? 0;
1098 // Utiliser la température si disponible, sinon utiliser le CPU comme approximation
1099 $temperature = $info['t'] ?? ($cpu > 0 ? $cpu : 0);
1100 $disk = $info['dp'] ?? 0;
1101 $networkIn = $info['bb'] ?? 0;
1102 $gpu = $info['g'] ?? 0;
1103 ?>
1104 <div class="system-card">
1105 <div class="card-header">
1106 <div class="card-title"><?= $name ?></div>
1107 <div class="status <?= $status ?>">
1108 <span><?= getStatusIcon($status) ?></span>
1109 <?= strtoupper($status) ?>
1110 </div>
1111 </div>
1112
1113 <div class="card-content">
1114 <div class="metrics-grid">
1115 <div class="metric <?= getMetricColorClass($cpu, 'cpu') ?>">
1116 <div class="metric-value"><?= round($cpu, 1) ?>%</div>
1117 <div class="metric-label">CPU</div>
1118 </div>
1119 <div class="metric <?= getMetricColorClass($memory, 'memory') ?>">
1120 <div class="metric-value"><?= round($memory, 1) ?>%</div>
1121 <div class="metric-label">RAM</div>
1122 </div>
1123 <div class="metric <?= getMetricColorClass($load, 'load') ?>">
1124 <div class="metric-value"><?= round($load, 2) ?></div>
1125 <div class="metric-label">Load</div>
1126 </div>
1127 <div class="metric <?= getMetricColorClass($temperature, 'temp') ?>">
1128 <div class="metric-value"><?= round($temperature, 1) ?>°C</div>
1129 <div class="metric-label">Temp</div>
1130 </div>
1131 <div class="metric <?= getMetricColorClass($disk, 'disk') ?>">
1132 <div class="metric-value"><?= round($disk, 1) ?>%</div>
1133 <div class="metric-label">Disk</div>
1134 </div>
1135 <?php if ($gpu > 0): ?>
1136 <div class="metric <?= getMetricColorClass($gpu, 'gpu') ?>">
1137 <div class="metric-value"><?= round($gpu, 1) ?>%</div>
1138 <div class="metric-label">GPU</div>
1139 </div>
1140 <?php endif; ?>
1141 </div>
1142
1143 <div class="system-info">
1144 <div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.5rem;">
1145 <span class="badge badge-time">🕒 <?= $lastUpdate ?></span>
1146 <span class="badge badge-id">🆔 <?= substr($systemId, 0, 8) ?>...</span>
1147 <span class="badge badge-ip">🌐 <?= htmlspecialchars($sys['host'] ?? 'N/A') ?></span>
1148 <span class="badge badge-port">🔌 <?= htmlspecialchars($sys['port'] ?? 'N/A') ?></span>
1149 </div>
1150 </div>
1151 </div>
1152 </div>
1153 <?php endforeach; ?>
1154 </div>
1155
1156 <!-- Systèmes - Vue Liste -->
1157 <div class="list-view <?= $viewMode === 'list' ? 'active' : '' ?>">
1158 <?php foreach ($systems as $sys):
1159 $name = htmlspecialchars($sys['name'] ?? 'Système inconnu');
1160 $status = htmlspecialchars($sys['status'] ?? 'unknown');
1161 $lastUpdate = date('Y-m-d H:i:s');
1162 $systemId = $sys['id'] ?? '';
1163
1164 // Récupération des métriques actuelles
1165 $info = $sys['info'] ?? [];
1166 $cpu = $info['cpu'] ?? 0;
1167 $memory = $info['mp'] ?? 0;
1168 $load = $info['la'][0] ?? 0;
1169 $temperature = $info['t'] ?? ($cpu > 0 ? $cpu : 0);
1170 $disk = $info['dp'] ?? 0;
1171 $gpu = $info['g'] ?? 0;
1172 ?>
1173 <div class="list-item">
1174 <div class="list-item-left">
1175 <div class="list-system-name"><?= $name ?></div>
1176 <div class="status <?= $status ?>">
1177 <span><?= getStatusIcon($status) ?></span>
1178 <?= strtoupper($status) ?>
1179 </div>
1180 </div>
1181 <div class="list-item-right">
1182 <div class="list-metrics">
1183 <div class="list-metric">
1184 <div class="list-metric-value"><?= round($cpu, 1) ?>%</div>
1185 <div class="list-metric-label">CPU</div>
1186 <div class="progress-bar">
1187 <div class="progress-fill <?= getProgressColorClass($cpu, 'cpu') ?>" style="width: <?= min($cpu, 100) ?>%"></div>
1188 </div>
1189 </div>
1190 <div class="list-metric">
1191 <div class="list-metric-value"><?= round($memory, 1) ?>%</div>
1192 <div class="list-metric-label">RAM</div>
1193 <div class="progress-bar">
1194 <div class="progress-fill <?= getProgressColorClass($memory, 'memory') ?>" style="width: <?= min($memory, 100) ?>%"></div>
1195 </div>
1196 </div>
1197 <div class="list-metric">
1198 <div class="list-metric-value"><?= round($load, 2) ?></div>
1199 <div class="list-metric-label">Load</div>
1200 <div class="progress-bar">
1201 <div class="progress-fill <?= getProgressColorClass($load, 'load') ?>" style="width: <?= min($load * 20, 100) ?>%"></div>
1202 </div>
1203 </div>
1204 <div class="list-metric">
1205 <div class="list-metric-value"><?= round($temperature, 1) ?>°C</div>
1206 <div class="list-metric-label">Temp</div>
1207 <div class="progress-bar">
1208 <div class="progress-fill <?= getProgressColorClass($temperature, 'temp') ?>" style="width: <?= min($temperature * 1.2, 100) ?>%"></div>
1209 </div>
1210 </div>
1211 <div class="list-metric">
1212 <div class="list-metric-value"><?= round($disk, 1) ?>%</div>
1213 <div class="list-metric-label">Disk</div>
1214 <div class="progress-bar">
1215 <div class="progress-fill <?= getProgressColorClass($disk, 'disk') ?>" style="width: <?= min($disk, 100) ?>%"></div>
1216 </div>
1217 </div>
1218 <?php if ($gpu > 0): ?>
1219 <div class="list-metric">
1220 <div class="list-metric-value"><?= round($gpu, 1) ?>%</div>
1221 <div class="list-metric-label">GPU</div>
1222 <div class="progress-bar">
1223 <div class="progress-fill <?= getProgressColorClass($gpu, 'cpu') ?>" style="width: <?= min($gpu, 100) ?>%"></div>
1224 </div>
1225 </div>
1226 <?php endif; ?>
1227 </div>
1228 <div class="list-badges">
1229 <span class="badge badge-time">🕒 <?= $lastUpdate ?></span>
1230 <span class="badge badge-id">🆔 <?= substr($systemId, 0, 8) ?>...</span>
1231 <span class="badge badge-ip">🌐 <?= htmlspecialchars($sys['host'] ?? 'N/A') ?></span>
1232 <span class="badge badge-port">🔌 <?= htmlspecialchars($sys['port'] ?? 'N/A') ?></span>
1233 </div>
1234 </div>
1235 </div>
1236 <?php endforeach; ?>
1237 </div>
1238
1239 <!-- Alertes -->
1240 <?php if (!empty($alerts)): ?>
1241 <div class="alerts-section">
1242 <h2>🚨 Alertes récentes</h2>
1243 <?php foreach (array_slice($alerts, 0, 10) as $alert): ?>
1244 <div class="alert-item alert-<?= $alert['severity'] ?? 'info' ?>">
1245 <div>
1246 <strong><?= htmlspecialchars($alert['message'] ?? 'Alerte') ?></strong><br>
1247 <small><?= date('d/m/Y H:i', strtotime($alert['created'])) ?></small>
1248 </div>
1249 </div>
1250 <?php endforeach; ?>
1251 </div>
1252 <?php endif; ?>
1253 </div>
1254
1255 <?php if ($autoRefresh === 'true' && $refreshInterval > 0): ?>
1256 <div class="refresh-indicator" id="refreshIndicator">
1257 <span id="refreshStatus">🔄</span> Prochaine mise à jour dans <span id="countdown"><?= $refreshInterval ?></span>s
1258 </div>
1259 <?php endif; ?>
1260
1261 <script>
1262 // Configuration des thèmes
1263 function toggleTheme() {
1264 const currentTheme = document.body.classList.contains('dark') ? 'dark' : 'light';
1265 const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
1266 window.location.href = `?theme=${newTheme}&refresh=<?= $refreshInterval ?>&auto=<?= $autoRefresh ?>`;
1267 }
1268
1269 // Gestion des paramètres
1270 function toggleSettings() {
1271 document.getElementById('settingsPanel').classList.toggle('open');
1272 document.getElementById('settingsOverlay').classList.toggle('open');
1273 }
1274
1275 function closeSettings() {
1276 document.getElementById('settingsPanel').classList.remove('open');
1277 document.getElementById('settingsOverlay').classList.remove('open');
1278 }
1279
1280 // Gestion de la légende
1281 function toggleLegend() {
1282 document.getElementById('legendPanel').classList.toggle('open');
1283 document.getElementById('legendOverlay').classList.toggle('open');
1284 }
1285
1286 function closeLegend() {
1287 document.getElementById('legendPanel').classList.remove('open');
1288 document.getElementById('legendOverlay').classList.remove('open');
1289 }
1290
1291 // Gestion du mode d'affichage
1292 function toggleView() {
1293 const currentView = '<?= $viewMode ?>';
1294 const newView = currentView === 'cards' ? 'list' : 'cards';
1295 window.location.href = `?view=${newView}&refresh=<?= $refreshInterval ?>&auto=<?= $autoRefresh ?>&theme=<?= $theme ?>`;
1296 }
1297
1298 // Compteur de rafraîchissement
1299 <?php if ($autoRefresh === 'true' && $refreshInterval > 0): ?>
1300 let countdown = <?= $refreshInterval ?>;
1301 const countdownElement = document.getElementById('countdown');
1302 const refreshStatusElement = document.getElementById('refreshStatus');
1303
1304 const countdownInterval = setInterval(function() {
1305 countdown--;
1306 if (countdownElement) {
1307 countdownElement.textContent = countdown;
1308 }
1309
1310 // Animation de l'icône de rafraîchissement
1311 if (refreshStatusElement) {
1312 refreshStatusElement.style.animation = 'spin 1s linear infinite';
1313 }
1314
1315 if (countdown <= 0) {
1316 clearInterval(countdownInterval);
1317 if (refreshStatusElement) {
1318 refreshStatusElement.textContent = '⏳';
1319 refreshStatusElement.style.animation = 'none';
1320 }
1321 location.reload();
1322 }
1323 }, 1000);
1324 <?php endif; ?>
1325
1326 // Mise à jour de l'heure
1327 function updateTime() {
1328 const now = new Date();
1329 const timeString = now.toLocaleTimeString('fr-FR');
1330 document.title = `Dashboard32 Beszel - ${timeString}`;
1331 }
1332
1333 setInterval(updateTime, 1000);
1334 updateTime();
1335
1336 // Forcer le rafraîchissement automatique si pas configuré
1337 <?php if ($autoRefresh === 'true' && $refreshInterval > 0): ?>
1338 // Le rafraîchissement automatique est déjà configuré
1339 <?php else: ?>
1340 // Forcer le rafraîchissement toutes les 30 secondes si pas configuré
1341 setTimeout(function() {
1342 location.reload();
1343 }, 30000);
1344 <?php endif; ?>
1345 </script>
1346</body>
1347</html>
1348