Fix: Mailbox Stats über Dovecot mit config/mailpool.php

main v1.0.25
boban 2025-10-24 23:25:14 +02:00
parent 812f91202f
commit e67c8613b3
38 changed files with 1141 additions and 834 deletions

0
, Normal file
View File

0
0 Normal file
View File

0
[hint], Normal file
View File

0
[label], Normal file
View File

View File

@ -0,0 +1,99 @@
<?php
namespace App\Console\Commands;
use App\Events\HealthUpdated;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
class CollectHealth extends Command
{
protected $signature = 'health:collect';
protected $description = 'Sammelt Systemmetriken und broadcastet sie';
public function handle(): int
{
$system = [
// CPU-Last (1/5/15) aus PHP
'cpu_load_1' => $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;
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
class CertPing implements ShouldBroadcastNow
{
public function __construct(public string $message) {}
public function broadcastOn(): Channel
{ return new Channel('system.tasks'); }
public function broadcastAs(): string
{ return 'cert.ping'; }
public function broadcastWith(): array
{ return ['message' => $this->message]; }
}

View File

@ -0,0 +1,27 @@
<?php
// App/Events/HealthUpdated.php
namespace App\Events;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class HealthUpdated implements ShouldBroadcastNow
{
use Dispatchable, SerializesModels;
public function __construct(public array $meta) {}
public function broadcastOn(): PrivateChannel
{
return new PrivateChannel('health');
}
// Wichtig: garantiert, dass "data" ein Objekt mit "meta" ist (kein JSON-String)
public function broadcastWith(): array
{
return ['meta' => $this->meta];
}
}

View File

@ -1,36 +0,0 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class Test
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('channel-name'),
];
}
}

View File

@ -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 */

View File

@ -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';

View File

