'–%','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'; } }