dispatch('toast', type: 'warn', badge: 'Backup', title: 'Kein Zeitplan', text: 'Bitte zuerst einen Backup-Zeitplan konfigurieren.', duration: 4000); return; } // Reset stale jobs (stuck > 30 min with no running process) BackupJob::whereIn('status', ['queued', 'running']) ->where('started_at', '<', now()->subMinutes(30)) ->update(['status' => 'failed', 'finished_at' => now(), 'error' => 'Timeout — Prozess nicht mehr aktiv.']); $running = BackupJob::whereIn('status', ['queued', 'running'])->exists(); if ($running) { $this->dispatch('toast', type: 'warn', badge: 'Backup', title: 'Läuft bereits', text: 'Ein Backup-Job ist bereits aktiv.', duration: 3000); return; } $job = BackupJob::create([ 'policy_id' => $policy->id, 'status' => 'queued', 'started_at' => now(), ]); $artisan = base_path('artisan'); exec("nohup php {$artisan} backup:run {$job->id} > /dev/null 2>&1 &"); $this->dispatch('openModal', component: 'ui.system.modal.backup-progress-modal', arguments: ['jobId' => $job->id] ); } public function openProgress(int $id): void { $this->dispatch('openModal', component: 'ui.system.modal.backup-progress-modal', arguments: ['jobId' => $id] ); } public function openDeleteConfirm(int $id): void { $this->dispatch('openModal', component: 'ui.system.modal.backup-delete-modal', arguments: ['jobId' => $id] ); } public function openRestoreConfirm(int $id): void { $this->dispatch('openModal', component: 'ui.system.modal.backup-restore-confirm-modal', arguments: ['jobId' => $id] ); } #[On('backup-list-refresh')] public function refresh(): void {} #[On('backup:do-restore')] public function onRestoreConfirmed(int $jobId): void { $this->restore($jobId); } public function restore(int $id): void { $sourceJob = BackupJob::findOrFail($id); if (!$sourceJob->artifact_path || !file_exists($sourceJob->artifact_path)) { $this->dispatch('toast', type: 'warn', badge: 'Backup', title: 'Datei nicht gefunden', text: 'Das Backup-Archiv wurde nicht gefunden.', duration: 4000); return; } $token = 'mailwolt_restore_' . uniqid(); $artisan = base_path('artisan'); exec("nohup php {$artisan} restore:run {$sourceJob->id} {$token} > /dev/null 2>&1 &"); $this->dispatch('openModal', component: 'ui.system.modal.backup-progress-modal', arguments: ['jobId' => $sourceJob->id, 'restoreToken' => $token] ); } public function delete(int $id): void { $job = BackupJob::findOrFail($id); if ($job->artifact_path && file_exists($job->artifact_path)) { @unlink($job->artifact_path); } $job->delete(); $this->dispatch('toast', type: 'done', badge: 'Backup', title: 'Gelöscht', text: 'Backup-Eintrag wurde entfernt.', duration: 3000); } public function render() { $jobs = BackupJob::where('checksum', '!=', 'restore')->orWhereNull('checksum')->latest('started_at')->paginate(20); $policy = BackupPolicy::first(); $hasRunning = BackupJob::whereIn('status', ['queued', 'running']) ->where('started_at', '>=', now()->subMinutes(30)) ->exists(); return view('livewire.ui.system.backup-job-list', compact('jobs', 'policy', 'hasRunning')); } }