diff --git a/, b/, new file mode 100644 index 0000000..e69de29 diff --git a/0 b/0 new file mode 100644 index 0000000..e69de29 diff --git a/[hint], b/[hint], new file mode 100644 index 0000000..e69de29 diff --git a/[label], b/[label], new file mode 100644 index 0000000..e69de29 diff --git a/app/Console/Commands/CollectHealth.php b/app/Console/Commands/CollectHealth.php new file mode 100644 index 0000000..ea2a58d --- /dev/null +++ b/app/Console/Commands/CollectHealth.php @@ -0,0 +1,99 @@ + $this->loadavg(0), + 'cpu_load_5' => $this->loadavg(1), + 'cpu_load_15' => $this->loadavg(2), + + // Kerne + 'cores' => $this->cpuCores(), + + // RAM % + Details aus /proc/meminfo (used = MemTotal - MemAvailable) + 'ram' => $this->ramInfo(), + + // Uptime in Sekunden aus /proc/uptime + 'uptime_seconds' => $this->uptimeSeconds(), + + // IPs aus ENV (falls gesetzt) + 'ipv4' => env('SERVER_PUBLIC_IPV4'), + 'ipv6' => env('SERVER_PUBLIC_IPV6'), + ]; + + // Optional: eine “CPU-%”-Schätzung aus Load(1m)/Cores (damit dein UI einen %-Wert hat) + if (is_numeric($system['cpu_load_1']) && $system['cores'] > 0) { + $system['cpu_percent'] = (int) round( + 100 * max(0, min(1, $system['cpu_load_1'] / $system['cores'])) + ); + } + + $meta = [ + 'system' => $system, + 'updated_at' => now()->toIso8601String(), + ]; + + Cache::put('health:meta', $meta, 60); + event(new HealthUpdated($meta)); + + $this->info('Health aktualisiert & gesendet.'); + return self::SUCCESS; + } + + private function loadavg(int $index): ?float + { + $la = @sys_getloadavg(); + return (is_array($la) && isset($la[$index])) ? (float) $la[$index] : null; + } + + private function cpuCores(): int + { + $n = (int) @shell_exec('nproc 2>/dev/null'); + return $n > 0 ? $n : 1; + // Alternativ: count(preg_grep('/^processor\s*:/', @file('/proc/cpuinfo') ?: [])) ?: 1; + } + + private function ramInfo(): array + { + $mem = @file('/proc/meminfo', FILE_IGNORE_NEW_LINES) ?: []; + $kv = []; + foreach ($mem as $ln) { + if (preg_match('/^(\w+):\s+(\d+)/', $ln, $m)) { + $kv[$m[1]] = (int) $m[2]; // kB + } + } + $totalKB = $kv['MemTotal'] ?? 0; + $availKB = $kv['MemAvailable'] ?? 0; // besser als “free” + $usedKB = max(0, $totalKB - $availKB); + + $totalGB = $totalKB / 1024 / 1024; + $usedGB = $usedKB / 1024 / 1024; + $percent = $totalKB > 0 ? (int) round(100 * $usedKB / $totalKB) : null; + + return [ + 'percent' => $percent, + 'used_gb' => (int) round($usedGB), + 'total_gb' => (int) round($totalGB), + ]; + } + + private function uptimeSeconds(): ?int + { + $s = @file_get_contents('/proc/uptime'); + if (!$s) return null; + $parts = explode(' ', trim($s)); + return isset($parts[0]) ? (int) floor((float) $parts[0]) : null; + } +} diff --git a/app/Events/CertPing.php b/app/Events/CertPing.php deleted file mode 100644 index d6db8d6..0000000 --- a/app/Events/CertPing.php +++ /dev/null @@ -1,20 +0,0 @@ - $this->message]; } -} diff --git a/app/Events/HealthUpdated.php b/app/Events/HealthUpdated.php new file mode 100644 index 0000000..d9c400e --- /dev/null +++ b/app/Events/HealthUpdated.php @@ -0,0 +1,27 @@ + $this->meta]; + } +} diff --git a/app/Events/Test.php b/app/Events/Test.php deleted file mode 100644 index 6d32e61..0000000 --- a/app/Events/Test.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ - public function broadcastOn(): array - { - return [ - new PrivateChannel('channel-name'), - ]; - } -} diff --git a/app/Jobs/RunHealthChecks.php b/app/Jobs/RunHealthChecks.php index dcf1efa..960fea1 100644 --- a/app/Jobs/RunHealthChecks.php +++ b/app/Jobs/RunHealthChecks.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Support\WoltGuard\Probes; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Facades\Artisan; @@ -13,52 +14,63 @@ use Symfony\Component\Finder\Finder; class RunHealthChecks implements ShouldQueue { - use Queueable; + use Queueable, Probes; public int $timeout = 10; // safety public int $tries = 1; public function handle(): void { - try { - $services = [ - $this->safe(fn() => $this->service('postfix'), ['name'=>'postfix']), - $this->safe(fn() => $this->service('dovecot'), ['name'=>'dovecot']), - $this->safe(fn() => $this->service('rspamd'), ['name'=>'rspamd']), - $this->safe(fn() => $this->tcp('127.0.0.1', 6379), ['name'=>'redis']), - $this->safe(fn() => $this->db(), ['name'=>'db']), -// $this->safe(fn() => $this->queueWorkers(), ['name'=>'queue']), - $this->safe(fn() => $this->tcp('127.0.0.1', 8080), ['name'=>'reverb']), - ]; - - $meta = [ - 'app_version' => config('app.version', app()->version()), - 'pending_migs' => $this->safe(fn() => $this->pendingMigrationsCount(), 0), - 'cert_soon' => $this->safe(fn() => $this->certificatesDue(30), ['count'=>0,'nearest_days'=>null]), - 'disk' => $this->safe(fn() => $this->diskUsage(), ['percent'=>null,'free_gb'=>null]), - 'system' => $this->systemLoad(), - 'updated_at' => now()->toIso8601String(), - ]; - - Cache::put('health:services', array_values($services), 300); - Cache::put('health:meta', $meta, 300); - Cache::put('metrics:queues', [ - 'outgoing' => 19, - 'incoming' => 5, - 'today_ok' => 834, - 'today_err'=> 12, - 'trend' => [ - 'outgoing' => [2,1,0,4,3,5,4,0,0,0], // letzte 10 Zeitfenster - 'incoming' => [1,0,0,1,0,2,1,0,0,0], - 'ok' => [50,62,71,88,92,110,96,120,130,115], - 'err' => [1,0,0,2,1,0,1,3,2,2], - ], - ], 120); - Cache::put('events:recent', $this->safe(fn() => $this->recentAlerts(), []), 300); - } catch (\Throwable $e) { - // Last-resort catch: never allow the job to fail hard - Log::error('RunHealthChecks fatal', ['ex' => $e]); + $cards = config('woltguard.cards', []); + $svcRows = []; + foreach ($cards as $key => $card) { + $ok = false; + foreach ($card['sources'] as $src) { + if ($this->check($src)) { $ok = true; break; } + } + $svcRows[] = ['name' => $key, 'ok' => $ok]; // labels brauchst du im UI } + Cache::put('health:services', $svcRows, 60); + +// try { +// $services = [ +// $this->safe(fn() => $this->service('postfix'), ['name'=>'postfix']), +// $this->safe(fn() => $this->service('dovecot'), ['name'=>'dovecot']), +// $this->safe(fn() => $this->service('rspamd'), ['name'=>'rspamd']), +// $this->safe(fn() => $this->tcp('127.0.0.1', 6379), ['name'=>'redis']), +// $this->safe(fn() => $this->db(), ['name'=>'db']), +// $this->safe(fn() => $this->queueWorkers(), ['name'=>'queue']), +// $this->safe(fn() => $this->tcp('127.0.0.1', 8080), ['name'=>'reverb']), +// ]; +// +// $meta = [ +// 'app_version' => config('app.version', app()->version()), +// 'pending_migs' => $this->safe(fn() => $this->pendingMigrationsCount(), 0), +// 'cert_soon' => $this->safe(fn() => $this->certificatesDue(30), ['count'=>0,'nearest_days'=>null]), +// 'disk' => $this->safe(fn() => $this->diskUsage(), ['percent'=>null,'free_gb'=>null]), +// 'system' => $this->systemLoad(), +// 'updated_at' => now()->toIso8601String(), +// ]; +// +// Cache::put('health:services', array_values($services), 300); +// Cache::put('health:meta', $meta, 300); +// Cache::put('metrics:queues', [ +// 'outgoing' => 19, +// 'incoming' => 5, +// 'today_ok' => 834, +// 'today_err'=> 12, +// 'trend' => [ +// 'outgoing' => [2,1,0,4,3,5,4,0,0,0], // letzte 10 Zeitfenster +// 'incoming' => [1,0,0,1,0,2,1,0,0,0], +// 'ok' => [50,62,71,88,92,110,96,120,130,115], +// 'err' => [1,0,0,2,1,0,1,3,2,2], +// ], +// ], 120); +// Cache::put('events:recent', $this->safe(fn() => $this->recentAlerts(), []), 300); +// } catch (\Throwable $e) { +// // Last-resort catch: never allow the job to fail hard +// Log::error('RunHealthChecks fatal', ['ex' => $e]); +// } } /** Wraps a probe; logs and returns fallback on error */ diff --git a/app/Livewire/Ui/System/HealthCard.php b/app/Livewire/Ui/System/HealthCard.php index 0554f03..ab51d4d 100644 --- a/app/Livewire/Ui/System/HealthCard.php +++ b/app/Livewire/Ui/System/HealthCard.php @@ -4,6 +4,7 @@ namespace App\Livewire\Ui\System; use Carbon\Carbon; use Illuminate\Support\Facades\Cache; +use Livewire\Attributes\On; use Livewire\Component; class HealthCard extends Component @@ -23,6 +24,10 @@ class HealthCard extends Component public ?string $loadText = null; public array $loadDots = []; public ?string $uptimeText = null; + public ?int $uptimeDays = null; + public ?int $uptimeHours = null; + public ?string $uptimeDaysLabel = null; + public ?string $uptimeHoursLabel = null; public string $uptimeIcon = 'ph ph-clock'; public ?string $updatedAtHuman = null; @@ -44,6 +49,47 @@ class HealthCard extends Component $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'); @@ -85,10 +131,20 @@ class HealthCard extends Component } // 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); +// $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 @@ -173,6 +229,22 @@ class HealthCard extends Component 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'; diff --git a/app/Livewire/Ui/System/ServicesCard.php b/app/Livewire/Ui/System/ServicesCard.php index 6eedea1..76f7fab 100644 --- a/app/Livewire/Ui/System/ServicesCard.php +++ b/app/Livewire/Ui/System/ServicesCard.php @@ -1,99 +1,58 @@ [ - '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', - // mehrere mögliche Units + Socket/PID + TCP - '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', - ], - ], + // Badge (rechts) + public string $badgeText = ''; + public string $badgeClass = ''; + public string $badgeIcon = 'ph ph-check-circle'; - // 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'], - ], + // optionales Polling im Blade (du hast es dort referenziert) + public int $pollSeconds = 15; - // 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 (Units ODER laufende artisan-Prozesse) - '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'], - ], - ]; + /** 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 { @@ -102,48 +61,154 @@ class ServicesCard extends Component public function refresh(): void { - Cache::forget('health:services.v2'); + // nur unsere eigene Services-Card neu aufbauen (Cache vom Job lassen wir in Ruhe) $this->load(); } - public function load(): void - { - $data = Cache::remember('health:services.v2', 15, function () { - $out = []; - foreach ($this->cards as $key => $card) { - $ok = false; - foreach ($card['sources'] as $src) { - if ($this->check($src)) { - $ok = true; - break; - } - } - $out[$key] = ['label' => $card['label'], 'hint' => $card['hint'], 'ok' => $ok]; - } - return $out; - }); - - $this->servicesCompact = collect($data)->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(); - } - public function render() { return view('livewire.ui.system.services-card'); } - // ───────────── Probes ───────────── + 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 { @@ -176,10 +241,8 @@ class ServicesCard extends Component protected function probeSystemd(string $unit): bool { - // versuche /bin und /usr/bin $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); @@ -189,17 +252,12 @@ class ServicesCard extends Component 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; - } + if (is_resource($fp)) { fclose($fp); return true; } return false; - // Alternative: stream_socket_client("tcp://$host:$port", …) } protected function probeProcessRegex(string $regex): bool { - // /proc durchsuchen – leichtgewichtig und ohne ps-Depend $regex = '#' . $regex . '#i'; foreach (@scandir('/proc') ?: [] as $d) { if (!ctype_digit($d)) continue; @@ -213,92 +271,7 @@ class ServicesCard extends Component protected function probeDatabase(): bool { - try { - DB::connection()->getPdo(); - return true; - } catch (\Throwable) { - return false; - } + try { DB::connection()->getPdo(); return true; } + catch (\Throwable) { return false; } } } - -// -//namespace App\Livewire\Ui\System; -// -//use Carbon\Carbon; -//use Illuminate\Support\Facades\Cache; -//use Livewire\Component; -// -//class ServicesCard extends Component -//{ -// public array $services = []; -// public array $servicesCompact = []; -// -// // Mapping für schöne Labels/Hints -// protected array $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 -// 'mailwolt-queue' => ['label' => 'MailWolt Queue', 'hint' => 'Job Worker'], -// 'mailwolt-schedule'=> ['label' => 'MailWolt Schedule', 'hint' => 'Task Scheduler'], -// 'mailwolt-ws' => ['label' => 'MailWolt WebSocket','hint' => 'Echtzeit Updates'], -// -// // Sonstiges -// 'fail2ban' => ['label' => 'Fail2Ban', 'hint' => 'SSH / Mail Protection'], -// 'systemd-journald'=> ['label' => 'System Logs', 'hint' => 'Journal'], -// 'rsyslog' => ['label' => 'Rsyslog', 'hint' => 'Logging'], -// -// // WebSocket/TCP -// '127.0.0.1:8080' => ['label' => 'Reverb', 'hint' => 'WebSocket Server'], -// ]; -// -// public function mount(): void -// { -// $this->load(); -// } -// -// public function load(): void -// { -// $this->services = Cache::get('health:services', []); -// $meta = Cache::get('health:meta', []); -// $updated = $meta['updated_at'] ?? null; -// -// $existing = collect($this->services)->keyBy('name'); -// -// $this->servicesCompact = collect($this->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, -// 'dotClass' => $ok ? 'bg-emerald-400' : 'bg-rose-400', -// 'pillText' => $ok ? 'Aktiv' : '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(); -// } -// -// public function render() -// { -// return view('livewire.ui.system.services-card'); -// } -//} diff --git a/app/Livewire/Ui/System/UpdateCard.php b/app/Livewire/Ui/System/UpdateCard.php index 1de55a5..ee6f61f 100644 --- a/app/Livewire/Ui/System/UpdateCard.php +++ b/app/Livewire/Ui/System/UpdateCard.php @@ -1,6 +1,5 @@ current = $this->readCurrentVersion(); - $this->latest = Cache::get('mailwolt.update_available'); - $this->refreshLowLevelState(); - - $this->recompute(); // setzt hasUpdate + display* + ggf. message + $this->reloadVersionsAndStatus(); + $this->recompute(); if ($this->running) $this->state = 'running'; } @@ -46,7 +36,7 @@ class UpdateCard extends Component return view('livewire.ui.system.update-card'); } - /* =================== Aktionen =================== */ + /* ========== Aktionen ========== */ public function refreshState(): void { @@ -57,43 +47,37 @@ class UpdateCard extends Component try { Artisan::call('mailwolt:check-updates'); } catch (\Throwable $e) { - // weich fallen + // kein fataler Abbruch } $this->reloadVersionsAndStatus(); $this->recompute(); - $this->finishUiIfNoUpdate(); // beendet progress, wenn nichts mehr offen ist + $this->finishUiIfNoUpdate(); } public function runUpdate(): void { - // Hinweis sofort entfernen (Badge weg) Cache::forget('mailwolt.update_available'); Cache::put($this->cacheStartedAtKey, time(), now()->addHour()); - // Wrapper asynchron starten @shell_exec('nohup sudo -n /usr/local/sbin/mw-update >/dev/null 2>&1 &'); - // UI-Status $this->latest = null; $this->displayLatest = null; $this->hasUpdate = false; - $this->state = 'running'; $this->running = true; $this->message = 'Update läuft …'; - $this->messagePositive = null; } - /** Wird nur gepollt, solange $state==='running' (wire:poll) */ public function tick(): void { $this->refreshLowLevelState(); - // Failsafe $started = (int)Cache::get($this->cacheStartedAtKey, 0); if ($this->running && $started > 0 && (time() - $started) > $this->failsafeSeconds) { $this->running = false; + $this->rc ??= 0; } if (!$this->running) { @@ -101,15 +85,41 @@ class UpdateCard extends Component $this->reloadVersionsAndStatus(); $this->recompute(); $this->finishUiIfNoUpdate(); + + if ($this->rc === 0) { + $ver = $this->displayCurrent ?? 'aktuelle Version'; + + // ✅ zentraler Toastra-Toast + $this->dispatch('toast', + type: 'success', + title: 'Update erfolgreich', + text: "MailWolt wurde erfolgreich auf {$ver} aktualisiert. Die Seite wird in 5 Sekunden neu geladen …", + badge: 'System', + duration: 5000 + ); + + // nach 5 Sekunden Seite neu laden + $this->dispatch('reload-page', delay: 5000); + } elseif ($this->rc !== null) { + $this->dispatch('toast', + type: 'error', + title: 'Update fehlgeschlagen', + text: "Update ist mit Rückgabecode {$this->rc} fehlgeschlagen. Bitte Logs prüfen.", + badge: 'System', + duration: 0 + ); + } + + $this->state = 'idle'; } } - /* =================== Helpers =================== */ + /* ========== Helper ========== */ protected function reloadVersionsAndStatus(): void { $this->current = $this->readCurrentVersion(); - $this->latest = Cache::get('mailwolt.update_available'); + $this->latest = Cache::get('mailwolt.update_available'); $this->refreshLowLevelState(); } @@ -118,11 +128,8 @@ class UpdateCard extends Component if (!$this->hasUpdate) { $this->state = 'idle'; $cur = $this->displayCurrent ?? '–'; - // <<< Klammern-Fix: erst coalescen, dann in den String einsetzen $this->message = "Du bist auf dem neuesten Stand ({$cur})"; $this->messagePositive = true; - - // zur Sicherheit: Cache leeren Cache::forget('mailwolt.update_available'); } } @@ -136,21 +143,9 @@ class UpdateCard extends Component ? version_compare($latNorm, $curNorm, '>') : false; - // Anzeige immer mit v-Präfix + // Immer mit v-Präfix anzeigen $this->displayCurrent = $curNorm ? 'v' . $curNorm : null; - $this->displayLatest = ($this->hasUpdate && $latNorm) ? 'v' . $latNorm : null; - - // Initiale Message (nur wenn noch nicht gesetzt) - if ($this->message === null) { - if ($this->hasUpdate) { - $this->message = "Neue Version verfügbar: {$this->displayLatest}"; - $this->messagePositive = false; - } else { - $cur = $this->displayCurrent ?? '–'; - $this->message = "Du bist auf dem neuesten Stand ({$cur})"; - $this->messagePositive = true; - } - } + $this->displayLatest = ($this->hasUpdate && $latNorm) ? 'v' . $latNorm : null; } protected function refreshLowLevelState(): void @@ -164,7 +159,6 @@ class UpdateCard extends Component protected function readCurrentVersion(): ?string { - // bevorzugt build.info $build = @file_get_contents('/etc/mailwolt/build.info'); if ($build) { foreach (preg_split('/\R+/', $build) as $line) { @@ -182,12 +176,198 @@ class UpdateCard extends Component { if ($v === null) return null; $v = trim($v); - // führendes v/V + whitespace entfernen - $v = ltrim($v, "vV \t\n\r\0\x0B"); - return $v === '' ? null : $v; + return ltrim($v, "vV \t\n\r\0\x0B") ?: null; } } + +//namespace App\Livewire\Ui\System; +// +//use Illuminate\Support\Facades\Artisan; +//use Illuminate\Support\Facades\Cache; +//use Livewire\Component; +// +//class UpdateCard extends Component +//{ +// /** Rohwerte (so wie gelesen/aus Cache) */ +// public ?string $current = null; // z.B. "v1.0.20" oder "1.0.20" +// public ?string $latest = null; // z.B. "1.0.20" oder "v1.0.20" +// +// /** Anzeige (immer mit v-Präfix) */ +// public ?string $displayCurrent = null; // z.B. "v1.0.20" +// public ?string $displayLatest = null; // nur gesetzt, wenn wirklich neuer +// +// /** Status/UX */ +// public bool $hasUpdate = false; +// public string $state = 'idle'; // idle|running +// public ?string $message = null; +// public ?bool $messagePositive = null; +// +// /** low-level / Wrapper */ +// public bool $running = false; +// public ?int $rc = null; +// +// /** intern */ +// protected string $cacheStartedAtKey = 'mw.update.started_at'; +// protected int $failsafeSeconds = 20 * 60; // 20 Min +// +// public function mount(): void +// { +// $this->current = $this->readCurrentVersion(); +// $this->latest = Cache::get('mailwolt.update_available'); +// $this->refreshLowLevelState(); +// +// $this->recompute(); // setzt hasUpdate + display* + ggf. message +// if ($this->running) $this->state = 'running'; +// } +// +// public function render() +// { +// return view('livewire.ui.system.update-card'); +// } +// +// /* =================== Aktionen =================== */ +// +// public function refreshState(): void +// { +// $this->state = 'running'; +// $this->message = 'Prüfe auf Updates …'; +// $this->messagePositive = null; +// +// try { +// Artisan::call('mailwolt:check-updates'); +// } catch (\Throwable $e) { +// // weich fallen +// } +// +// $this->reloadVersionsAndStatus(); +// $this->recompute(); +// $this->finishUiIfNoUpdate(); // beendet progress, wenn nichts mehr offen ist +// } +// +// public function runUpdate(): void +// { +// // Hinweis sofort entfernen (Badge weg) +// Cache::forget('mailwolt.update_available'); +// Cache::put($this->cacheStartedAtKey, time(), now()->addHour()); +// +// // Wrapper asynchron starten +// @shell_exec('nohup sudo -n /usr/local/sbin/mw-update >/dev/null 2>&1 &'); +// +// // UI-Status +// $this->latest = null; +// $this->displayLatest = null; +// $this->hasUpdate = false; +// +// $this->state = 'running'; +// $this->running = true; +// $this->message = 'Update läuft …'; +// $this->messagePositive = null; +// } +// +// /** Wird nur gepollt, solange $state==='running' (wire:poll) */ +// public function tick(): void +// { +// $this->refreshLowLevelState(); +// +// // Failsafe +// $started = (int)Cache::get($this->cacheStartedAtKey, 0); +// if ($this->running && $started > 0 && (time() - $started) > $this->failsafeSeconds) { +// $this->running = false; +// } +// +// if (!$this->running) { +// Cache::forget($this->cacheStartedAtKey); +// $this->reloadVersionsAndStatus(); +// $this->recompute(); +// $this->finishUiIfNoUpdate(); +// } +// } +// +// /* =================== Helpers =================== */ +// +// protected function reloadVersionsAndStatus(): void +// { +// $this->current = $this->readCurrentVersion(); +// $this->latest = Cache::get('mailwolt.update_available'); +// $this->refreshLowLevelState(); +// } +// +// protected function finishUiIfNoUpdate(): void +// { +// if (!$this->hasUpdate) { +// $this->state = 'idle'; +// $cur = $this->displayCurrent ?? '–'; +// // <<< Klammern-Fix: erst coalescen, dann in den String einsetzen +// $this->message = "Du bist auf dem neuesten Stand ({$cur})"; +// $this->messagePositive = true; +// +// // zur Sicherheit: Cache leeren +// Cache::forget('mailwolt.update_available'); +// } +// } +// +// protected function recompute(): void +// { +// $curNorm = $this->normalizeVersion($this->current); +// $latNorm = $this->normalizeVersion($this->latest); +// +// $this->hasUpdate = ($curNorm && $latNorm) +// ? version_compare($latNorm, $curNorm, '>') +// : false; +// +// // Anzeige immer mit v-Präfix +// $this->displayCurrent = $curNorm ? 'v' . $curNorm : null; +// $this->displayLatest = ($this->hasUpdate && $latNorm) ? 'v' . $latNorm : null; +// +// // Initiale Message (nur wenn noch nicht gesetzt) +// if ($this->message === null) { +// if ($this->hasUpdate) { +// $this->message = "Neue Version verfügbar: {$this->displayLatest}"; +// $this->messagePositive = false; +// } else { +// $cur = $this->displayCurrent ?? '–'; +// $this->message = "Du bist auf dem neuesten Stand ({$cur})"; +// $this->messagePositive = true; +// } +// } +// } +// +// protected function refreshLowLevelState(): void +// { +// $state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: ''); +// $this->running = ($state === 'running'); +// +// $rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: ''); +// $this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null; +// } +// +// protected function readCurrentVersion(): ?string +// { +// // bevorzugt build.info +// $build = @file_get_contents('/etc/mailwolt/build.info'); +// if ($build) { +// foreach (preg_split('/\R+/', $build) as $line) { +// if (str_starts_with($line, 'version=')) { +// $v = trim(substr($line, 8)); +// return $v !== '' ? $v : null; +// } +// } +// } +// $v = config('app.version'); +// return $v !== '' ? $v : null; +// } +// +// protected function normalizeVersion(?string $v): ?string +// { +// if ($v === null) return null; +// $v = trim($v); +// // führendes v/V + whitespace entfernen +// $v = ltrim($v, "vV \t\n\r\0\x0B"); +// return $v === '' ? null : $v; +// } +//} + //namespace App\Livewire\Ui\System; // //use Illuminate\Support\Facades\Artisan; diff --git a/app/Livewire/Ui/System/UpdateManager.php b/app/Livewire/Ui/System/UpdateManager.php deleted file mode 100644 index 2149365..0000000 --- a/app/Livewire/Ui/System/UpdateManager.php +++ /dev/null @@ -1,72 +0,0 @@ -refreshState(); - // Optional: latest Tag/Version aus Cache anzeigen - $this->latest = cache('mailwolt.update_available'); - } - - public function refreshState(): void - { - $state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: ''); - $this->running = ($state === 'running'); - - $rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: ''); - $this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null; - - // letzte 200 Zeilen Log - $this->log = @shell_exec('tail -n 200 /var/log/mailwolt-update.log 2>/dev/null') ?? ''; - } - - public function runUpdate(): void - { - // Hinweis „Update verfügbar“ ausblenden - cache()->forget('mailwolt.update_available'); - - // Update im Hintergrund starten - @shell_exec('nohup sudo -n /usr/local/sbin/mw-update >/dev/null 2>&1 &'); - - $this->running = true; - $this->dispatch('toast', - type: 'info', - badge: 'Update', - title: 'Update gestartet', - text: 'Das System wird aktualisiert …', - duration: 6000 - ); - } - - public function poll(): void - { - $before = $this->running; - $this->refreshState(); - - if ($before && !$this->running && $this->rc !== null) { - $ok = ($this->rc === 0); - $this->dispatch('toast', - type: $ok ? 'done' : 'warn', - badge: 'Update', - title: $ok ? 'Update abgeschlossen' : 'Update fehlgeschlagen', - text: $ok ? 'Die neue Version ist aktiv.' : "Fehlercode: {$this->rc}. Log prüfen.", - duration: 8000 - ); - } - } - - public function render() - { - return view('livewire.ui.system.update-manager'); - } -} diff --git a/app/Support/WoltGuard/Probes.php b/app/Support/WoltGuard/Probes.php new file mode 100644 index 0000000..5f6fdfc --- /dev/null +++ b/app/Support/WoltGuard/Probes.php @@ -0,0 +1,63 @@ +probeSystemd(substr($source, 8)); + if (str_starts_with($source, 'tcp:')) return $this->probeTcp(...$this->splitTcp(substr($source, 4))); + if (str_starts_with($source, 'socket:')) return @file_exists(substr($source, 7)); + if (str_starts_with($source, 'pid:')) return $this->probePid(substr($source, 4)); + if (str_starts_with($source, 'proc:')) return $this->probeProcessRegex(substr($source, 5)); + if ($source === 'db') return $this->probeDatabase(); + return false; + } + + protected function splitTcp(string $s): array { [$h,$p] = explode(':', $s, 2); return [$h,(int)$p]; } + + 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 probePid(string $path): bool + { + if (!@is_file($path)) return false; + $pid = (int) trim(@file_get_contents($path) ?: ''); + return $pid > 1 && @posix_kill($pid, 0); + } + + 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) 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; } + } +} diff --git a/badgeClass b/badgeClass new file mode 100644 index 0000000..e69de29 diff --git a/badgeIcon b/badgeIcon new file mode 100644 index 0000000..e69de29 diff --git a/badgeText b/badgeText new file mode 100644 index 0000000..e69de29 diff --git a/bootstrap/app.php b/bootstrap/app.php index 444441e..90e6e8b 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -8,6 +8,7 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__ . '/../routes/web.php', api: __DIR__ . '/../routes/api.php', + channels: __DIR__ . '/../routes/channels.php', commands: __DIR__ . '/../routes/console.php', health: '/up', ) diff --git a/config/woltguard.php b/config/woltguard.php new file mode 100644 index 0000000..b4f6eb5 --- /dev/null +++ b/config/woltguard.php @@ -0,0 +1,76 @@ + [ + // 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 + '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'], + ], + ], +]; diff --git a/guardOk b/guardOk new file mode 100644 index 0000000..e69de29 diff --git a/okCount b/okCount new file mode 100644 index 0000000..e69de29 diff --git a/resources/js/app.js b/resources/js/app.js index 503b3b5..880a38d 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -12,4 +12,4 @@ import './components/sidebar.js'; import './plugins/GlassToastra/toastra.glass.js' import './plugins/GlassToastra/livewire-adapter'; -// import './utils/events.js'; +import './utils/events.js'; diff --git a/resources/js/plugins/GlassToastra/livewire-adapter.js b/resources/js/plugins/GlassToastra/livewire-adapter.js index a85ea1c..bcb15b6 100644 --- a/resources/js/plugins/GlassToastra/livewire-adapter.js +++ b/resources/js/plugins/GlassToastra/livewire-adapter.js @@ -5,6 +5,7 @@ document.addEventListener('livewire:init', () => { window.addEventListener('toast', (e) => showToast(e?.detail || {})); window.addEventListener('toast.update', (e) => showToast(e?.detail || {})); // gleiche id => ersetzt Karte window.addEventListener('toast.clear', (e) => window.toastraGlass?.clear(e?.detail?.position)); + window.addEventListener('toast.reload', (e) => setTimeout(() => window.location.reload(), e.delay || 0)); }); // optional global diff --git a/resources/js/utils/events.js b/resources/js/utils/events.js index 9c0f216..5f382e4 100644 --- a/resources/js/utils/events.js +++ b/resources/js/utils/events.js @@ -1,276 +1,293 @@ -// import { showToast } from '../ui/toast.js' +Livewire.on('reload-page', e => setTimeout(() => window.location.reload(), e.delay || 0)); -// — Livewire-Hooks (global) +// // import { showToast } from '../ui/toast.js' +// +// // — Livewire-Hooks (global) +// // document.addEventListener('livewire:init', () => { +// // if (window.Livewire?.on) { +// // window.Livewire.on('toast', (payload = {}) => showToast(payload)) +// // } +// // }) +// // document.addEventListener('livewire:init', () => { -// if (window.Livewire?.on) { -// window.Livewire.on('toast', (payload = {}) => showToast(payload)) +// // Neu: Livewire v3 Browser-Events +// window.addEventListener('toast', (e) => { +// const d = e?.detail || {}; +// showToastGlass(d); +// }); +// +// // Optional: Update/Dismiss/Clear per Event +// window.addEventListener('toast.update', (e) => { +// const d = e?.detail || {}; +// if (d.id) window.toastraGlass?.update(d.id, d); +// }); +// window.addEventListener('toast.clear', (e) => { +// window.toastraGlass?.clear(e?.detail?.position); +// }); +// }); +// +// +// // document.addEventListener('livewire:init', (e) => { +// // console.log(e) +// // +// // window.addEventListener('toast-reload', (e) => { +// // console.log(e) +// // setTimeout(() => window.location.reload(), e.delay || 0) +// // }); +// // }); +// +// // Adapter: normalisiert Payload und ruft toastraGlass +// function showToastGlass({ +// id, +// type, state, // "success" | "warning" | "error" ODER "done" | "failed" | "running" +// text, message, title, // Textquellen +// badge, domain, +// position = 'bottom-right', +// duration = 0, // 0 = stehen lassen; bei done/failed auto auf 6000ms +// } = {}) { +// // Map: type -> state +// const t = (type || state || 'done').toLowerCase(); +// const map = {success: 'done', ok: 'done', error: 'failed', danger: 'failed', info: 'queued', warning: 'queued'}; +// const st = ['done', 'failed', 'running', 'queued'].includes(t) ? t : (map[t] || 'queued'); +// +// const msg = message || text || title || ''; +// const _id = id || ('toast-' + Date.now()); +// +// if (window.toastraGlass?.show) { +// window.toastraGlass.show({ +// id: _id, +// state: st, // queued|running|done|failed → färbt Badge/Icon +// badge, // z.B. "DNS", "Signup" +// domain, // optional: kleine Überschrift rechts +// message: msg, +// position, +// duration, // 0 = stehen lassen; sonst ms +// }); +// } else if (window.toastr) { +// // Fallback: alte toastr API +// const level = (type || (st === 'failed' ? 'error' : st === 'done' ? 'success' : 'info')); +// window.toastr[level](msg, badge || domain || ''); +// } else { +// // Minimal-Fallback +// const box = document.createElement('div'); +// box.className = 'fixed top-4 right-4 z-[9999] rounded-xl bg-emerald-500/90 text-white px-4 py-3 backdrop-blur shadow-lg border border-white/10'; +// box.textContent = msg || 'OK'; +// document.body.appendChild(box); +// setTimeout(() => box.remove(), 3500); // } -// }) - -document.addEventListener('livewire:init', () => { - // Neu: Livewire v3 Browser-Events - window.addEventListener('toast', (e) => { - const d = e?.detail || {}; - showToastGlass(d); - }); - - // Optional: Update/Dismiss/Clear per Event - window.addEventListener('toast.update', (e) => { - const d = e?.detail || {}; - if (d.id) window.toastraGlass?.update(d.id, d); - }); - window.addEventListener('toast.clear', (e) => { - window.toastraGlass?.clear(e?.detail?.position); - }); -}); - -// Adapter: normalisiert Payload und ruft toastraGlass -function showToastGlass({ - id, - type, state, // "success" | "warning" | "error" ODER "done" | "failed" | "running" - text, message, title, // Textquellen - badge, domain, - position = 'bottom-right', - duration = 0, // 0 = stehen lassen; bei done/failed auto auf 6000ms - } = {}) { - // Map: type -> state - const t = (type || state || 'done').toLowerCase(); - const map = { success: 'done', ok: 'done', error: 'failed', danger: 'failed', info: 'queued', warning: 'queued' }; - const st = ['done','failed','running','queued'].includes(t) ? t : (map[t] || 'queued'); - - const msg = message || text || title || ''; - const _id = id || ('toast-' + Date.now()); - - if (window.toastraGlass?.show) { - window.toastraGlass.show({ - id: _id, - state: st, // queued|running|done|failed → färbt Badge/Icon - badge, // z.B. "DNS", "Signup" - domain, // optional: kleine Überschrift rechts - message: msg, - position, - duration, // 0 = stehen lassen; sonst ms - }); - } else if (window.toastr) { - // Fallback: alte toastr API - const level = (type || (st === 'failed' ? 'error' : st === 'done' ? 'success' : 'info')); - window.toastr[level](msg, badge || domain || ''); - } else { - // Minimal-Fallback - const box = document.createElement('div'); - box.className = 'fixed top-4 right-4 z-[9999] rounded-xl bg-emerald-500/90 text-white px-4 py-3 backdrop-blur shadow-lg border border-white/10'; - box.textContent = msg || 'OK'; - document.body.appendChild(box); - setTimeout(() => box.remove(), 3500); - } - - return _id; -} - -// — Session-Flash vom Backend (einmal pro Page-Load) -function bootstrapFlashFromLayout() { - const el = document.getElementById('__flash') - if (!el) return - try { - const data = JSON.parse(el.textContent || '{}') - if (data?.toast) showToast(data.toast) - } catch {} -} -document.addEventListener('DOMContentLoaded', bootstrapFlashFromLayout) - -// — Optional: Echo/WebSocket-Kanal für „Push-Toasts“ -function setupEchoToasts() { - if (!window.Echo) return - // userId wird im Layout in das JSON injiziert (siehe unten) - const el = document.getElementById('__flash') - let uid = null - try { uid = JSON.parse(el?.textContent || '{}')?.userId ?? null } catch {} - if (!uid) return - - window.Echo.private(`users.${uid}`) - .listen('.ToastPushed', (e) => { - // e: { type, text, title } - showToast(e) - }) -} -document.addEventListener('DOMContentLoaded', setupEchoToasts) - -// — Optional: global machen, falls du manuell aus JS/Blade rufen willst -// window.showToast = showToast - - - -// document.addEventListener('livewire:init', () => { -// Livewire.on('toastra:show', (payload) => { -// // optionaler "mute" pro Nutzer lokal: -// if (localStorage.getItem('toast:hide:' + payload.id)) return; // -// const id = window.toastraGlass.show({ -// id: payload.id, -// state: payload.state, // queued|running|done|failed -// badge: payload.badge, -// domain: payload.domain, -// message: payload.message, -// position: payload.position || 'bottom-center', -// duration: payload.duration ?? 0, -// close: payload.close !== false, -// }); +// return _id; +// } // -// // Wenn der User X klickt, markiere lokal als verborgen: -// window.addEventListener('toastra:closed:' + id, () => { -// localStorage.setItem('toast:hide:' + id, '1'); -// }, { once: true }); -// }); -// }); -// document.addEventListener('livewire:init', () => { -// Livewire.on('notify', (payload) => { -// const o = Array.isArray(payload) ? payload[0] : payload; -// window.toastraGlass?.show({ -// id: o.id, state: o.state, badge: o.badge, domain: o.domain, -// message: o.message, position: o.position || 'bottom-right', -// duration: Number(o.duration ?? 0), close: o.close !== false, -// finalNote: (o.state === 'done' || o.state === 'failed') -// ? 'Diese Meldung verschwindet automatisch.' : '' -// }); -// }); -// }); -// document.addEventListener('livewire:init', () => { -// Livewire.on('notify', (payload) => { -// // Livewire liefert das Event als Array mit einem Objekt -// const o = Array.isArray(payload) ? payload[0] : payload; +// // — Session-Flash vom Backend (einmal pro Page-Load) +// function bootstrapFlashFromLayout() { +// const el = document.getElementById('__flash') +// if (!el) return +// try { +// const data = JSON.parse(el.textContent || '{}') +// if (data?.toast) showToast(data.toast) +// } catch { +// } +// } // -// // Ein Aufruf reicht: gleiche id => ersetzt bestehenden Toast -// window.toastraGlass?.show({ -// id: o.id, -// state: o.state, // queued|running|done|failed -// badge: o.badge, // z.B. CERTBOT -// domain: o.domain, // z.B. mail.example.com -// message: o.message, -// position: o.position || 'bottom-right', -// duration: Number(o.duration ?? 0), -// close: o.close !== false, -// // optional kannst du finalNote je nach state setzen: -// finalNote: (o.state === 'done' || o.state === 'failed') -// ? 'Diese Meldung verschwindet automatisch.' -// : '' -// }); -// }); -// }); - -// document.addEventListener('livewire:init', () => { -// Livewire.on('notify', (payload) => { -// const e = Array.isArray(payload) ? payload[0] : payload; +// document.addEventListener('DOMContentLoaded', bootstrapFlashFromLayout) // -// // e.state: 'queued'|'running'|'done'|'failed' -// window.toastraGlass.show({ -// id: e.id || ('toast-'+Date.now()), -// state: e.state || 'queued', -// badge: e.badge || (e.type ? String(e.type).toUpperCase() : null), -// domain: e.domain || '', -// message: e.message ?? e.text ?? '', -// position: e.position || 'bottom-right', -// duration: typeof e.duration === 'number' ? e.duration : (['done','failed'].includes(e.state) ? 6000 : 0), -// close: e.close ?? true, -// finalNote: (['done','failed'].includes(e.state) ? 'Diese Meldung verschwindet nach Aktualisierung automatisch.' : '') -// }); -// }); -// }); - - -// document.addEventListener('livewire:init', () => { -// Livewire.on('notify', (event) => { -// const p = Array.isArray(event) ? event[0] : event; +// // — Optional: Echo/WebSocket-Kanal für „Push-Toasts“ +// function setupEchoToasts() { +// if (!window.Echo) return +// // userId wird im Layout in das JSON injiziert (siehe unten) +// const el = document.getElementById('__flash') +// let uid = null +// try { +// uid = JSON.parse(el?.textContent || '{}')?.userId ?? null +// } catch { +// } +// if (!uid) return // -// window.toastraGlass.show({ -// id: p.id || ('toast-'+Date.now()), -// state: (p.type || 'info'), // info|update|success|warning|error -// title: p.title || '', -// domain: p.domain || '', -// message: p.message ?? p.text ?? '', -// badge: p.badge || null, -// duration: (typeof p.duration === 'number' ? p.duration : 0), // 0 = bleibt -// position: p.position || 'bottom-center', // top-left|top-center|top-right|bottom-* -// close: (p.close ?? true), -// }); -// }); -// }); - -// document.addEventListener('livewire:init', () => { -// // 1) Events aus PHP/Livewire-Komponenten -// Livewire.on('notify', (payload) => { -// const e = payload[0] || payload; -// toastra.notify({ -// id: e.id, -// badge: e.badge || null, -// replace: true, -// title: e.title, -// text: e.text, -// subtitle: e.subtitle || null, -// type: e.type, -// // classname: e.classname, -// duration: e.duration ?? 0, -// close: e.close ?? true, -// icon: e.icon || null, -// }); -// }); +// window.Echo.private(`users.${uid}`) +// .listen('.ToastPushed', (e) => { +// // e: { type, text, title } +// showToast(e) +// }) +// } // -// document.addEventListener('notify', (e) => { -// console.log(e.detail); -// }); -// // 2) Reine Browser-Events (für Konsole/JS) -// // window.addEventListener('notify', (ev) => { -// // const e = ev.detail || {}; -// // toastra.notify({ -// // id: e.id, replace: true, -// // title: e.title, text: e.message, -// // type: e.type, duration: e.duration ?? 0, -// // close: e.close ?? true, icon: e.icon || null, -// // }); -// // }); -// }); +// document.addEventListener('DOMContentLoaded', setupEchoToasts) // -// // document.addEventListener('notify', (e) => { -// // const d = e.detail; -// // toastra.notify({ -// // id: d.id, -// // title: d.title, -// // text: d.text || d.message || '', // fallback -// // type: d.type, -// // duration: d.duration ?? 0, -// // close: d.close ?? true, +// // — Optional: global machen, falls du manuell aus JS/Blade rufen willst +// // window.showToast = showToast +// +// +// // document.addEventListener('livewire:init', () => { +// // Livewire.on('toastra:show', (payload) => { +// // // optionaler "mute" pro Nutzer lokal: +// // if (localStorage.getItem('toast:hide:' + payload.id)) return; +// // +// // const id = window.toastraGlass.show({ +// // id: payload.id, +// // state: payload.state, // queued|running|done|failed +// // badge: payload.badge, +// // domain: payload.domain, +// // message: payload.message, +// // position: payload.position || 'bottom-center', +// // duration: payload.duration ?? 0, +// // close: payload.close !== false, +// // }); +// // +// // // Wenn der User X klickt, markiere lokal als verborgen: +// // window.addEventListener('toastra:closed:' + id, () => { +// // localStorage.setItem('toast:hide:' + id, '1'); +// // }, { once: true }); +// // }); +// // }); +// // document.addEventListener('livewire:init', () => { +// // Livewire.on('notify', (payload) => { +// // const o = Array.isArray(payload) ? payload[0] : payload; +// // window.toastraGlass?.show({ +// // id: o.id, state: o.state, badge: o.badge, domain: o.domain, +// // message: o.message, position: o.position || 'bottom-right', +// // duration: Number(o.duration ?? 0), close: o.close !== false, +// // finalNote: (o.state === 'done' || o.state === 'failed') +// // ? 'Diese Meldung verschwindet automatisch.' : '' +// // }); +// // }); +// // }); +// // document.addEventListener('livewire:init', () => { +// // Livewire.on('notify', (payload) => { +// // // Livewire liefert das Event als Array mit einem Objekt +// // const o = Array.isArray(payload) ? payload[0] : payload; +// // +// // // Ein Aufruf reicht: gleiche id => ersetzt bestehenden Toast +// // window.toastraGlass?.show({ +// // id: o.id, +// // state: o.state, // queued|running|done|failed +// // badge: o.badge, // z.B. CERTBOT +// // domain: o.domain, // z.B. mail.example.com +// // message: o.message, +// // position: o.position || 'bottom-right', +// // duration: Number(o.duration ?? 0), +// // close: o.close !== false, +// // // optional kannst du finalNote je nach state setzen: +// // finalNote: (o.state === 'done' || o.state === 'failed') +// // ? 'Diese Meldung verschwindet automatisch.' +// // : '' +// // }); // // }); // // }); // // // document.addEventListener('livewire:init', () => { -// // // Livewire.on('notify', (event) => { -// // // toastra.notify({ -// // // title: event[0].title, -// // // text: event[0].message, -// // // type: event[0].type, -// // // duration: event[0].duration, -// // // close: event[0].close -// // // }); -// // // }); +// // Livewire.on('notify', (payload) => { +// // const e = Array.isArray(payload) ? payload[0] : payload; // // +// // // e.state: 'queued'|'running'|'done'|'failed' +// // window.toastraGlass.show({ +// // id: e.id || ('toast-'+Date.now()), +// // state: e.state || 'queued', +// // badge: e.badge || (e.type ? String(e.type).toUpperCase() : null), +// // domain: e.domain || '', +// // message: e.message ?? e.text ?? '', +// // position: e.position || 'bottom-right', +// // duration: typeof e.duration === 'number' ? e.duration : (['done','failed'].includes(e.state) ? 6000 : 0), +// // close: e.close ?? true, +// // finalNote: (['done','failed'].includes(e.state) ? 'Diese Meldung verschwindet nach Aktualisierung automatisch.' : '') +// // }); +// // }); +// // }); +// +// +// // document.addEventListener('livewire:init', () => { +// // Livewire.on('notify', (event) => { +// // const p = Array.isArray(event) ? event[0] : event; +// // +// // window.toastraGlass.show({ +// // id: p.id || ('toast-'+Date.now()), +// // state: (p.type || 'info'), // info|update|success|warning|error +// // title: p.title || '', +// // domain: p.domain || '', +// // message: p.message ?? p.text ?? '', +// // badge: p.badge || null, +// // duration: (typeof p.duration === 'number' ? p.duration : 0), // 0 = bleibt +// // position: p.position || 'bottom-center', // top-left|top-center|top-right|bottom-* +// // close: (p.close ?? true), +// // }); +// // }); +// // }); +// +// // document.addEventListener('livewire:init', () => { +// // // 1) Events aus PHP/Livewire-Komponenten // // Livewire.on('notify', (payload) => { // // const e = payload[0] || payload; // // toastra.notify({ // // id: e.id, +// // badge: e.badge || null, // // replace: true, // // title: e.title, -// // text: e.message, +// // text: e.text, +// // subtitle: e.subtitle || null, // // type: e.type, +// // // classname: e.classname, // // duration: e.duration ?? 0, // // close: e.close ?? true, -// // icon: e.icon || null +// // icon: e.icon || null, // // }); // // }); // // -// // Livewire.on('notify-replace', (event) => { -// // const opts = event[0] || {}; -// // const wrap = document.getElementById('notification'); -// // if (wrap) wrap.innerHTML = ''; -// // toastra.notify(opts); +// // document.addEventListener('notify', (e) => { +// // console.log(e.detail); // // }); -// // +// // // 2) Reine Browser-Events (für Konsole/JS) +// // // window.addEventListener('notify', (ev) => { +// // // const e = ev.detail || {}; +// // // toastra.notify({ +// // // id: e.id, replace: true, +// // // title: e.title, text: e.message, +// // // type: e.type, duration: e.duration ?? 0, +// // // close: e.close ?? true, icon: e.icon || null, +// // // }); +// // // }); // // }); +// // +// // // document.addEventListener('notify', (e) => { +// // // const d = e.detail; +// // // toastra.notify({ +// // // id: d.id, +// // // title: d.title, +// // // text: d.text || d.message || '', // fallback +// // // type: d.type, +// // // duration: d.duration ?? 0, +// // // close: d.close ?? true, +// // // }); +// // // }); +// // +// // // document.addEventListener('livewire:init', () => { +// // // // Livewire.on('notify', (event) => { +// // // // toastra.notify({ +// // // // title: event[0].title, +// // // // text: event[0].message, +// // // // type: event[0].type, +// // // // duration: event[0].duration, +// // // // close: event[0].close +// // // // }); +// // // // }); +// // // +// // // Livewire.on('notify', (payload) => { +// // // const e = payload[0] || payload; +// // // toastra.notify({ +// // // id: e.id, +// // // replace: true, +// // // title: e.title, +// // // text: e.message, +// // // type: e.type, +// // // duration: e.duration ?? 0, +// // // close: e.close ?? true, +// // // icon: e.icon || null +// // // }); +// // // }); +// // // +// // // Livewire.on('notify-replace', (event) => { +// // // const opts = event[0] || {}; +// // // const wrap = document.getElementById('notification'); +// // // if (wrap) wrap.innerHTML = ''; +// // // toastra.notify(opts); +// // // }); +// // // +// // // }); diff --git a/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php b/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php index 3b0f77e..8515255 100644 --- a/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php +++ b/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php @@ -112,11 +112,42 @@ @push('modal.footer')
{{ $log }}
- @endif
-