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

main v1.0.27
boban 2025-10-25 13:10:06 +02:00
parent e67c8613b3
commit a9609d358b
9 changed files with 495 additions and 205 deletions

View File

@ -11,29 +11,113 @@ class CheckUpdates extends Command
public function handle(): int
{
$appPath = base_path();
$current = trim(@file_get_contents(base_path('VERSION'))) ?: '0.0.0';
// newest tag from origin (sorted semver-friendly)
$latest = trim(shell_exec(
"cd {$appPath} && git fetch --tags --quiet origin && git tag --list | sort -V | tail -n1"
) ?? '');
$currentNorm = $this->readInstalledVersionNorm();
$currentRaw = $this->readInstalledVersionRaw() ?? ($currentNorm ? 'v'.$currentNorm : null);
// Tags haben usually ein 'v' Prefix entfernen
$latest = ltrim($latest, 'v');
// Neuesten Tag vom Remote holen (semver-freundlich sortiert)
$appPath = base_path();
$cmd = <<<BASH
set -e
cd {$appPath}
git fetch --tags --force --quiet origin +refs/tags/*:refs/tags/*
# Beste Wahl zuerst: v*-Tags semver-sortiert, sonst alle Tags
(git tag -l 'v*' --sort=-v:refname | head -n1) || true
BASH;
if (!$latest) {
$latestTagRaw = trim((string) shell_exec($cmd));
if ($latestTagRaw === '') {
// Fallback auf alle Tags (ohne Filter), falls keine v*-Tags existieren
$latestTagRaw = trim((string) shell_exec("cd {$appPath} && git tag -l --sort=-v:refname | head -n1"));
}
// Normieren (v weg, Suffixe wie -3-gabcd/-dirty entfernen)
$latestNorm = $this->normalizeVersion($latestTagRaw);
// Nichts gefunden -> Caches leeren
if (!$latestNorm) {
cache()->forget('updates:latest');
cache()->forget('updates:latest_raw');
cache()->forget('mailwolt.update_available'); // Legacy
$this->warn('Keine Release-Tags gefunden.');
cache()->forget('mailwolt.update_available');
return 0;
return self::SUCCESS;
}
if (version_compare($latest, $current, '>')) {
cache()->forever('mailwolt.update_available', $latest);
$this->info("Update verfügbar: {$latest} (installiert: {$current})");
// Cache schreiben
cache()->put('updates:latest', $latestNorm, now()->addMinutes(10));
cache()->put('updates:latest_raw', $latestTagRaw, now()->addMinutes(10));
cache()->forever('mailwolt.update_available', $latestNorm); // Legacy-Kompat
// Vergleich & Ausgabe
if ($currentNorm && version_compare($latestNorm, $currentNorm, '>')) {
$this->info("Update verfügbar: {$latestTagRaw} (installiert: ".($currentRaw ?? $currentNorm).")");
} else {
$this->info("Aktuell (installiert: ".($currentRaw ?? $currentNorm ?? 'unbekannt').").");
// Kein Update Legacy-Key aufräumen (UI liest die neuen Keys)
cache()->forget('mailwolt.update_available');
$this->info("Aktuell (installiert: {$current}).");
}
return 0;
cache()->put('updates:last_checked_at', now(), now()->addMinutes(10));
return self::SUCCESS;
}
/* ===== Helpers ===== */
private function readInstalledVersionNorm(): ?string
{
$paths = [
'/var/lib/mailwolt/version', // vom Wrapper (normiert)
base_path('VERSION'), // App-Fallback
];
foreach ($paths as $p) {
$raw = @trim(@file_get_contents($p) ?: '');
if ($raw !== '') return $this->normalizeVersion($raw);
}
// Noch ein Fallback aus RAW-Datei
$raw = $this->readInstalledVersionRaw();
return $raw ? $this->normalizeVersion($raw) : null;
}
private function readInstalledVersionRaw(): ?string
{
$p = '/var/lib/mailwolt/version_raw'; // vom Wrapper (z.B. "v1.0.25" oder "v1.0.25-3-gabcd")
$raw = @trim(@file_get_contents($p) ?: '');
return $raw !== '' ? $raw : null;
}
private function normalizeVersion(?string $v): ?string
{
if (!$v) return null;
$v = trim($v);
if ($v === '') return null;
$v = ltrim($v, "vV \t\n\r\0\x0B"); // führendes v entfernen
$v = preg_replace('/-.*$/', '', $v); // Build-/dirty-Suffix abschneiden
return $v !== '' ? $v : null;
}
// public function handle(): int
// {
// $appPath = base_path();
// $current = trim(@file_get_contents(base_path('VERSION'))) ?: '0.0.0';
// // newest tag from origin (sorted semver-friendly)
// $latest = trim(shell_exec(
// "cd {$appPath} && git fetch --tags --quiet origin && git tag --list | sort -V | tail -n1"
// ) ?? '');
//
// // Tags haben usually ein 'v' Prefix entfernen
// $latest = ltrim($latest, 'v');
//
// if (!$latest) {
// $this->warn('Keine Release-Tags gefunden.');
// cache()->forget('mailwolt.update_available');
// return 0;
// }
//
// if (version_compare($latest, $current, '>')) {
// cache()->forever('mailwolt.update_available', $latest);
// $this->info("Update verfügbar: {$latest} (installiert: {$current})");
// } else {
// cache()->forget('mailwolt.update_available');
// $this->info("Aktuell (installiert: {$current}).");
// }
// return 0;
// }
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class MailwoltRestart extends Command
{
protected $signature = 'mailwolt:restart-services';
protected $description = 'Restart or reload MailWolt-related system services';
public function handle(): int
{
$units = config('mailwolt.units', []);
foreach ($units as $u) {
$unit = rtrim($u['name'] ?? '', '.service') . '.service';
$action = $u['action'] ?? 'try-reload-or-restart';
$cmd = sprintf('sudo -n /usr/bin/systemctl %s %s', escapeshellarg($action), escapeshellarg($unit));
$this->info("{$unit} ({$action})");
exec($cmd . ' 2>&1', $out, $rc);
foreach ($out as $line) {
$this->line(" $line");
}
if ($rc !== 0) {
$this->warn(" [!] Fehler beim Neustart von {$unit} (rc={$rc})");
} else {
$this->line(" [✓] Erfolgreich");
}
}
return self::SUCCESS;
}
}

View File

@ -2,6 +2,7 @@
namespace App\Jobs;
use App\Support\CacheVer;
use App\Support\WoltGuard\Probes;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
@ -30,47 +31,9 @@ class RunHealthChecks implements ShouldQueue
}
$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]);
// }
Cache::put(CacheVer::k('health:services'), $svcRows, 60);
Cache::forget('health:services');
}
/** Wraps a probe; logs and returns fallback on error */

