parent
e67c8613b3
commit
a9609d358b
|
|
@ -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;
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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'],
|
||||
],
|
||||
];
|
||||
|
|
@ -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>.
|
||||
|
|
|
|||
|
|
@ -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 --}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue