mailwolt/app/Livewire/Ui/Dashboard/HealthCard.php

352 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Livewire\Ui\Dashboard;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
class HealthCard extends Component
{
// Rohdaten
public array $services = [];
public array $meta = [];
// UI: Balken/Segmente
public int $barSegments = 24;
public array $cpuSeg = [];
public array $ramSeg = [];
// UI: Load & Uptime
public string $loadBadgeText = '';
public array $loadDots = [];
public array $uptimeChips = [];
public string $uptimeIcon = 'ph ph-clock';
// UI: Services kompakt
public array $servicesCompact = [];
// Storage
public ?int $diskUsedGb = null;
public ?int $diskTotalGb = null;
public ?int $diskPercent = null;
public ?int $diskFreeGb = null;
public array $diskSegments = [];
public int $diskInnerSize = 160; // grauer Kreis
public int $diskSegOuterRadius = 92; // Abstand der Ticks
public array $diskCenterText = ['percent'=>'%','label'=>'Speicher belegt'];
// Anzeige oben
public ?string $ramSummary = null;
public ?string $loadText = null;
public ?string $uptimeText = null;
public ?int $cpuPercent = null;
public ?int $ramPercent = null;
public ?string $updatedAtHuman = null;
public $guardOk = [];
public function mount(): void
{
$this->loadData();
}
public function loadData(): void
{
$this->services = Cache::get('health:services', []);
$this->meta = Cache::get('health:meta', []);
$this->hydrateSystem();
$this->hydrateDisk();
$this->hydrateUpdatedAt();
$this->decorateServicesCompact();
$this->decorateDisk();
}
public function render()
{
return view('livewire.ui.dashboard.health-card');
}
/* ---------------- Aufbereitung ---------------- */
protected function hydrateSystem(): void
{
$sys = is_array($this->meta['system'] ?? null) ? $this->meta['system'] : [];
// RAM %
$this->ramPercent = $this->numInt($this->pick($sys, ['ram','ram_percent','mem_percent','memory']));
if ($this->ramPercent === null && is_array($sys['ram'] ?? null)) {
$this->ramPercent = $this->numInt($this->pick($sys['ram'], ['percent','used_percent']));
}
// RAM Summary "used / total"
$used = $this->numInt($this->pick($sys['ram'] ?? [], ['used_gb','used','usedGB']));
$tot = $this->numInt($this->pick($sys['ram'] ?? [], ['total_gb','total','totalGB']));
$this->ramSummary = ($used !== null && $tot !== null) ? "{$used} GB / {$tot} GB" : null;
// Load (1/5/15)
$load1 = $this->numFloat($this->pick($sys, ['cpu_load_1','load1','load_1']));
$load5 = $this->numFloat($this->pick($sys, ['cpu_load_5','load5','load_5']));
$load15 = $this->numFloat($this->pick($sys, ['cpu_load_15','load15','load_15']));
$loadMixed = $this->pick($sys, ['load','loadavg','load_avg','load_text']);
if (is_array($loadMixed)) {
$vals = array_values($loadMixed);
$load1 ??= $this->numFloat($vals[0] ?? null);
$load5 ??= $this->numFloat($vals[1] ?? null);
$load15 ??= $this->numFloat($vals[2] ?? null);
}
$this->loadText = $this->fmtLoad($load1, $load5, $load15, is_string($loadMixed) ? $loadMixed : null);
// CPU %
$this->cpuPercent = $this->numInt($this->pick($sys, ['cpu','cpu_percent','cpuUsage','cpu_usage']));
if ($this->cpuPercent === null && $load1 !== null) {
$cores = $this->numInt($this->pick($sys, ['cores','cpu_cores','num_cores'])) ?: 1;
$this->cpuPercent = (int) round(min(100, max(0, ($load1 / max(1,$cores)) * 100)));
}
// Uptime
$this->uptimeText = $this->pick($sys, ['uptime_human','uptimeText','uptime']);
if (!$this->uptimeText) {
$uptimeSec = $this->numInt($this->pick($sys, ['uptime_seconds','uptime_sec','uptime_s']));
if ($uptimeSec !== null) $this->uptimeText = $this->secondsToHuman($uptimeSec);
}
// Segment-Balken
$this->cpuSeg = $this->buildSegments($this->cpuPercent);
$this->ramSeg = $this->buildSegments($this->ramPercent);
// Load: Dots (1/5/15 relativ zu Kernen)
$cores = max(1, (int)($this->pick($sys, ['cores','cpu_cores','num_cores']) ?? 1));
$ratio1 = $load1 !== null ? $load1 / $cores : null;
$ratio5 = $load5 !== null ? $load5 / $cores : null;
$ratio15 = $load15 !== null ? $load15 / $cores : null;
$this->loadBadgeText = $this->loadText ?? '';
$this->loadDots = [
['label'=>'1m', 'cls'=>$this->loadDotClass($ratio1)],
['label'=>'5m', 'cls'=>$this->loadDotClass($ratio5)],
['label'=>'15m', 'cls'=>$this->loadDotClass($ratio15)],
];
// Uptime Chips
$chips = [];
if ($this->uptimeText) {
$d = $h = $m = null;
if (preg_match('/(\d+)d/i', $this->uptimeText, $m1)) $d = (int)$m1[1];
if (preg_match('/(\d+)h/i', $this->uptimeText, $m2)) $h = (int)$m2[1];
if (preg_match('/(\d+)m/i', $this->uptimeText, $m3)) $m = (int)$m3[1];
if ($d !== null) $chips[] = ['v'=>$d, 'u'=>'Tage'];
if ($h !== null) $chips[] = ['v'=>$h, 'u'=>'Stunden'];
if ($m !== null) $chips[] = ['v'=>$m, 'u'=>'Minuten'];
}
$this->uptimeChips = $chips ?: [['v'=>'','u'=>'']];
}
protected function hydrateDisk(): void
{
$disk = is_array($this->meta['disk'] ?? null) ? $this->meta['disk'] : [];
$this->diskPercent = $this->numInt($this->pick($disk, ['percent','usage']));
$this->diskFreeGb = $this->numInt($this->pick($disk, ['free_gb','free']));
// total/used berechnen, falls nicht geliefert
if ($this->diskFreeGb !== null && $this->diskPercent !== null && $this->diskPercent < 100) {
$p = max(0, min(100, $this->diskPercent)) / 100;
$estTotal = (int) round($this->diskFreeGb / (1 - $p));
$this->diskTotalGb = $estTotal > 0 ? $estTotal : null;
}
if ($this->diskTotalGb !== null && $this->diskFreeGb !== null) {
$u = $this->diskTotalGb - $this->diskFreeGb;
$this->diskUsedGb = $u >= 0 ? $u : null;
}
}
protected function hydrateUpdatedAt(): void
{
$updated = $this->meta['updated_at'] ?? null;
try { $this->updatedAtHuman = $updated ? Carbon::parse($updated)->diffForHumans() : '';
} catch (\Throwable) { $this->updatedAtHuman = ''; }
}
protected function decorateServicesCompact(): void
{
$nameMap = [
'postfix' => ['label' => 'Postfix', 'hint' => 'MTA'],
'dovecot' => ['label' => 'Dovecot', 'hint' => 'IMAP/POP3'],
'rspamd' => ['label' => 'Rspamd', 'hint' => 'Spamfilter'],
'db' => ['label' => 'Datenbank', 'hint' => 'MySQL/MariaDB'],
'127.0.0.1:6379' => ['label' => 'Redis', 'hint' => 'Cache/Queue'],
'127.0.0.1:8080' => ['label' => 'Reverb', 'hint' => 'WebSocket'],
];
// $nameMap = [
// // --- Mail ---
// 'postfix' => ['label' => 'Postfix', 'hint' => 'MTA / Versand'],
// 'dovecot' => ['label' => 'Dovecot', 'hint' => 'IMAP / POP3'],
// 'rspamd' => ['label' => 'Rspamd', 'hint' => 'Spamfilter'],
// 'clamav' => ['label' => 'ClamAV', 'hint' => 'Virenscanner'],
//
// // --- Daten & Cache ---
// 'db' => ['label' => 'Datenbank', 'hint' => 'MySQL / MariaDB'],
// '127.0.0.1:6379' => ['label' => 'Redis', 'hint' => 'Cache / Queue'],
//
// // --- Web / PHP ---
// 'php8.2-fpm' => ['label' => 'PHP-FPM', 'hint' => 'PHP Runtime'],
// 'nginx' => ['label' => 'Nginx', 'hint' => 'Webserver'],
//
// // --- MailWolt spezifisch ---
// 'mailwolt-queue' => ['label' => 'MailWolt Queue', 'hint' => 'Job Worker'],
// 'mailwolt-schedule'=> ['label' => 'MailWolt Schedule','hint' => 'Task Scheduler'],
//
// // --- Sonstige Infrastruktur ---
// 'fail2ban' => ['label' => 'Fail2Ban', 'hint' => 'SSH / Mail Protection'],
// 'systemd-journald'=> ['label' => 'System Logs', 'hint' => 'Logging / Journal'],
//
// // --- WebSocket & Echtzeit ---
// '127.0.0.1:8080' => ['label' => 'Reverb', 'hint' => 'WebSocket Server'],
// ];
$existing = collect($this->services)->keyBy('name');
$this->servicesCompact = collect($nameMap)
->map(function ($meta, $key) use ($existing) {
$srv = $existing->get($key, []);
$ok = (bool)($srv['ok'] ?? false);
return [
'label' => $meta['label'],
'hint' => $meta['hint'],
'ok' => $ok,
// Punktfarbe
'dotClass' => $ok ? 'bg-emerald-400' : 'bg-rose-400',
// ✅ Bessere Status-Texte
'pillText' => $ok ? 'Aktiv' : 'Offline',
// Farbe für Pill
'pillClass' => $ok
? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'
: 'text-rose-300 border-rose-400/30 bg-rose-500/10',
];
})
->values()
->all();
$this->guardOk = collect($this->services)->every(
fn($s) => (bool)($s['ok'] ?? false)
);
// $this->servicesCompact = collect($this->services)
// ->map(function ($srv) use ($nameMap) {
// $key = (string)($srv['name'] ?? '');
// $ok = (bool) ($srv['ok'] ?? false);
// $label = $nameMap[$key]['label'] ?? ucfirst($key);
// $hint = $nameMap[$key]['hint'] ?? (str_contains($key, ':') ? $key : '');
//
// var_dump($srv);
// return [
// 'label' => $label,
// 'hint' => $hint,
// 'ok' => $ok,
// 'dotClass' => $ok ? 'bg-emerald-400' : 'bg-rose-400',
// 'pillText' => $ok ? 'Läuft' : 'Down',
// 'pillClass' => $ok
// ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'
// : 'text-rose-300 border-rose-400/30 bg-rose-500/10',
// ];
// })
// ->values()
// ->all();
}
protected function decorateDisk(): void
{
$percent = (int) max(0, min(100, (int)($this->diskPercent ?? 0)));
$this->diskCenterText = [
'percent' => is_numeric($this->diskPercent) ? $this->diskPercent.'%' : '%',
'label' => 'Speicher belegt',
];
$count = 48;
$activeN = is_numeric($this->diskPercent) ? (int) round($count * $percent / 100) : 0;
$step = 360 / $count;
$activeClass = match (true) {
$percent >= 90 => 'bg-rose-400',
$percent >= 70 => 'bg-amber-300',
default => 'bg-emerald-400',
};
$this->diskSegments = [];
for ($i = 0; $i < $count; $i++) {
$angle = ($i * $step) - 90; // Start bei 12 Uhr
$this->diskSegments[] = [
'angle' => $angle,
'class' => $i < $activeN ? $activeClass : 'bg-white/16',
];
}
}
/* ---------------- Helpers ---------------- */
protected function pick(array $arr, array $keys)
{
foreach ($keys as $k) {
if (array_key_exists($k, $arr) && $arr[$k] !== null && $arr[$k] !== '') {
return $arr[$k];
}
}
return null;
}
protected function numInt($v): ?int { return is_numeric($v) ? (int) round($v) : null; }
protected function numFloat($v): ?float{ return is_numeric($v) ? (float)$v : null; }
protected function fmtLoad(?float $l1, ?float $l5, ?float $l15, ?string $fallback): ?string
{
if ($l1 !== null || $l5 !== null || $l15 !== null) {
$fmt = fn($x) => $x === null ? '' : number_format($x, 2);
return "{$fmt($l1)} / {$fmt($l5)} / {$fmt($l15)}";
}
return $fallback ?: null;
}
protected function secondsToHuman(int $s): string
{
$d = intdiv($s, 86400); $s %= 86400;
$h = intdiv($s, 3600); $s %= 3600;
$m = intdiv($s, 60);
if ($d > 0) return "{$d}d {$h}h";
if ($h > 0) return "{$h}h {$m}m";
return "{$m}m";
}
protected function toneByPercent(?int $p): string {
if ($p === null) return 'white';
if ($p >= 90) return 'rose';
if ($p >= 70) return 'amber';
return 'emerald';
}
protected function buildSegments(?int $percent): array {
$n = max(6, $this->barSegments);
$filled = is_numeric($percent) ? (int) round($n * max(0, min(100, $percent)) / 100) : 0;
$tone = $this->toneByPercent($percent);
$fillCls = match($tone) {
'rose' => 'bg-rose-500/80',
'amber' => 'bg-amber-400/80',
'emerald'=> 'bg-emerald-500/80',
default => 'bg-white/20',
};
return array_map(fn($i) => $i < $filled ? $fillCls : 'bg-white/10', range(0, $n-1));
}
protected function loadDotClass(?float $ratio): string {
if ($ratio === null) return 'bg-white/25';
if ($ratio >= 0.9) return 'bg-rose-500';
if ($ratio >= 0.7) return 'bg-amber-400';
return 'bg-emerald-400';
}
}