diff --git a/app/Filament/Admin/Pages/ServerUpdates.php b/app/Filament/Admin/Pages/ServerUpdates.php index 06bdffb..fee4506 100644 --- a/app/Filament/Admin/Pages/ServerUpdates.php +++ b/app/Filament/Admin/Pages/ServerUpdates.php @@ -17,6 +17,9 @@ use Filament\Tables\Concerns\InteractsWithTable; use Filament\Tables\Contracts\HasTable; use Filament\Tables\Table; use Illuminate\Contracts\Support\Htmlable; +use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\View; class ServerUpdates extends Page implements HasActions, HasTable { @@ -33,23 +36,61 @@ class ServerUpdates extends Page implements HasActions, HasTable public array $packages = []; + public ?string $currentVersion = null; + + public ?int $behindCount = null; + + public array $recentChanges = []; + + public ?string $lastCheckedAt = null; + + public ?string $jabaliOutput = null; + + public ?string $jabaliOutputTitle = null; + + public ?string $jabaliOutputAt = null; + + public bool $isChecking = false; + + public bool $isUpgrading = false; + + public array $refreshOutput = []; + + public ?string $refreshOutputAt = null; + + public ?string $refreshOutputTitle = null; + protected ?AgentClient $agent = null; protected bool $updatesLoaded = false; public function getTitle(): string|Htmlable { - return __('Server Updates'); + return __('System Updates'); } public static function getNavigationLabel(): string { - return __('Server Updates'); + return __('System Updates'); } public function mount(): void { $this->loadUpdates(false); + $this->loadVersionInfo(); + } + + public function getUpdateStatusLabelProperty(): string + { + if ($this->behindCount === null) { + return __('Not checked'); + } + + if ($this->behindCount === 0) { + return __('Up to date'); + } + + return __(':count commit(s) behind', ['count' => $this->behindCount]); } protected function getAgent(): AgentClient @@ -57,15 +98,39 @@ class ServerUpdates extends Page implements HasActions, HasTable return $this->agent ??= new AgentClient; } - public function loadUpdates(bool $refreshTable = true): void + public function loadUpdates(bool $refreshTable = true, bool $refreshApt = false): void { try { - $result = $this->getAgent()->updatesList(); + $result = $this->getAgent()->updatesList($refreshApt); $this->packages = $result['packages'] ?? []; $this->updatesLoaded = true; + + if ($refreshApt) { + $refreshOutput = $result['refresh_output'] ?? []; + $refreshLines = is_array($refreshOutput) ? $refreshOutput : [$refreshOutput]; + $this->refreshOutput = ! empty(array_filter($refreshLines, static fn ($line) => $line !== null && $line !== '')) + ? $refreshLines + : [__('No output captured.')]; + $this->refreshOutputAt = now()->format('Y-m-d H:i:s'); + $this->refreshOutputTitle = __('Update Refresh Output'); + } + + $warnings = $result['warnings'] ?? []; + if (! empty($warnings)) { + Notification::make() + ->title(__('Update check completed with warnings')) + ->body(implode("\n", array_filter($warnings))) + ->warning() + ->send(); + } } catch (\Exception $e) { $this->packages = []; $this->updatesLoaded = true; + if ($refreshApt) { + $this->refreshOutput = [$e->getMessage()]; + $this->refreshOutputAt = now()->format('Y-m-d H:i:s'); + $this->refreshOutputTitle = __('Update Refresh Output'); + } Notification::make() ->title(__('Failed to load updates')) ->body($e->getMessage()) @@ -78,15 +143,103 @@ class ServerUpdates extends Page implements HasActions, HasTable } } + public function loadVersionInfo(): void + { + $this->currentVersion = $this->readVersion(); + } + + public function checkForUpdates(): void + { + $this->isChecking = true; + + try { + $exitCode = Artisan::call('jabali:upgrade', ['--check' => true]); + $output = trim(Artisan::output()); + + if ($exitCode !== 0) { + throw new \RuntimeException($output !== '' ? $output : __('Update check failed.')); + } + + $this->jabaliOutput = $output !== '' ? $output : __('No output captured.'); + $this->jabaliOutputTitle = __('Update Check Output'); + $this->jabaliOutputAt = now()->format('Y-m-d H:i:s'); + $this->lastCheckedAt = $this->jabaliOutputAt; + $this->parseUpdateOutput($output); + + Notification::make() + ->title(__('Update check completed')) + ->success() + ->send(); + } catch (\Throwable $e) { + $this->jabaliOutput = $e->getMessage(); + $this->jabaliOutputTitle = __('Update Check Output'); + $this->jabaliOutputAt = now()->format('Y-m-d H:i:s'); + Notification::make() + ->title(__('Update check failed')) + ->body($e->getMessage()) + ->danger() + ->send(); + } finally { + $this->isChecking = false; + } + } + + public function performUpgrade(): void + { + $this->isUpgrading = true; + + try { + $exitCode = Artisan::call('jabali:upgrade', ['--force' => true]); + $output = trim(Artisan::output()); + + if ($exitCode !== 0) { + throw new \RuntimeException($output !== '' ? $output : __('Upgrade failed.')); + } + + $this->jabaliOutput = $output !== '' ? $output : __('No output captured.'); + $this->jabaliOutputTitle = __('Upgrade Output'); + $this->jabaliOutputAt = now()->format('Y-m-d H:i:s'); + $this->loadVersionInfo(); + $this->behindCount = 0; + $this->recentChanges = []; + + Notification::make() + ->title(__('Upgrade completed')) + ->success() + ->send(); + } catch (\Throwable $e) { + $this->jabaliOutput = $e->getMessage(); + $this->jabaliOutputTitle = __('Upgrade Output'); + $this->jabaliOutputAt = now()->format('Y-m-d H:i:s'); + Notification::make() + ->title(__('Upgrade failed')) + ->body($e->getMessage()) + ->danger() + ->send(); + } finally { + $this->isUpgrading = false; + } + } + public function runUpdates(): void { try { - $this->getAgent()->updatesRun(); + $result = $this->getAgent()->updatesRun(); + $output = $result['output'] ?? []; + $outputLines = is_array($output) ? $output : [$output]; + $this->refreshOutput = ! empty(array_filter($outputLines, static fn ($line) => $line !== null && $line !== '')) + ? $outputLines + : [__('No output captured.')]; + $this->refreshOutputAt = now()->format('Y-m-d H:i:s'); + $this->refreshOutputTitle = __('System Update Output'); Notification::make() ->title(__('Updates completed')) ->success() ->send(); } catch (\Exception $e) { + $this->refreshOutput = [$e->getMessage()]; + $this->refreshOutputAt = now()->format('Y-m-d H:i:s'); + $this->refreshOutputTitle = __('System Update Output'); Notification::make() ->title(__('Update failed')) ->body($e->getMessage()) @@ -97,6 +250,38 @@ class ServerUpdates extends Page implements HasActions, HasTable $this->loadUpdates(); } + protected function parseUpdateOutput(string $output): void + { + $this->behindCount = null; + $this->recentChanges = []; + + if (preg_match('/Updates available:\s+(\d+)/', $output, $matches)) { + $this->behindCount = (int) $matches[1]; + } elseif (str_contains(strtolower($output), 'up to date')) { + $this->behindCount = 0; + } + + if (preg_match('/Recent changes:\s*(.+)$/s', $output, $matches)) { + $lines = preg_split('/\r?\n/', trim($matches[1])); + $this->recentChanges = array_values(array_filter($lines, static fn (string $line): bool => trim($line) !== '')); + } + } + + protected function readVersion(): string + { + $versionFile = base_path('VERSION'); + if (! File::exists($versionFile)) { + return 'unknown'; + } + + $content = File::get($versionFile); + if (preg_match('/VERSION=(.+)/', $content, $matches)) { + return trim($matches[1]); + } + + return 'unknown'; + } + public function table(Table $table): Table { return $table @@ -133,7 +318,7 @@ class ServerUpdates extends Page implements HasActions, HasTable Action::make('refresh') ->label(__('Refresh')) ->icon('heroicon-o-arrow-path') - ->action(fn () => $this->loadUpdates()), + ->action(fn () => $this->loadUpdates(true, true)), Action::make('runUpdates') ->label(__('Run Updates')) ->icon('heroicon-o-arrow-path-rounded-square') diff --git a/resources/views/filament/admin/pages/server-updates-output.blade.php b/resources/views/filament/admin/pages/server-updates-output.blade.php new file mode 100644 index 0000000..0ea3b08 --- /dev/null +++ b/resources/views/filament/admin/pages/server-updates-output.blade.php @@ -0,0 +1,13 @@ + + + {{ $refreshOutputTitle ?? __('Update Output') }} + @if($refreshOutputAt) + + {{ __('Last refreshed at :time', ['time' => $refreshOutputAt]) }} + + @endif + + + {{ implode("\n", $refreshOutput) }} + + diff --git a/resources/views/filament/admin/pages/server-updates.blade.php b/resources/views/filament/admin/pages/server-updates.blade.php index d166217..b144bd6 100644 --- a/resources/views/filament/admin/pages/server-updates.blade.php +++ b/resources/views/filament/admin/pages/server-updates.blade.php @@ -1,5 +1,95 @@ + + + {{ __('Jabali Panel Update') }} + + + {{ __('Update the panel codebase and assets.') }} + + + + + {{ __('Current Version') }} + {{ $currentVersion ?? 'unknown' }} + + + {{ __('Status') }} + {{ $this->updateStatusLabel }} + + + {{ __('Last Checked') }} + {{ $lastCheckedAt ?? __('Not checked') }} + + + + @if (! empty($recentChanges)) + + {{ __('Recent Changes') }} + + @foreach ($recentChanges as $change) + {{ $change }} + @endforeach + + + @endif + + + + {{ __('Check for updates') }} + + + + {{ __('Upgrade now') }} + + + + @if ($jabaliOutput !== null) + + + {{ $jabaliOutputTitle ?? __('Jabali Update Output') }} + @if ($jabaliOutputAt) + + {{ __('Last run at :time', ['time' => $jabaliOutputAt]) }} + + @endif + + + {{ $jabaliOutput }} + + + @endif + + {{ $this->table }} + @if ($refreshOutputAt) + + + {{ $refreshOutputTitle ?? __('System Update Output') }} + + {{ __('Last run at :time', ['time' => $refreshOutputAt]) }} + + + + @php + $outputLines = is_array($refreshOutput) ? $refreshOutput : [$refreshOutput]; + @endphp + {{ implode("\n", $outputLines) }} + + + @endif +
{{ implode("\n", $refreshOutput) }}
{{ $jabaliOutput }}
{{ implode("\n", $outputLines) }}