mailwolt/app/Livewire/Ui/System/ServicesCard.php

336 lines
12 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\System;
use App\Support\CacheVer;
use App\Support\WoltGuard\Probes;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
use Illuminate\Support\Collection;
class ServicesCard extends Component
{
use Probes;
/** Daten fürs Blade */
public array $servicesCompact = [];
public int $okCount = 0;
public int $totalCount = 0;
public bool $guardOk = false;
// Badge (rechts)
public string $badgeText = '';
public string $badgeClass = '';
public string $badgeIcon = 'ph ph-check-circle';
// optionales Polling im Blade (du hast es dort referenziert)
public int $pollSeconds = 15;
public function mount(): void
{
$this->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
{
// 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; }
}
}