parent
812f91202f
commit
e67c8613b3
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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]; }
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
// // // });
|
||||
// // //
|
||||
// // // });
|
||||
|
|
|
|||
|
|
@ -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--}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue