['name' => 'Nginx', 'description' => 'Web Server', 'icon' => 'globe'], 'mariadb' => ['name' => 'MariaDB', 'description' => 'Database Server', 'icon' => 'database'], 'redis-server' => ['name' => 'Redis', 'description' => 'Cache Server', 'icon' => 'bolt'], 'postfix' => ['name' => 'Postfix', 'description' => 'Mail Transfer Agent', 'icon' => 'envelope'], 'dovecot' => ['name' => 'Dovecot', 'description' => 'IMAP/POP3 Server', 'icon' => 'inbox'], 'rspamd' => ['name' => 'Rspamd', 'description' => 'Spam Filter', 'icon' => 'shield'], 'clamav-daemon' => ['name' => 'ClamAV', 'description' => 'Antivirus Scanner', 'icon' => 'bug'], 'named' => ['name' => 'BIND9', 'description' => 'DNS Server', 'icon' => 'server'], 'opendkim' => ['name' => 'OpenDKIM', 'description' => 'DKIM Signing', 'icon' => 'key'], 'fail2ban' => ['name' => 'Fail2Ban', 'description' => 'Intrusion Prevention', 'icon' => 'lock'], 'ssh' => ['name' => 'SSH', 'description' => 'Secure Shell', 'icon' => 'terminal'], 'cron' => ['name' => 'Cron', 'description' => 'Task Scheduler', 'icon' => 'clock'], ]; protected ?array $managedServices = null; protected function getManagedServices(): array { if ($this->managedServices !== null) { return $this->managedServices; } $this->managedServices = []; foreach ($this->baseServices as $key => $config) { $this->managedServices[$key] = $config; if ($key === 'nginx') { foreach ($this->detectPhpFpmVersions() as $service => $phpConfig) { $this->managedServices[$service] = $phpConfig; } } } return $this->managedServices; } protected function detectPhpFpmVersions(): array { $phpServices = []; $output = []; exec('ls /lib/systemd/system/php*-fpm.service 2>/dev/null', $output); foreach ($output as $servicePath) { if (preg_match('/php([\d.]+)-fpm\.service$/', $servicePath, $matches)) { $version = $matches[1]; $serviceName = "php{$version}-fpm"; $phpServices[$serviceName] = [ 'name' => "PHP {$version} FPM", 'description' => 'PHP FastCGI Process Manager', 'icon' => 'code', ]; } } uksort($phpServices, function ($a, $b) { preg_match('/php([\d.]+)-fpm/', $a, $matchA); preg_match('/php([\d.]+)-fpm/', $b, $matchB); return version_compare($matchB[1] ?? '0', $matchA[1] ?? '0'); }); return $phpServices; } public function getTitle(): string|Htmlable { return __('Service Manager'); } public function getAgent(): AgentClient { return $this->agent ??= new AgentClient; } public function mount(): void { $this->loadServices(); } public function loadServices(): void { $managedServices = $this->getManagedServices(); try { $result = $this->getAgent()->send('service.list', [ 'services' => array_keys($managedServices), ]); if ($result['success'] ?? false) { $this->services = []; foreach ($result['services'] ?? [] as $name => $status) { $config = $managedServices[$name] ?? [ 'name' => ucfirst($name), 'description' => '', 'icon' => 'cog', ]; $this->services[$name] = array_merge($config, [ 'service' => $name, 'is_active' => $status['is_active'] ?? false, 'is_enabled' => $status['is_enabled'] ?? false, 'status' => $status['status'] ?? 'unknown', ]); } } } catch (Exception $e) { Notification::make()->title(__('Error loading services'))->body($e->getMessage())->danger()->send(); } } public function table(Table $table): Table { return $table ->records(fn () => array_values($this->services)) ->columns([ TextColumn::make('name') ->label(__('Service')) ->icon(fn (array $record): string => match ($record['icon'] ?? 'cog') { 'globe' => 'heroicon-o-globe-alt', 'code' => 'heroicon-o-code-bracket', 'database' => 'heroicon-o-circle-stack', 'bolt' => 'heroicon-o-bolt', 'envelope' => 'heroicon-o-envelope', 'inbox' => 'heroicon-o-inbox', 'shield' => 'heroicon-o-shield-check', 'server' => 'heroicon-o-server', 'key' => 'heroicon-o-key', 'lock' => 'heroicon-o-lock-closed', 'terminal' => 'heroicon-o-command-line', 'clock' => 'heroicon-o-clock', 'bug' => 'heroicon-o-bug-ant', default => 'heroicon-o-cog-6-tooth', }) ->iconColor(fn (array $record): string => $record['is_active'] ? 'success' : 'danger') ->description(fn (array $record): string => $record['description'] ?? '') ->weight('medium') ->searchable(), TextColumn::make('is_active') ->label(__('Status')) ->badge() ->formatStateUsing(fn (array $record): string => $record['is_active'] ? __('Running') : __('Stopped')) ->color(fn (array $record): string => $record['is_active'] ? 'success' : 'danger'), TextColumn::make('is_enabled') ->label(__('Boot')) ->badge() ->formatStateUsing(fn (array $record): string => $record['is_enabled'] ? __('Enabled') : __('Disabled')) ->color(fn (array $record): string => $record['is_enabled'] ? 'success' : 'warning'), ]) ->recordActions([ Action::make('start') ->label(__('Start')) ->icon('heroicon-o-play') ->color('success') ->size('sm') ->visible(fn (array $record): bool => ! $record['is_active']) ->action(fn (array $record) => $this->executeServiceAction($record['service'], 'start')), Action::make('stop') ->label(__('Stop')) ->icon('heroicon-o-stop') ->color('danger') ->size('sm') ->visible(fn (array $record): bool => $record['is_active']) ->requiresConfirmation() ->modalHeading(__('Stop Service')) ->modalDescription(fn (array $record): string => __('Are you sure you want to stop :service? This may affect running websites and services.', ['service' => $record['name']])) ->modalSubmitActionLabel(__('Stop Service')) ->action(fn (array $record) => $this->executeServiceAction($record['service'], 'stop')), Action::make('restart') ->label(fn (array $record): string => $this->shouldReloadService($record['service']) ? __('Reload') : __('Restart')) ->icon('heroicon-o-arrow-path') ->color('info') ->size('sm') ->visible(fn (array $record): bool => $record['is_active']) ->action(fn (array $record) => $this->executeServiceAction( $record['service'], $this->shouldReloadService($record['service']) ? 'reload' : 'restart' )), Action::make('enable') ->label(__('Enable')) ->icon('heroicon-o-check') ->color('gray') ->size('sm') ->visible(fn (array $record): bool => ! $record['is_enabled']) ->action(fn (array $record) => $this->executeServiceAction($record['service'], 'enable')), Action::make('disable') ->label(__('Disable')) ->icon('heroicon-o-x-mark') ->color('warning') ->size('sm') ->visible(fn (array $record): bool => $record['is_enabled']) ->requiresConfirmation() ->modalHeading(__('Disable Service')) ->modalDescription(fn (array $record): string => __("Are you sure you want to disable :service? It won't start automatically on boot.", ['service' => $record['name']])) ->modalSubmitActionLabel(__('Disable Service')) ->action(fn (array $record) => $this->executeServiceAction($record['service'], 'disable')), ]) ->headerActions([ Action::make('refresh') ->label(__('Refresh')) ->icon('heroicon-o-arrow-path') ->color('gray') ->action(function () { $this->loadServices(); $this->resetTable(); Notification::make()->title(__('Services refreshed'))->success()->duration(1500)->send(); }), ]) ->emptyStateHeading(__('No services found')) ->emptyStateDescription(__('Unable to load system services')) ->emptyStateIcon('heroicon-o-cog-6-tooth') ->striped(); } public function getTableRecordKey(Model|array $record): string { return is_array($record) ? $record['service'] : $record->getKey(); } protected function executeServiceAction(string $service, string $action): void { try { $result = $this->getAgent()->send("service.{$action}", [ 'service' => $service, ]); if ($result['success'] ?? false) { $notificationTitle = match ($action) { 'start' => __(':service started', ['service' => ucfirst($service)]), 'stop' => __(':service stopped', ['service' => ucfirst($service)]), 'restart' => __(':service restarted', ['service' => ucfirst($service)]), 'reload' => __(':service reloaded', ['service' => ucfirst($service)]), 'enable' => __(':service enabled', ['service' => ucfirst($service)]), 'disable' => __(':service disabled', ['service' => ucfirst($service)]), default => ucfirst($service).' '.$action }; $actionPast = match ($action) { 'start' => 'started', 'stop' => 'stopped', 'restart' => 'restarted', 'reload' => 'reloaded', 'enable' => 'enabled', 'disable' => 'disabled', default => $action }; Notification::make() ->title($notificationTitle) ->success() ->send(); AuditLog::logServiceAction($actionPast, $service); $this->loadServices(); $this->resetTable(); } else { throw new Exception($result['error'] ?? $result['message'] ?? __('Unknown error')); } } catch (Exception $e) { Notification::make() ->title(__('Action failed')) ->body($e->getMessage()) ->danger() ->send(); } } protected function getHeaderActions(): array { return [ $this->getTourAction(), ]; } protected function shouldReloadService(string $service): bool { if ($service === 'nginx') { return true; } return preg_match('/^php(\d+\.\d+)?-fpm$/', $service) === 1; } }