View File

@ -2,6 +2,7 @@
namespace App\Livewire\Ui\System;
use App\Support\CacheVer;
use App\Support\WoltGuard\Probes;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
@ -25,35 +26,6 @@ class ServicesCard extends Component
// optionales Polling im Blade (du hast es dort referenziert)
public int $pollSeconds = 15;
/** Pro Karte mehrere Quellen Grün sobald eine Quelle OK ist */
// protected array $cards = [
// // Mail
// 'postfix' => ['label'=>'Postfix','hint'=>'MTA / Versand','sources'=>['systemd:postfix']],
// 'dovecot' => ['label'=>'Dovecot','hint'=>'IMAP / POP3','sources'=>['systemd:dovecot','tcp:127.0.0.1:993']],
// 'rspamd' => ['label'=>'Rspamd','hint'=>'Spamfilter','sources'=>['systemd:rspamd','tcp:127.0.0.1:11333','tcp:127.0.0.1:11334']],
// 'clamav' => ['label'=>'ClamAV','hint'=>'Virenscanner','sources'=>[
// 'systemd:clamav-daemon','systemd:clamav-daemon@scan','systemd:clamd',
// 'socket:/run/clamav/clamd.ctl','pid:/run/clamav/clamd.pid','tcp:127.0.0.1:3310',
// ]],
// // Daten & Cache
// 'db' => ['label'=>'Datenbank','hint'=>'MySQL / MariaDB','sources'=>['db']],
// 'redis' => ['label'=>'Redis','hint'=>'Cache / Queue','sources'=>['tcp:127.0.0.1:6379','systemd:redis-server','systemd:redis']],
// // Web / PHP
// 'php-fpm' => ['label'=>'PHP-FPM','hint'=>'PHP Runtime','sources'=>[
// 'systemd:php8.3-fpm','systemd:php8.2-fpm','systemd:php8.1-fpm','systemd:php-fpm',
// 'socket:/run/php/php8.3-fpm.sock','socket:/run/php/php8.2-fpm.sock',
// 'socket:/run/php/php8.1-fpm.sock','socket:/run/php/php-fpm.sock','tcp:127.0.0.1:9000',
// ]],
// 'nginx' => ['label'=>'Nginx','hint'=>'Webserver','sources'=>['systemd:nginx','tcp:127.0.0.1:80']],
// // MailWolt (Unit ODER laufender artisan Prozess)
// 'mw-queue' => ['label'=>'MailWolt Queue','hint'=>'Job Worker','sources'=>['systemd:mailwolt-queue','proc:/php.*artisan(\.php)?\s+queue:work']],
// 'mw-schedule' => ['label'=>'MailWolt Schedule','hint'=>'Task Scheduler','sources'=>['systemd:mailwolt-schedule','proc:/php.*artisan(\.php)?\s+schedule:work']],
// 'mw-ws' => ['label'=>'MailWolt WebSocket','hint'=>'Echtzeit Updates','sources'=>['systemd:mailwolt-ws','tcp:127.0.0.1:8080']],
// // Sonstiges
// 'fail2ban' => ['label'=>'Fail2Ban','hint'=>'SSH / Mail Protection','sources'=>['systemd:fail2ban']],
// 'journal' => ['label'=>'System Logs','hint'=>'Journal','sources'=>['systemd:systemd-journald','systemd:rsyslog']],
// ];
public function mount(): void
{
$this->load();
@ -73,7 +45,15 @@ class ServicesCard extends Component
public function load(): void
{
$cards = config('woltguard.cards', []);
$cached = collect(Cache::get('health:services', []))->keyBy('name');
$raw = Cache::get(CacheVer::k('health:services'), []);
// einmaliger Fallback für ältere Deploys:
if (empty($raw)) {
$raw = Cache::get('health:services', []);
}
$cached = collect($raw)->keyBy('name');
$rows = [];
$ok = 0;

View File

@ -21,117 +21,34 @@ class UpdateCard extends Component
public bool $running = false;
public ?int $rc = null;
public bool $postActionsDone = false;
protected string $cacheStartedAtKey = 'mw.update.started_at';
protected int $failsafeSeconds = 20 * 60; // 20 Minuten
public function mount(): void
{
$this->reloadVersionsAndStatus();
$this->recompute();
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) {
// kein fataler Abbruch
}
$this->reloadVersionsAndStatus();
$this->recompute();
$this->finishUiIfNoUpdate();
}
public function runUpdate(): void
{
Cache::forget('mailwolt.update_available');
Cache::put($this->cacheStartedAtKey, time(), now()->addHour());
@shell_exec('nohup sudo -n /usr/local/sbin/mw-update >/dev/null 2>&1 &');
$this->latest = null;
$this->displayLatest = null;
$this->hasUpdate = false;
$this->state = 'running';
$this->running = true;
$this->message = 'Update läuft …';
}
public function tick(): void
{
$this->refreshLowLevelState();
$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) {
Cache::forget($this->cacheStartedAtKey);
$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';
}
}
/* ========== Helper ========== */
protected int $failsafeSeconds = 20 * 60;
private const VERSION_FILE = '/var/lib/mailwolt/version'; // normiert, ohne "v"
private const VERSION_FILE_RAW = '/var/lib/mailwolt/version_raw'; // original ("v1.0.25" o.ä.)
private const BUILD_INFO = '/etc/mailwolt/build.info'; // Fallback
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 ?? '';
$this->message = "Du bist auf dem neuesten Stand ({$cur})";
$this->messagePositive = true;
Cache::forget('mailwolt.update_available');
// Update-Checker soll beide Keys schreiben:
// - updates:latest (normiert, ohne v)
// - updates:latest_raw (Original)
$latNorm = Cache::get('updates:latest');
$latRaw = Cache::get('updates:latest_raw');
// Falls dein alter Key noch benutzt wird, weiter kompatibel:
if (!$latNorm && ($legacy = Cache::get('mailwolt.update_available'))) {
$latNorm = $this->normalizeVersion($legacy);
$latRaw = $legacy;
}
$this->latest = $latNorm ?: null; // für Vergleiche
$this->displayLatest = $latRaw ?: ($latNorm ? 'v'.$latNorm : null);
$this->refreshLowLevelState();
}
protected function recompute(): void
@ -143,44 +60,310 @@ class UpdateCard extends Component
? version_compare($latNorm, $curNorm, '>')
: false;
// Immer mit v-Präfix anzeigen
$this->displayCurrent = $curNorm ? 'v' . $curNorm : null;
$this->displayLatest = ($this->hasUpdate && $latNorm) ? 'v' . $latNorm : null;
// Anzeige immer hübsch mit "v", Vergleich bleibt normiert
$this->displayCurrent = $curNorm ? 'v'.$curNorm : null;
// displayLatest NICHT mehr vom hasUpdate abhängig machen bleibt informativ sichtbar
if (!$this->displayLatest && $latNorm) {
$this->displayLatest = 'v'.$latNorm;
}
}
protected function refreshLowLevelState(): void
protected function finishUiIfNoUpdate(): void
{
$state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: '');
$this->running = ($state === 'running');
if (!$this->hasUpdate) {
$this->state = 'idle';
$cur = $this->displayCurrent ?? '';
$this->message = "Du bist auf dem neuesten Stand ({$cur})";
$this->messagePositive = true;
$rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: '');
$this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
// Nur den alten Legacy-Key aufräumen
Cache::forget('mailwolt.update_available');
}
}
protected function readCurrentVersion(): ?string
{
$build = @file_get_contents('/etc/mailwolt/build.info');
// 1) Wrapper-Datei (normiert, ohne „v“)
$v = @trim(@file_get_contents(self::VERSION_FILE) ?: '');
if ($v !== '') return $v;
// 2) Raw -> normieren
$raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: '');
if ($raw !== '') return $this->normalizeVersion($raw);
// 3) Fallback build.info (format: "version=…")
$build = @file_get_contents(self::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 = $this->normalizeVersion(trim(substr($line, 8)));
if ($v) return $v;
}
}
}
$v = config('app.version');
return $v !== '' ? $v : null;
// 4) Noch ein letzter Fallback
$v = $this->normalizeVersion(config('app.version') ?: '');
return $v ?: null;
}
protected function normalizeVersion(?string $v): ?string
{
if ($v === null) return null;
$v = trim($v);
return ltrim($v, "vV \t\n\r\0\x0B") ?: null;
if ($v === '') return null;
// führendes v entfernen, Build-Suffixe (z.B. "-3-gabcd" oder "-dirty") kappen
$v = ltrim($v, "vV \t\n\r\0\x0B");
$v = preg_replace('/-.*$/', '', $v); // alles nach erster '-' weg
return $v !== '' ? $v : null;
}
public function runUpdate(): void
{
Cache::forget('mailwolt.update_available');
Cache::put($this->cacheStartedAtKey, time(), now()->addHour());
// Name korrigiert: Wrapper ist /usr/local/sbin/mailwolt-update
@shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-update >/dev/null 2>&1 &');
$this->latest = null;
$this->displayLatest = null;
$this->hasUpdate = false;
$this->state = 'running';
$this->running = true;
$this->message = 'Update läuft …';
}
protected function refreshLowLevelState(): void
{
// Wrapper-Status lesen
$state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: '');
// Läuft, wenn Datei "running" enthält alles andere gilt als nicht laufend
$this->running = ($state === 'running');
// Rückgabecode (nur gesetzt, wenn Update beendet ist)
$rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: '');
$this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
}
public function pollUpdate(): void
{
// 1) Low-Level Status vom Wrapper (/var/lib/mailwolt/update/{state,rc})
$this->refreshLowLevelState();
// 2) failsafe: wenn zu lange läuft, als beendet markieren
$started = (int) Cache::get($this->cacheStartedAtKey, 0);
if ($this->running && $started > 0 && (time() - $started) > $this->failsafeSeconds) {
$this->running = false;
$this->rc ??= 0; // notfalls als “erfolgreich” werten
}
// 3) Wenn nicht mehr running ⇒ Abschluss behandeln
if (!$this->running) {
Cache::forget($this->cacheStartedAtKey);
// Versionen neu laden und “hasUpdate” neu berechnen
$this->reloadVersionsAndStatus();
$this->recompute();
// 3a) Erfolg: jetzt erst Dienste neu starten
if ($this->rc === 0 && !$this->postActionsDone) {
// Restart asynchron starten (dein Artisan-Command macht systemctl etc.)
@shell_exec('nohup php /var/www/mailwolt/artisan mailwolt:restart-services >/dev/null 2>&1 &');
$this->postActionsDone = true;
// Toast
$ver = $this->displayCurrent ?? 'aktuelle Version';
$this->dispatch('toast',
type: 'success',
title: 'Update erfolgreich',
text: "MailWolt wurde auf {$ver} aktualisiert. Dienste werden neu gestartet …",
badge: 'System',
duration: 5000
);
// Seite nach 5s neu laden
$this->dispatch('reload-page', delay: 5000);
}
// 3b) Fehler: Meldung zeigen
elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
$this->postActionsDone = true;
$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
);
}
// 4) UI zurück auf idle
$this->state = 'idle';
}
}
}
// public function mount(): void
// {
// $this->reloadVersionsAndStatus();
// $this->recompute();
// 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) {
// // kein fataler Abbruch
// }
//
// $this->reloadVersionsAndStatus();
// $this->recompute();
// $this->finishUiIfNoUpdate();
// }
//
// public function runUpdate(): void
// {
// Cache::forget('mailwolt.update_available');
// Cache::put($this->cacheStartedAtKey, time(), now()->addHour());
//
// @shell_exec('nohup sudo -n /usr/local/sbin/mw-update >/dev/null 2>&1 &');
//
// $this->latest = null;
// $this->displayLatest = null;
// $this->hasUpdate = false;
// $this->state = 'running';
// $this->running = true;
// $this->message = 'Update läuft …';
// }
//
// public function tick(): void
// {
// $this->refreshLowLevelState();
//
// $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) {
// Cache::forget($this->cacheStartedAtKey);
// $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';
// }
// }
//
// /* ========== Helper ========== */
//
// 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 ?? '';
// $this->message = "Du bist auf dem neuesten Stand ({$cur})";
// $this->messagePositive = true;
// 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;
//
// // Immer mit v-Präfix anzeigen
// $this->displayCurrent = $curNorm ? 'v' . $curNorm : null;
// $this->displayLatest = ($this->hasUpdate && $latNorm) ? 'v' . $latNorm : null;
// }
//
// 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
// {
// $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);
// return ltrim($v, "vV \t\n\r\0\x0B") ?: null;
// }
//namespace App\Livewire\Ui\System;
//
//use Illuminate\Support\Facades\Artisan;

