loadData(); } public function loadData(): void { $this->meta = Cache::get('health:meta', []); $this->hydrateSystem(); $this->hydrateIps(); $this->hydrateUpdatedAt(); } public function getListeners(): array { // exakt auf die Event-Klasse hören (kein Alias) return [ 'echo-private:health,HealthUpdated' => 'onHealthEcho', ]; } public function onHealthEcho($payload): void { // ① Bei manchen Treibern kommt $payload als JSON-String if (is_string($payload)) { $decoded = json_decode($payload, true); if (json_last_error() === JSON_ERROR_NONE) { $payload = $decoded; } } // ② Manche Treiber kapseln erneut in "data" if (isset($payload['data']) && is_string($payload['data'])) { $decoded = json_decode($payload['data'], true); if (json_last_error() === JSON_ERROR_NONE) { $payload = $decoded; } } elseif (isset($payload['data']) && is_array($payload['data'])) { $payload = $payload['data']; } $meta = $payload['meta'] ?? []; if (!is_array($meta)) $meta = []; $this->meta = $meta; $this->hydrateSystem(); $this->hydrateIps(); $this->hydrateUpdatedAt(); // optionales Debug-Log (kein JS nötig) logger('Health WS received', ['meta' => $this->meta]); } public function render() { return view('livewire.ui.system.health-card'); } /* --------------- Aufbereitung --------------- */ protected function hydrateSystem(): void { $sys = is_array($this->meta['system'] ?? null) ? $this->meta['system'] : []; // RAM % + Summary $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'])); } $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) $l1 = $this->numFloat($this->pick($sys, ['cpu_load_1','load1','load_1'])); $l5 = $this->numFloat($this->pick($sys, ['cpu_load_5','load5','load_5'])); $l15 = $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); $l1 ??= $this->numFloat($vals[0] ?? null); $l5 ??= $this->numFloat($vals[1] ?? null); $l15 ??= $this->numFloat($vals[2] ?? null); } $this->loadText = $this->fmtLoad($l1, $l5, $l15, is_string($loadMixed) ? $loadMixed : null); // CPU % $this->cpuPercent = $this->numInt($this->pick($sys, ['cpu','cpu_percent','cpuUsage','cpu_usage'])); if ($this->cpuPercent === null && $l1 !== null) { $cores = $this->numInt($this->pick($sys, ['cores','cpu_cores','num_cores'])) ?: 1; $this->cpuPercent = (int) round(min(100, max(0, ($l1 / 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); // } $uptimeSec = $this->numInt($this->pick($sys, ['uptime_seconds','uptime_sec','uptime_s'])); if ($uptimeSec !== null) { $parsed = $this->parseUptime($uptimeSec); $this->uptimeText = "{$parsed['days']}d {$parsed['hours']}h"; // bestehende Anzeige $this->uptimeDays = $parsed['days']; $this->uptimeHours = $parsed['hours']; $this->uptimeDaysLabel = $parsed['days_text']; $this->uptimeHoursLabel = $parsed['hours_text']; } // Segmente $this->cpuSeg = $this->buildSegments($this->cpuPercent); $this->ramSeg = $this->buildSegments($this->ramPercent); // Load Dots relativ zu Kernen $cores = max(1, (int)($this->pick($sys, ['cores','cpu_cores','num_cores']) ?? 1)); $ratio1 = $l1 !== null ? $l1 / $cores : null; $ratio5 = $l5 !== null ? $l5 / $cores : null; $ratio15 = $l15 !== null ? $l15 / $cores : null; $this->loadDots = [ ['label'=>'1m', 'cls'=>$this->loadDotClass($ratio1)], ['label'=>'5m', 'cls'=>$this->loadDotClass($ratio5)], ['label'=>'15m', 'cls'=>$this->loadDotClass($ratio15)], ]; } protected function hydrateIps(): void { $sys = is_array($this->meta['system'] ?? null) ? $this->meta['system'] : []; // Versuche diverse mögliche Keys $this->ip4 = $this->pick($sys, ['ipv4','ip4','public_ipv4','server_public_ipv4','lan_ipv4']); $this->ip6 = $this->pick($sys, ['ipv6','ip6','public_ipv6','server_public_ipv6','lan_ipv6']); // Fallback: aus installer.env lesen if (!$this->ip4 || !$this->ip6) { $env = @file('/etc/mailwolt/installer.env', FILE_IGNORE_NEW_LINES) ?: []; foreach ($env as $ln) { if (!$this->ip4 && str_starts_with($ln, 'SERVER_PUBLIC_IPV4=')) { $this->ip4 = trim(substr($ln, strlen('SERVER_PUBLIC_IPV4='))); } if (!$this->ip6 && str_starts_with($ln, 'SERVER_PUBLIC_IPV6=')) { $this->ip6 = trim(substr($ln, strlen('SERVER_PUBLIC_IPV6='))); } } } // Auf hübsch: dünne Zwischenräume (wir lassen Blade das stylen – monospaced + tracking) $this->ip4 = $this->ip4 ?: '–'; $this->ip6 = $this->ip6 ?: '–'; } protected function hydrateUpdatedAt(): void { $updated = $this->meta['updated_at'] ?? null; try { $this->updatedAtHuman = $updated ? Carbon::parse($updated)->diffForHumans() : '–'; } catch (\Throwable) { $this->updatedAtHuman = '–'; } } /* --------------- 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 parseUptime(int $seconds): array { $days = intdiv($seconds, 86400); $hours = intdiv($seconds % 86400, 3600); $minutes = intdiv($seconds % 3600, 60); return [ 'days' => $days, 'hours' => $hours, 'minutes' => $minutes, 'days_text' => $days === 1 ? 'Tag' : 'Tage', // string 'hours_text' => $hours === 1 ? 'Stunde' : 'Stunden', // string ]; } 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'; } }