load(); } public function refresh(): void { // nur unsere eigene Services-Card neu aufbauen (Cache vom Job lassen wir in Ruhe) $this->load(); } public function render() { return view('livewire.ui.system.services-card'); } public function load(): void { $cards = config('woltguard.cards', []); // 1) Cache lesen (versionierter Key) $raw = Cache::get(CacheVer::k('health:services')); // 2) Neues Format { ts, rows } → entpacken if (is_array($raw) && array_key_exists('rows', $raw)) { $raw = $raw['rows']; } // 3) Fallback: Legacy-Key ODER Settings (DB/Redis) – ebenfalls entpacken if (empty($raw)) { $legacy = Cache::get('health:services', []); if (is_array($legacy) && array_key_exists('rows', $legacy)) { $raw = $legacy['rows']; } elseif (!empty($legacy)) { $raw = $legacy; } else { // persistierter Payload aus DB (Settings) $persist = \App\Support\Setting::get('woltguard.services', []); $raw = (is_array($persist) && isset($persist['rows']) && is_array($persist['rows'])) ? $persist['rows'] : []; } } // Ab hier ist $raw eine Liste von {name, ok} $cached = collect($raw)->keyBy('name'); $rows = []; foreach ($cards as $key => $card) { $isOk = (bool) ($cached->get($key)['ok'] ?? false); // Nur wenn wirklich gar nichts im Cache ist, aktiv prüfen if ($cached->isEmpty()) { foreach ($card['sources'] as $src) { if ($this->check($src)) { $isOk = true; break; } } } $rows[] = [ 'label' => $card['label'], 'hint' => $card['hint'], 'ok' => $isOk, ]; } // Zähler / Badge / Compact wie vorher… $this->totalCount = count($rows); $this->okCount = collect($rows)->where('ok', true)->count(); $this->guardOk = $this->totalCount > 0 && $this->okCount === $this->totalCount; [$this->badgeText, $this->badgeClass, $this->badgeIcon] = $this->guardOk ? ['alle Dienste OK', 'text-emerald-200 bg-emerald-500/10 border-emerald-400/30', 'ph ph-check-circle'] : ["{$this->okCount}/{$this->totalCount} aktiv", 'text-amber-200 bg-amber-500/10 border-amber-400/30', 'ph ph-warning']; $this->servicesCompact = collect($rows)->map(fn($row) => [ 'label' => $row['label'], 'hint' => $row['hint'], 'ok' => $row['ok'], 'dotClass' => $row['ok'] ? 'bg-emerald-400' : 'bg-rose-400', 'pillText' => $row['ok'] ? 'Online' : 'Offline', 'pillClass' => $row['ok'] ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' : 'text-rose-300 border-rose-400/30 bg-rose-500/10', ])->all(); } // public function load(): void // { // // 1) Karten aus Config laden // $cards = config('woltguard.cards', []); // // if (empty($cards)) { // // Config-Clear/-Cache synchron ausführen (kostet ~ms) // try { // Artisan::call('config:clear'); // Artisan::call('config:cache'); // } catch (\Throwable $e) { // // ignoriere – UI soll trotzdem laufen // } // $cards = config('woltguard.cards', []); // } // // // 2) Service-Status aus Cache (versionierter Key) // $key = \App\Support\CacheVer::k('health:services'); // $raw = Cache::get($key, []); // // Legacy-Key als Fallback // if (empty($raw)) $raw = Cache::get('health:services', []); // // $cached = collect($raw)->keyBy('name'); // // $rows = []; // $ok = 0; // // foreach ($cards as $name => $card) { // $isOk = (bool) ($cached->get($name)['ok'] ?? false); // // // 3) Wenn Cache leer → aktiv prüfen // if ($cached->isEmpty()) { // foreach ($card['sources'] as $src) { // if ($this->check($src)) { $isOk = true; break; } // } // } // // if ($isOk) $ok++; // // $rows[] = [ // 'name' => $name, // <— wichtig, damit der Cache keyBy('name') später klappt // 'label' => $card['label'] ?? $name, // 'hint' => $card['hint'] ?? null, // 'ok' => $isOk, // ]; // } // // // 4) Wenn wir aktiv geprüft haben (Cache war leer) → Cache auffüllen // if ($cached->isEmpty() && !empty($rows)) { // Cache::put($key, $rows, 600); // 10 Minuten // Cache::forget('health:services'); // Legacy aufräumen // } // // // 5) Kopfzahlen + UI-Daten // $this->totalCount = count($rows); // $this->okCount = $ok; // $this->guardOk = $this->totalCount > 0 && $this->okCount === $this->totalCount; // // [$this->badgeText, $this->badgeClass, $this->badgeIcon] = // $this->guardOk // ? ['alle Dienste OK', 'text-emerald-200 bg-emerald-500/10 border-emerald-400/30', 'ph ph-check-circle'] // : ["{$this->okCount}/{$this->totalCount} aktiv", 'text-amber-200 bg-amber-500/10 border-amber-400/30', 'ph ph-warning']; // // $this->servicesCompact = collect($rows)->map(fn($r) => [ // 'label' => $r['label'], // 'hint' => $r['hint'], // 'ok' => $r['ok'], // 'dotClass' => $r['ok'] ? 'bg-emerald-400' : 'bg-rose-400', // 'pillText' => $r['ok'] ? 'Online' : 'Offline', // 'pillClass' => $r['ok'] // ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' // : 'text-rose-300 border-rose-400/30 bg-rose-500/10', // ])->all(); // } // public function load(): void // { // $cards = config('woltguard.cards', []); // // $raw = Cache::get(CacheVer::k('health:services'), []); // // // einmaliger Fallback für ältere Deploys: // if (empty($raw)) { // $raw = Cache::get('health:services', []); // } // // $cached = collect($raw)->keyBy('name'); // // $rows = []; // $ok = 0; // $total = 0; // // foreach ($cards as $key => $card) { // $total++; // // 1) Cache-Hit? // $isOk = (bool) ($cached->get($key)['ok'] ?? false); // // // 2) Fallback probe (falls Cache leer/alt) // if ($cached->isEmpty()) { // foreach ($card['sources'] as $src) { // if ($this->check($src)) { $isOk = true; break; } // } // } // // if ($isOk) $ok++; // // $rows[] = [ // 'label' => $card['label'], // 'hint' => $card['hint'], // 'ok' => $isOk, // ]; // } // // $this->totalCount = count($rows); // $this->okCount = collect($rows)->where('ok', true)->count(); // $this->guardOk = $this->totalCount > 0 && $this->okCount === $this->totalCount; // // [$this->badgeText, $this->badgeClass, $this->badgeIcon] = // $this->guardOk // ? ['alle Dienste OK', 'text-emerald-200 bg-emerald-500/10 border-emerald-400/30', 'ph ph-check-circle'] // : ["{$this->okCount}/{$this->totalCount} aktiv", 'text-amber-200 bg-amber-500/10 border-amber-400/30', 'ph ph-warning']; // // $this->servicesCompact = collect($rows)->map(fn($row) => [ // 'label' => $row['label'], // 'hint' => $row['hint'], // 'ok' => $row['ok'], // 'dotClass' => $row['ok'] ? 'bg-emerald-400' : 'bg-rose-400', // 'pillText' => $row['ok'] ? 'Online' : 'Offline', // 'pillClass' => $row['ok'] // ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10' // : 'text-rose-300 border-rose-400/30 bg-rose-500/10', // ])->all(); // } // public function load(): void // { // // Cache nur als optionaler Beschleuniger für Einzel-Checks, // // NICHT für Summen / Anzahl Services. // $cached = collect(Cache::get('health:services', []))->keyBy('name'); // // $rows = []; // $ok = 0; // $total = 0; // // foreach ($this->cards as $key => $card) { // $total++; // // $isOk = false; // // // 1) Cache-Shortcut (wenn vorhanden) // foreach ($card['sources'] as $src) { // $hit = $this->isSourceOkFromCache($src, $cached); // if ($hit === true) { $isOk = true; break; } // } // // // 2) aktive Probes, wenn Cache nicht geholfen hat // if (!$isOk) { // foreach ($card['sources'] as $src) { // if ($this->check($src)) { $isOk = true; break; } // } // } // // if ($isOk) $ok++; // // $rows[$key] = [ // 'label' => $card['label'], // 'hint' => $card['hint'], // 'ok' => $isOk, // ]; // } // // // Zahlen für die Kopfzeile / Badge // $this->totalCount = $total; // $this->okCount = $ok; // $this->guardOk = $total > 0 && $ok === $total; // // // Badge-Props // if ($this->guardOk) { // $this->badgeText = 'alle Dienste OK'; // $this->badgeClass = 'text-emerald-200 bg-emerald-500/10 border-emerald-400/30'; // $this->badgeIcon = 'ph ph-check-circle'; // } else { // $this->badgeText = "{$ok}/{$total} aktiv"; // $this->badgeClass = 'text-amber-200 bg-amber-500/10 border-amber-400/30'; // $this->badgeIcon = 'ph ph-warning'; // } // // // Liste für die Darstellung // $this->servicesCompact = collect($rows)->map(function ($row) { // $ok = (bool)$row['ok']; // return [ // 'label' => $row['label'], // 'hint' => $row['hint'], // 'ok' => $ok, // 'dotClass' => $ok ? 'bg-emerald-400' : 'bg-rose-400', // 'pillText' => $ok ? 'Online' : 'Offline', // '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(); // } /* ---------- Cache-Matcher ---------- */ protected function isSourceOkFromCache(string $source, Collection $idx): ?bool { if (str_starts_with($source, 'tcp:')) { [$host, $port] = explode(':', substr($source, 4), 2); $name = "{$host}:{$port}"; return $idx->has($name) ? (bool)($idx[$name]['ok'] ?? false) : null; } if ($source === 'db') { return $idx->has('db') ? (bool)($idx['db']['ok'] ?? false) : null; } if (str_starts_with($source, 'systemd:')) { $unit = substr($source, 8); return $idx->has($unit) ? (bool)($idx[$unit]['ok'] ?? false) : null; } return null; // für socket:/ pid:/ proc: keine Cache-Infos } /* ---------- Probes ---------- */ protected function check(string $source): bool { if (str_starts_with($source, 'systemd:')) { return $this->probeSystemd(substr($source, 8)); } if (str_starts_with($source, 'tcp:')) { [$host, $port] = explode(':', substr($source, 4), 2); return $this->probeTcp($host, (int)$port); } if (str_starts_with($source, 'socket:')) { $path = substr($source, 7); return is_string($path) && @file_exists($path); } if (str_starts_with($source, 'pid:')) { $path = substr($source, 4); if (!@is_file($path)) return false; $pid = (int)trim(@file_get_contents($path) ?: ''); return $pid > 1 && @posix_kill($pid, 0); } if (str_starts_with($source, 'proc:')) { $regex = substr($source, 5); return $this->probeProcessRegex($regex); } if ($source === 'db') { return $this->probeDatabase(); } return false; } protected function probeSystemd(string $unit): bool { $bin = file_exists('/bin/systemctl') ? '/bin/systemctl' : (file_exists('/usr/bin/systemctl') ? '/usr/bin/systemctl' : 'systemctl'); $cmd = sprintf('%s is-active --quiet %s 2>/dev/null', escapeshellcmd($bin), escapeshellarg($unit)); $exit = null; @exec($cmd, $_, $exit); return $exit === 0; } protected function probeTcp(string $host, int $port, int $timeout = 1): bool { $fp = @fsockopen($host, $port, $e1, $e2, $timeout); if (is_resource($fp)) { fclose($fp); return true; } return false; } protected function probeProcessRegex(string $regex): bool { $regex = '#' . $regex . '#i'; foreach (@scandir('/proc') ?: [] as $d) { if (!ctype_digit($d)) continue; $cmd = @file_get_contents("/proc/$d/cmdline"); if ($cmd === false || $cmd === '') continue; $cmd = str_replace("\0", ' ', $cmd); if (preg_match($regex, $cmd)) return true; } return false; } protected function probeDatabase(): bool { try { DB::connection()->getPdo(); return true; } catch (\Throwable) { return false; } } }