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

278 lines
11 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\WoltGuard\Probes;
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;
/** Pro Karte mehrere Quellen Grün sobald eine Quelle OK ist */
// protected array $cards = [
// // Mail
// 'postfix' => ['label'=>'Postfix','hint'=>'MTA / Versand','sources'=>['systemd:postfix']],
// 'dovecot' => ['label'=>'Dovecot','hint'=>'IMAP / POP3','sources'=>['systemd:dovecot','tcp:127.0.0.1:993']],
// 'rspamd' => ['label'=>'Rspamd','hint'=>'Spamfilter','sources'=>['systemd:rspamd','tcp:127.0.0.1:11333','tcp:127.0.0.1:11334']],
// 'clamav' => ['label'=>'ClamAV','hint'=>'Virenscanner','sources'=>[
// 'systemd:clamav-daemon','systemd:clamav-daemon@scan','systemd:clamd',
// 'socket:/run/clamav/clamd.ctl','pid:/run/clamav/clamd.pid','tcp:127.0.0.1:3310',
// ]],
// // Daten & Cache
// 'db' => ['label'=>'Datenbank','hint'=>'MySQL / MariaDB','sources'=>['db']],
// 'redis' => ['label'=>'Redis','hint'=>'Cache / Queue','sources'=>['tcp:127.0.0.1:6379','systemd:redis-server','systemd:redis']],
// // Web / PHP
// 'php-fpm' => ['label'=>'PHP-FPM','hint'=>'PHP Runtime','sources'=>[
// 'systemd:php8.3-fpm','systemd:php8.2-fpm','systemd:php8.1-fpm','systemd:php-fpm',
// 'socket:/run/php/php8.3-fpm.sock','socket:/run/php/php8.2-fpm.sock',
// 'socket:/run/php/php8.1-fpm.sock','socket:/run/php/php-fpm.sock','tcp:127.0.0.1:9000',
// ]],
// 'nginx' => ['label'=>'Nginx','hint'=>'Webserver','sources'=>['systemd:nginx','tcp:127.0.0.1:80']],
// // MailWolt (Unit ODER laufender artisan Prozess)
// 'mw-queue' => ['label'=>'MailWolt Queue','hint'=>'Job Worker','sources'=>['systemd:mailwolt-queue','proc:/php.*artisan(\.php)?\s+queue:work']],
// 'mw-schedule' => ['label'=>'MailWolt Schedule','hint'=>'Task Scheduler','sources'=>['systemd:mailwolt-schedule','proc:/php.*artisan(\.php)?\s+schedule:work']],
// 'mw-ws' => ['label'=>'MailWolt WebSocket','hint'=>'Echtzeit Updates','sources'=>['systemd:mailwolt-ws','tcp:127.0.0.1:8080']],
// // Sonstiges
// 'fail2ban' => ['label'=>'Fail2Ban','hint'=>'SSH / Mail Protection','sources'=>['systemd:fail2ban']],
// 'journal' => ['label'=>'System Logs','hint'=>'Journal','sources'=>['systemd:systemd-journald','systemd:rsyslog']],
// ];
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
{
$cards = config('woltguard.cards', []);
$cached = collect(Cache::get('health:services', []))->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; }
}
}