28
app/Support/CacheVer.php Normal file
View File

@ -0,0 +1,28 @@
<?php
namespace App\Support;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
final class CacheVer
{
private const KEY = 'app:cache_v';
public static function get(): string
{
return (string) (Cache::get(self::KEY) ?? '1');
}
public static function bump(): string
{
$v = (string) Str::uuid(); // oder (int)+=1, UUID ist robust
Cache::forever(self::KEY, $v);
return $v;
}
public static function k(string $key): string
{
return 'v:'.self::get().':'.$key;
}
}

15
config/mailwolt.php Normal file
View File

@ -0,0 +1,15 @@
<?php
return [
'units' => [
['name' => 'nginx', 'action' => 'reload'],
['name' => 'php8.2-fpm', 'action' => 'restart'],
['name' => 'postfix', 'action' => 'try-reload-or-restart'],
['name' => 'dovecot', 'action' => 'try-reload-or-restart'],
['name' => 'rspamd', 'action' => 'try-reload-or-restart'],
['name' => 'opendkim', 'action' => 'try-reload-or-restart'],
['name' => 'opendmarc', 'action' => 'try-reload-or-restart'],
['name' => 'clamav-daemon', 'action' => 'try-reload-or-restart'],
['name' => 'redis-server', 'action' => 'try-reload-or-restart'],
],
];

View File

@ -8,6 +8,7 @@
<i class="ph ph-clock text-slate-300"></i> TTL: {{ $ttl }}
</span>
<h2 class="text-[18px] font-semibold text-slate-100">DNS-Einträge</h2>
<p class="text-[13px] text-slate-300/80">
Setze die folgenden Records für
<span class="text-sky-300 underline decoration-sky-500/40 underline-offset-2">{{ $zone }}</span>.

View File

@ -97,7 +97,7 @@
{{--</div>--}}
<div
class="glass-card rounded-2xl p-4 border border-white/10 bg-white/5 max-h-fit"
@if($state === 'running') wire:poll.1200ms="tick" @endif
@if($state === 'running') wire:poll.3s="pollUpdate" @endif
>
<div class="flex items-start gap-2">
{{-- Shield + Update-Icon --}}