@ -1,99 +1,58 @@
<?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;
/** 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',
// 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');
// }
//}

View File

@ -1,6 +1,5 @@
<?php
namespace App\Livewire\Ui\System;
use Illuminate\Support\Facades\Artisan;
@ -9,35 +8,26 @@ 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"
public ?string $current = null;
public ?string $latest = null;
public ?string $displayCurrent = null;
public ?string $displayLatest = null;
/** 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 bool $hasUpdate = false;
public string $state = 'idle';
public ?string $message = null;
public ?bool $messagePositive = 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
protected int $failsafeSeconds = 20 * 60; // 20 Minuten
public function mount(): void
{
$this->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;

View File

@ -1,72 +0,0 @@
<?php
namespace App\Livewire\Ui\System;
use Livewire\Component;
class UpdateManager extends Component
{
public bool $running = false;
public string $log = '';
public ?int $rc = null;
public ?string $latest = null; // optional: von Cache o.ä.
public function mount(): void
{
$this->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');
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Support\WoltGuard;
use Illuminate\Support\Facades\DB;
trait 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:')) 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; }
}
}

0
badgeClass Normal file
View File

0
badgeIcon Normal file
View File

0
badgeText Normal file
View File

View File

@ -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',
)

76
config/woltguard.php Normal file
View File

@ -0,0 +1,76 @@
<?php
return [
'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
'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'],
],
],
];

0
guardOk Normal file
View File

0
okCount Normal file
View File

View File

@ -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';

View File

@ -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

View File

@ -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);
// // // });
// // //
// // // });

View File

@ -112,11 +112,42 @@
@push('modal.footer')
<div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">
<div class="flex justify-end">
<button wire:click="$dispatch('closeModal')"
class="px-4 py-2 rounded-lg text-sm bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 hover:bg-emerald-500/30">
<i class="ph ph-check"></i> Fertig
</button>
<div class="flex flex-wrap items-center gap-4 text-[12px] text-slate-300">
<span class="inline-flex items-center gap-1">
<i class="ph ph-check-circle text-emerald-300"></i> vorhanden
</span>
<span class="inline-flex items-center gap-1">
<i class="ph ph-warning text-amber-300"></i> abweichend
</span>
<span class="inline-flex items-center gap-1">
<i class="ph ph-x-circle text-rose-300"></i> fehlt
</span>
<span class="ml-auto">
<div class="flex items-center gap-2">
<button wire:click="$dispatch('domain:check-dns')" wire:loading.attr="disabled"
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-[12px]
bg-white/5 border border-white/10 hover:bg-white/10">
<i class="ph ph-magnifying-glass"></i>
<span wire:loading.remove>DNS prüfen</span>
<span wire:loading>prüfe…</span>
</button>
<button wire:click="$dispatch('closeModal')"
class="px-3 py-1.5 rounded-lg text-sm bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 hover:bg-emerald-500/30">
<i class="ph ph-check"></i> Fertig
</button>
</div>
</span>
</div>
</div>
@endpush
{{--@push('modal.footer')--}}
{{-- <div class="px-5 py-3 border-t border-white/10 backdrop-blur rounded-b-2xl">--}}
{{-- <div class="flex justify-end">--}}
{{-- <button wire:click="$dispatch('closeModal')"--}}
{{-- class="px-4 py-2 rounded-lg text-sm bg-emerald-500/20 text-emerald-300 border border-emerald-400/30 hover:bg-emerald-500/30">--}}
{{-- <i class="ph ph-check"></i> Fertig--}}
{{-- </button>--}}
{{-- </div>--}}
{{-- </div>--}}
{{--@endpush--}}

View File

@ -1,4 +1,4 @@
<div wire:poll.30s="refresh" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
<div class="flex items-center justify-between mb-3">
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
<i class="ph ph-shield-checkered text-white/70 text-[13px]"></i>

View File

@ -1,4 +1,4 @@
<div wire:poll.30m="refresh" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
<div class="flex items-center justify-between">
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
<i class="ph ph-shield-warning text-white/70 text-[13px]"></i>

View File

@ -1,4 +1,4 @@
<div wire:poll.30s="refresh" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
<div class="flex items-center justify-between mb-3">
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
<i class="ph ph-shield-star text-white/70 text-[13px]"></i>

View File

@ -1,4 +1,4 @@
<div wire:poll.15s="refresh" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
<div class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full mb-3">
<i class="ph ph-bell-ringing text-white/70 text-[13px]"></i>
<span class="text-[11px] uppercase text-white/70">System-Warnungen</span>

View File

@ -1,4 +1,4 @@
<div wire:key="storage-{{ md5($target ?? '/') }}" wire:poll.60s="refresh" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
<div wire:key="storage-{{ md5($target ?? '/') }}" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
<div class="flex items-center justify-between mb-2">
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
<i class="ph ph-archive-box text-white/70 text-[13px]"></i>

View File

@ -1,4 +1,4 @@
<div wire:poll.10s="loadData" class="#glass-card #p-5">
<div class="#glass-card #p-5">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
{{-- CPU --}}
<div class="glass-card p-4 flex flex-col justify-between min-h-[140px]">
@ -67,6 +67,16 @@
</div>
<div class="text-2xl font-semibold">{{ $uptimeText ?? '' }}</div>
</div>
<div class="flex items-center gap-2 text-[10px]">
<div class="inline-flex items-center gap-1 rounded-full bg-white/5 border border-white/10 px-1.5 py-0.5 #mb-3">
<span class="tracking-wide uppercase text-white/70">{{ $uptimeDays }}</span>
<span class="text-white/70">{{ $uptimeDaysLabel ?? '' }}</span>
</div>
<div class="inline-flex items-center gap-1 rounded-full bg-white/5 border border-white/10 px-1.5 py-0.5 #mb-3">
<span class="tracking-wide uppercase text-white/70">{{ $uptimeHours }}</span>
<span class="text-white/70">{{ $uptimeHoursLabel ?? '' }}</span>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div wire:poll.15s="load" class="glass-card p-4">
<div class="glass-card p-4">
<div class="flex items-center justify-between mb-3">
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<i class="ph ph-gear-six text-white/70 text-[13px]"></i>

View File

@ -1,4 +1,4 @@
<div wire:poll.30s="refresh" class="glass-card relative p-4 max-h-fit border border-white/10 bg-white/5 rounded-2xl">
<div class="glass-card relative p-4 max-h-fit border border-white/10 bg-white/5 rounded-2xl">
{{-- Kopf --}}
<div class="flex items-center justify-between -mb-3">
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
@ -27,6 +27,11 @@
<div class="text-[10px] md:text-[11px] text-white/60 mt-1 uppercase tracking-wide">
{{ $diskCenterText['label'] }}
</div>
@if($measuredAt)
<div class="absolute bottom-12 mt-2 text-[10px] text-white/45 text-center">
zuletzt aktualisiert: <br> {{ \Carbon\Carbon::parse($measuredAt)->diffForHumans() }}
</div>
@endif
</div>
{{-- Segment-Ring --}}
@ -41,14 +46,6 @@
</div>
</div>
@if($measuredAt)
<div class="mt-2 text-[11px] text-white/45">
zuletzt aktualisiert: {{ \Carbon\Carbon::parse($measuredAt)->diffForHumans() }}
</div>
@endif
<button wire:click="refresh" class="mt-2 px-2 py-1 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20">
Jetzt aktualisieren
</button>
{{-- Zahlen --}}
<div class="md:pl-2">
<dl class="space-y-2">

View File

@ -1,36 +0,0 @@
<div class="bg-white/5 border border-white/10 rounded-xl p-4"
@if($running) wire:poll.3s="poll" @endif>
<div class="flex items-center justify-between gap-3">
<div class="text-white/80">
<strong>MailWolt Update</strong>
@if($latest)
<span class="ml-2 px-2 py-0.5 rounded-full text-xs border border-blue-400/40 bg-blue-500/10 text-blue-200">
verfügbar: {{ $latest }}
</span>
@endif
@if($running)
<span class="ml-2 px-2 py-0.5 rounded-full text-xs border border-white/20 bg-white/10">läuft </span>
@elseif(!is_null($rc))
<span class="ml-2 px-2 py-0.5 rounded-full text-xs border {{ $rc===0 ? 'border-emerald-400/40 bg-emerald-500/10 text-emerald-200' : 'border-rose-400/40 bg-rose-500/10 text-rose-200' }}">
{{ $rc===0 ? 'fertig' : 'fehler' }}
</span>
@endif
</div>
<div class="flex items-center gap-2">
<button wire:click="runUpdate"
@disabled($running)
class="bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white px-3 py-1 rounded">
Jetzt aktualisieren
</button>
<button wire:click="refreshState"
class="border border-white/10 bg-white/5 text-white/80 px-3 py-1 rounded hover:border-white/20">
Neu laden
</button>
</div>
</div>
@if($running || !empty($log))
<pre class="mt-3 max-h-64 overflow-auto text-xs bg-black/40 rounded p-3 border border-white/10 text-white/80">{{ $log }}</pre>
@endif
</div>

View File

@ -12,10 +12,6 @@
<livewire:ui.system.health-card />
</div>
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.dashboard.health-card/>--}}
{{-- </div>--}}
{{-- ========== 2. Systemstatus / Updates ========== --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<livewire:ui.system.woltguard-card />
@ -49,94 +45,4 @@
</div>
</div>
{{-- <div class="max-w-9xl mx-auto space-y-6 px-2 md:px-4">--}}
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.dashboard.top-bar/>--}}
{{-- </div>--}}
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.system.health-card/>--}}
{{-- </div>--}}
{{-- <div class="md:col-span-6">--}}
{{-- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">--}}
{{-- <div>--}}
{{-- <livewire:ui.system.update-card />--}}
{{-- </div>--}}
{{-- <div>--}}
{{-- <livewire:ui.system.woltguard-card />--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- <div class="grid grid-cols-1 md:grid-cols-12 gap-4">--}}
{{-- --}}{{-- links --}}
{{-- <div class="md:col-span-6 space-y-4">--}}
{{-- <livewire:ui.system.storage-card/>--}}
{{-- <livewire:ui.system.woltguard-card/>--}}
{{-- <livewire:ui.system.storage-card/>--}}
{{-- <livewire:ui.security.spam-av-card/>--}}
{{-- <livewire:ui.mail.dns-health-card/>--}}
{{-- <livewire:ui.security.fail2-ban-card/>--}}
{{-- <livewire:ui.mail.bounce-card/>--}}
{{-- </div>--}}
{{-- --}}{{-- rechts --}}
{{-- <div class="md:col-span-6 space-y-4">--}}
{{-- <livewire:ui.system.services-card/>--}}
{{-- <livewire:ui.security.rbl-card/>--}}
{{-- <livewire:ui.system.backup-status-card/>--}}
{{-- <livewire:ui.system.alerts-card/>--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- --}}{{-- <div class="max-w-fit col-span-4 #lg:col-span-6">--}}
{{-- --}}{{-- <livewire:ui.system.update-manager />--}}
{{-- --}}{{-- </div>--}}
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.system.alerts-card/>--}}
{{-- </div>--}}
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.mail.bounce-card/>--}}
{{-- </div>--}}
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.security.fail2-ban-card/>--}}
{{-- </div>--}}
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.system.backup-status-card/>--}}
{{-- </div>--}}
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.security.rbl-card/>--}}
{{-- </div>--}}
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.mail.dns-health-card/>--}}
{{-- </div>--}}
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.security.spam-av-card/>--}}
{{-- </div>--}}
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.mail.queue-card/>--}}
{{-- </div>--}}
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.dashboard.top-bar/>--}}
{{-- </div>--}}
{{-- <div class="col-span-12 lg:col-span-6">--}}
{{-- <livewire:ui.dashboard.health-card/>--}}
{{-- </div>--}}
{{-- <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">--}}
{{-- <livewire:ui.dashboard.domains-panel/>--}}
{{-- <livewire:ui.dashboard.recent-logins-table/>--}}
{{-- </div>--}}
{{-- </div>--}}
@endsection

View File

@ -7,3 +7,8 @@ use Illuminate\Support\Facades\Broadcast;
//});
Broadcast::channel('system.tasks', fn() => true);
Broadcast::routes(['middleware' => ['web', 'auth']]);
Broadcast::channel('health', function ($user) {
return (bool) $user;
});

View File

@ -15,3 +15,4 @@ Schedule::command('mailwolt:check-updates')->everytwoMinutes();
Schedule::command('mail:update-stats')->everyFiveMinutes()->withoutOverlapping();
Schedule::command('health:probe-disk', ['target' => '/', '--ttl' => 900])->everyTenMinutes();
Schedule::command('health:collect')->everyMinute();

0
totalCount Normal file
View File