Files
jabali-panel/app/Filament/Admin/Pages/Services.php
2026-02-02 03:11:45 +02:00

329 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Admin\Pages;
use App\Models\AuditLog;
use App\Services\Agent\AgentClient;
use BackedEnum;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Model;
class Services extends Page implements HasActions, HasForms, HasTable
{
use InteractsWithActions;
use InteractsWithForms;
use InteractsWithTable;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-cog-6-tooth';
protected static ?int $navigationSort = 10;
public static function getNavigationLabel(): string
{
return __('Services');
}
protected string $view = 'filament.admin.pages.services';
public array $services = [];
public ?string $selectedService = null;
protected ?AgentClient $agent = null;
protected array $baseServices = [
'nginx' => ['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'),
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 [
];
}
protected function shouldReloadService(string $service): bool
{
if ($service === 'nginx') {
return true;
}
return preg_match('/^php(\d+\.\d+)?-fpm$/', $service) === 1;
}
}