reloadVersionsAndStatus(); $this->recompute(); $this->readLogLines(); if ($this->running) { $this->state = 'running'; } $this->recalcProgress(); } public function render() { return view('livewire.ui.system.update-page'); } /* ================== Aktionen ================== */ public function checkForUpdates(): void { @shell_exec('php ' . base_path('artisan') . ' mailwolt:check-updates 2>&1'); $this->reloadVersionsAndStatus(); $this->recompute(); if ($this->hasUpdate) { $this->dispatch('toast', type: 'done', badge: 'Updates', title: 'Update verfügbar', text: "Version {$this->displayLatest} ist verfügbar.", duration: 4000); } else { $this->dispatch('toast', type: 'done', badge: 'Updates', title: 'Alles aktuell', text: 'Es sind keine Updates verfügbar.', duration: 3000); } } public function runUpdate(): void { if ($this->running || $this->state === 'running') { $this->dispatch('toast', type: 'warn', badge: 'Updates', title: 'Läuft bereits', text: 'Ein Update-Prozess ist bereits aktiv.', duration: 3000); return; } Cache::forget('mailwolt.update_available'); Cache::put($this->cacheStartedAtKey, time(), now()->addHour()); @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->rc = null; $this->postActionsDone = false; $this->logLines = ['Update gestartet …']; $this->progressPct = 5; } public function pollStatus(): void { $this->refreshLowLevelState(); $this->readLogLines(); $this->recalcProgress(); if ($this->rc !== null) { $this->running = false; } // Failsafe $started = (int) Cache::get($this->cacheStartedAtKey, 0); if ($this->running && $started > 0 && (time() - $started) > $this->failsafeSeconds) { $this->running = false; $this->lowState = 'done'; $this->rc ??= 0; } if ($this->lowState === 'done') { Cache::forget($this->cacheStartedAtKey); usleep(300_000); $this->reloadVersionsAndStatus(); $this->recompute(); $this->readLogLines(); $this->progressPct = 100; if ($this->rc === 0 && !$this->postActionsDone) { @shell_exec('nohup php /var/www/mailwolt/artisan optimize:clear >/dev/null 2>&1 &'); @shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &'); @shell_exec('nohup php /var/www/mailwolt/artisan settings:sync >/dev/null 2>&1 &'); $this->postActionsDone = true; $ver = $this->displayCurrent ?? 'aktuelle Version'; $this->dispatch('toast', type: 'done', badge: 'Updates', title: 'Update abgeschlossen', text: "Mailwolt wurde auf {$ver} aktualisiert.", duration: 6000); } elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) { $this->postActionsDone = true; $this->dispatch('toast', type: 'error', badge: 'Updates', title: 'Update fehlgeschlagen', text: "Rückgabecode: {$this->rc}. Bitte Log prüfen.", duration: 0); } $this->state = 'idle'; } } public function clearLog(): void { // Only admins / allow via gate if you have policy; basic protection: @file_put_contents(self::UPDATE_LOG, ''); $this->logLines = []; $this->dispatch('toast', type: 'done', badge: 'Updates', title: 'Log geleert', text: '', duration: 2500); } /* ================== Helpers ================== */ protected function reloadVersionsAndStatus(): void { $this->current = $this->readCurrentVersion(); $latNorm = Cache::get('updates:latest'); $latRaw = Cache::get('updates:latest_raw'); if (!$latNorm && ($legacy = Cache::get('mailwolt.update_available'))) { $latNorm = $this->normalizeVersion($legacy); $latRaw = $legacy; } $this->latest = $latNorm ?: null; $this->displayLatest = $latRaw ?: ($latNorm ? 'v' . $latNorm : null); $this->refreshLowLevelState(); } protected function recompute(): void { $curNorm = $this->normalizeVersion($this->current); $latNorm = $this->normalizeVersion($this->latest); $this->hasUpdate = ($curNorm && $latNorm) ? version_compare($latNorm, $curNorm, '>') : false; $this->displayCurrent = $curNorm ? 'v' . $curNorm : null; if (!$this->displayLatest && $latNorm) { $this->displayLatest = 'v' . $latNorm; } } protected function refreshLowLevelState(): void { $state = @trim(@file_get_contents(self::STATE_DIR . '/state') ?: ''); $rcRaw = @trim(@file_get_contents(self::STATE_DIR . '/rc') ?: ''); $this->lowState = $state !== '' ? $state : null; $this->running = ($this->lowState !== 'done'); $this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int) $rcRaw : null; } protected function readLogLines(): void { $p = self::UPDATE_LOG; if (!is_readable($p)) { $this->logLines = []; return; } $lines = @file($p, FILE_IGNORE_NEW_LINES) ?: []; $this->logLines = array_slice($lines, -100); } protected function recalcProgress(): void { if ($this->state !== 'running' && $this->lowState !== 'running') { if ($this->lowState === 'done') { $this->progressPct = 100; } return; } $text = implode("\n", $this->logLines); $pct = 5; foreach ([ 'Update gestartet' => 10, 'Composer' => 25, 'npm ci' => 40, 'npm run build' => 60, 'migrate' => 75, 'optimize' => 85, 'Version aktualisiert' => 95, 'Update beendet' => 100, ] as $needle => $val) { if (stripos($text, $needle) !== false) { $pct = max($pct, $val); } } if ($this->lowState === 'done') { $pct = 100; } $this->progressPct = $pct; } protected function readCurrentVersion(): ?string { // Lokal: direkt aus git describe lesen damit Entwicklungsumgebung immer aktuell ist if (app()->isLocal()) { $tag = @trim((string) shell_exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null')); $v = $this->normalizeVersion($tag); if ($v) return $v; } $v = @trim(@file_get_contents(self::VERSION_FILE) ?: ''); if ($v !== '') return $v; $raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: ''); if ($raw !== '') return $this->normalizeVersion($raw); $build = @file_get_contents(self::BUILD_INFO); if ($build) { foreach (preg_split('/\R+/', $build) as $line) { if (str_starts_with($line, 'version=')) { $v = $this->normalizeVersion(trim(substr($line, 8))); if ($v) return $v; } } } $v = $this->normalizeVersion(config('app.version') ?: ''); return $v ?: null; } protected function normalizeVersion(?string $v): ?string { if ($v === null) return null; $v = trim($v); if ($v === '') return null; $v = ltrim($v, "vV \t\n\r\0\x0B"); $v = preg_replace('/-.*$/', '', $v); return $v !== '' ? $v : null; } protected function getBuildTimestamp(): ?string { $build = @file_get_contents(self::BUILD_INFO); if (!$build) return null; foreach (preg_split('/\R+/', $build) as $line) { if (str_starts_with($line, 'built_at=') || str_starts_with($line, 'date=')) { $parts = explode('=', $line, 2); return trim($parts[1] ?? ''); } } return null; } }