*/ public array $diagnostics = []; public function mount(): void { $this->fillForm(); $this->runDiagnostics(); } protected function fillForm(): void { $paths = $this->autoDetectPaths(); $this->data = [ 'alert_email_enabled' => $this->getSettingBool('alert_email_enabled'), 'alert_email_address' => $this->getSetting('alert_email_address', ''), 'alert_discord_enabled' => $this->getSettingBool('alert_discord_enabled'), 'alert_discord_webhook_url' => $this->getSetting('alert_discord_webhook_url', ''), 'discord_webhook_ranks' => json_decode($this->getSetting('discord_webhook_ranks', '[]'), true) ?? [], 'alert_emulator_enabled' => $this->getSettingBool('alert_emulator_enabled', true), 'alert_ddos_enabled' => $this->getSettingBool('alert_ddos_enabled', true), 'alert_ddos_threshold' => (int) $this->getSetting('alert_ddos_threshold', '100'), 'alert_ddos_auto_block' => $this->getSettingBool('alert_ddos_auto_block'), 'alert_errors_enabled' => $this->getSettingBool('alert_errors_enabled', true), 'alert_error_threshold' => $this->getSetting('alert_error_threshold', '10'), 'alert_min_severity' => $this->getSetting('alert_min_severity', AlertSeverity::ERROR->value), 'emulator_github_url' => $this->getSetting('emulator_github_url', ''), 'emulator_source_repo' => $this->getSetting('emulator_source_repo', ''), 'emulator_jar_direct_url' => $this->getSetting('emulator_jar_direct_url', ''), 'emulator_jar_path' => $this->getSetting('emulator_jar_path', $paths['emulator_jar_path']), 'emulator_source_path' => $this->getSetting('emulator_source_path', $paths['emulator_source_path']), 'emulator_service_name' => $this->getSetting('emulator_service_name', 'arcturus'), 'emulator_github_branch' => $this->getSetting('emulator_github_branch', 'main'), 'emulator_database_host' => $this->getSetting('emulator_database_host', '127.0.0.1'), 'emulator_database_port' => $this->getSetting('emulator_database_port', '3306'), 'emulator_database_name' => $this->getSetting('emulator_database_name', ''), 'emulator_database_username' => $this->getSetting('emulator_database_username', ''), 'emulator_database_password' => $this->getSetting('emulator_database_password', ''), 'emulator_version' => $this->getSetting('emulator_version', 'Onbekend'), 'hotel_alert_message' => '', ]; } public function form(Schema $schema): Schema { return $schema ->components([ Section::make(__('commandocentrum.live_status')) ->description(__('commandocentrum.live_status_desc')) ->icon('heroicon-o-heart') ->columns(4) ->schema([ Placeholder::make('online_users') ->label(__('commandocentrum.online')) ->content(fn (): HtmlString => $this->getCardHtml(__('commandocentrum.online'), $this->getOnlineUsersCount(), '#22c55e', 'heroicon-o-users')), Placeholder::make('emulator_status') ->label(__('commandocentrum.emulator')) ->content(fn (): HtmlString => $this->getCardHtml(__('commandocentrum.emulator'), $this->getEmulatorStatusText(), $this->getEmulatorStatusColor(), 'heroicon-o-server')), Placeholder::make('database_status') ->label(__('commandocentrum.database')) ->content(fn (): HtmlString => $this->getCardHtml(__('commandocentrum.database'), $this->isDatabaseOnline() ? __('commandocentrum.online') : __('commandocentrum.offline'), $this->isDatabaseOnline() ? '#22c55e' : '#ef4444', 'heroicon-o-circle-stack')), Placeholder::make('server_load') ->label(__('commandocentrum.load')) ->content(fn (): HtmlString => $this->getCardHtml(__('commandocentrum.load'), $this->getServerLoad(), '#3b82f6', 'heroicon-o-cpu-chip')), ]), Section::make(__('commandocentrum.server_info')) ->description(__('commandocentrum.server_info_desc')) ->icon('heroicon-o-information-circle') ->schema([ Placeholder::make('server_info') ->label('') ->content(fn () => $this->renderServerInfoView()), ]), Section::make(__('commandocentrum.system_health')) ->description(__('commandocentrum.system_health_desc')) ->icon('heroicon-o-heart') ->afterHeader([ Action::make('refresh_diagnostics') ->label(__('commandocentrum.refresh')) ->icon('heroicon-o-arrow-path') ->color('info') ->action('refreshDiagnostics'), ]) ->schema([ Placeholder::make('diagnostics') ->label('') ->content(fn (): HtmlString => $this->renderDiagnostics()), ]), Section::make(__('commandocentrum.hotel_status')) ->description(__('commandocentrum.hotel_status_desc')) ->icon('heroicon-o-building-office') ->schema([ Placeholder::make('hotel_status') ->label('') ->content(fn () => $this->renderHotelStatusView()), ]), Section::make(__('commandocentrum.hotel_alert')) ->description(__('commandocentrum.hotel_alert_desc')) ->icon('heroicon-o-megaphone') ->schema([ Placeholder::make('alert_form') ->label('') ->content(fn () => view('filament.components.commandocentrum.alert-form')), ]), Section::make(__('commandocentrum.emulator_logs')) ->description(__('commandocentrum.emulator_logs_desc')) ->icon('heroicon-o-document-text') ->columnSpanFull() ->schema([ Placeholder::make('emulator_logs') ->label('') ->content(fn () => view('filament.components.emulator-log-viewer')), ]), Section::make(__('commandocentrum.emulator_control')) ->description(__('commandocentrum.emulator_control_desc')) ->icon('heroicon-o-server') ->columns(3) ->afterHeader([ Action::make('start_emulator') ->label(__('commandocentrum.start')) ->icon('heroicon-o-play') ->color('success') ->action('startEmulator'), Action::make('stop_emulator') ->label(__('commandocentrum.stop')) ->icon('heroicon-o-stop') ->color('danger') ->action('stopEmulator'), Action::make('restart_emulator') ->label(__('commandocentrum.restart')) ->icon('heroicon-o-arrow-path') ->color('warning') ->action('restartEmulator'), Action::make('check_emulator') ->label(__('commandocentrum.check')) ->icon('heroicon-o-check-circle') ->color('info') ->action('checkEmulator'), ]) ->schema([ Placeholder::make('emulator_info') ->label('') ->content(fn () => $this->renderEmulatorInfoView()), ]), Section::make(__('commandocentrum.clothing_sync')) ->description(__('commandocentrum.clothing_sync_desc')) ->icon('heroicon-o-user') ->afterHeader([ Action::make('sync_clothing') ->label('🔄 ' . __('commandocentrum.sync')) ->color('success') ->action('syncClothing'), ]) ->schema([ Placeholder::make('clothing_status') ->label('') ->content(fn () => $this->renderClothingStatusView()), ]), Section::make(__('commandocentrum.notifications')) ->description(__('commandocentrum.notifications_desc')) ->icon('heroicon-o-bell') ->columns(2) ->afterHeader([ Action::make('save_alerts') ->label(__('commandocentrum.save')) ->color('primary') ->action('saveAlerts'), Action::make('test_discord') ->label(__('commandocentrum.test_discord')) ->color('info') ->action('testDiscord'), ]) ->schema([ Toggle::make('alert_email_enabled') ->label(__('commandocentrum.email_notifications')), TextInput::make('alert_email_address') ->label(__('commandocentrum.email_address')) ->email() ->columnSpanFull(), Toggle::make('alert_discord_enabled') ->label(__('commandocentrum.discord_notifications')), TextInput::make('alert_discord_webhook_url') ->label(__('commandocentrum.webhook_url')) ->columnSpanFull(), Select::make('discord_webhook_ranks') ->label(__('commandocentrum.discord_ranks')) ->multiple() ->options(fn () => WebsitePermission::query()->pluck('permission', 'min_rank')->mapWithKeys(fn ($perm, $rank) => [$rank => "Rank {$rank} ({$perm})"])->toArray()) ->helperText(__('commandocentrum.discord_ranks_helper')), ]), Section::make(__('commandocentrum.social_login')) ->description(__('commandocentrum.social_login_desc')) ->icon('heroicon-o-user-circle') ->schema([ Toggle::make('social_login_google_enabled') ->label(__('commandocentrum.google_login')) ->helperText(__('commandocentrum.google_login_helper')), TextInput::make('social_login_google_client_id') ->label(__('commandocentrum.google_client_id')) ->helperText(__('commandocentrum.google_client_id_helper')), TextInput::make('social_login_google_client_secret') ->label(__('commandocentrum.google_client_secret')) ->type('password'), Toggle::make('social_login_discord_enabled') ->label(__('commandocentrum.discord_login')) ->helperText(__('commandocentrum.discord_login_helper')), TextInput::make('social_login_discord_client_id') ->label(__('commandocentrum.discord_client_id')) ->helperText(__('commandocentrum.discord_client_id_helper')), TextInput::make('social_login_discord_client_secret') ->label(__('commandocentrum.discord_client_secret')) ->type('password'), Toggle::make('social_login_github_enabled') ->label(__('commandocentrum.github_login')) ->helperText(__('commandocentrum.github_login_helper')), TextInput::make('social_login_github_client_id') ->label(__('commandocentrum.github_client_id')) ->helperText(__('commandocentrum.github_client_id_helper')), TextInput::make('social_login_github_client_secret') ->label(__('commandocentrum.github_client_secret')) ->type('password'), ]) ->columns(2), Section::make(__('commandocentrum.staff_activity')) ->description(__('commandocentrum.staff_activity_desc')) ->icon('heroicon-o-user-group') ->schema([ Placeholder::make('staff_activity') ->label('') ->content(fn () => $this->renderStaffActivityView()), ]), ]); } private function renderServerInfoView(): View { $load = sys_getloadavg(); return view('filament.components.commandocentrum.server-info', [ 'phpVersion' => phpversion(), 'laravelVersion' => app()->version(), 'memoryUsage' => round(memory_get_usage() / 1024 / 1024, 2), 'memoryLimit' => ini_get('memory_limit'), 'diskUsage' => $this->runCommand("df -h /var/www | tail -1 | awk '{print \$3 \"/\" \$2}'") ?: 'N/B', 'uptime' => $this->runCommand('uptime -p 2>/dev/null') ?: 'N/B', 'load1' => $load ? number_format($load[0], 2) : 'N/A', 'load5' => $load ? number_format($load[1], 2) : 'N/A', 'load15' => $load ? number_format($load[2], 2) : 'N/A', ]); } private function renderHotelStatusView(): View { $serviceName = $this->getSetting('emulator_service_name', 'emulator'); $serviceStatus = $this->runCommand('systemctl is-active ' . escapeshellarg($serviceName) . ' 2>/dev/null') ?: 'inactive'; $serviceColor = $serviceStatus === 'active' ? '#22c55e' : '#ef4444'; $nitroClientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client'); $nitroRendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer'); $nitroWebroot = $this->getSetting('nitro_webroot', '/var/www/Client'); $clientCommit = $this->getGitCommit($nitroClientPath); $rendererCommit = $this->getGitCommit($nitroRendererPath); $clientExists = $this->checkPathExists($nitroClientPath); $rendererExists = $this->checkPathExists($nitroRendererPath); $clientColor = $clientExists ? '#22c55e' : '#ef4444'; $rendererColor = $rendererExists ? '#22c55e' : '#ef4444'; $clientText = $clientExists ? '✓ ' . substr($clientCommit, 0, 7) : '✗ ' . __('commandocentrum.not_found'); $rendererText = $rendererExists ? '✓ ' . substr($rendererCommit, 0, 7) : '✗ ' . __('commandocentrum.not_found'); $webrootText = $rendererExists ? '✓ ' . basename($nitroWebroot) : '✗ ' . __('commandocentrum.not_found'); $webrootColor = $rendererExists ? '#22c55e' : '#ef4444'; return view('filament.components.commandocentrum.hotel-status', [ 'serviceStatus' => $serviceStatus, 'serviceColor' => $serviceColor, 'onlineUsers' => $this->getOnlineUsersCount(), 'emulatorStatus' => $this->getEmulatorStatusText(), 'dbStatus' => $this->isDatabaseOnline() ? __('commandocentrum.online') : __('commandocentrum.offline'), 'dbColor' => $this->isDatabaseOnline() ? '#22c55e' : '#ef4444', 'clientExists' => $clientExists, 'clientColor' => $clientColor, 'clientText' => $clientText, 'rendererExists' => $rendererExists, 'rendererColor' => $rendererColor, 'rendererText' => $rendererText, 'webrootText' => $webrootText, 'webrootColor' => $webrootColor, ]); } private function renderEmulatorInfoView(): View { $status = $this->getEmulatorStatusText(); return view('filament.components.commandocentrum.emulator-info', [ 'version' => $this->getSetting('emulator_version', 'Onbekend'), 'serviceName' => $this->getSetting('emulator_service_name', 'arcturus'), 'status' => $status, 'color' => $status === 'Online' ? '#22c55e' : '#ef4444', ]); } private function renderClothingStatusView(): View { try { $count = DB::table('catalog_clothing')->count(); } catch (Exception) { $count = 0; } return view('filament.components.commandocentrum.clothing-status', [ 'clothingCount' => $count, ]); } private function renderStaffActivityView(): View { try { $activities = StaffActivity::with('user:id,username,look') ->orderByDesc('created_at') ->limit(20) ->get(); } catch (Exception) { $activities = collect(); } return view('filament.components.commandocentrum.staff-activity', [ 'activities' => $activities, ]); } private function getSetting(string $key, string $default = ''): string { try { return app(SettingsService::class)->getOrDefault($key, $default); } catch (Exception) { return $default; } } private function getSettingBool(string $key, bool $default = false): bool { try { $value = app(SettingsService::class)->getOrDefault($key, $default ? '1' : '0'); return in_array($value, ['1', 'true', 'yes'], true); } catch (Exception) { return $default; } } private function getCurrentSiteUrl(): string { try { return config('app.url', 'https://epicnabbo.nl'); } catch (Exception) { return 'https://epicnabbo.nl'; } } private function autoDetectPaths(): array { $autoDetect = AutoDetectService::getInstance(); $autoDetect->clearCache(); return [ 'nitro_client_path' => $autoDetect->detectNitroClientPath(), 'nitro_renderer_path' => $autoDetect->detectNitroRendererPath(), 'nitro_build_path' => $autoDetect->detectNitroBuildPath(), 'nitro_webroot' => $autoDetect->detectNitroWebroot(), 'gamedata_path' => $autoDetect->detectGamedataPath(), 'emulator_jar_path' => $autoDetect->detectEmulatorJarPath(), 'emulator_source_path' => $autoDetect->detectEmulatorSourcePath(), ]; } private function getCardHtml(string $label, string $value, string $color, string $icon): HtmlString { $iconSvg = match ($icon) { 'heroicon-o-users' => '', 'heroicon-o-server' => '', 'heroicon-o-circle-stack' => '', 'heroicon-o-cpu-chip' => '', default => '', }; return new HtmlString(<<
{$iconSvg}
{$label}
{$value}
HTML); } private function getOnlineUsersCount(): string { try { return (string) DB::connection('mysql')->table('users')->where('online', '=', '1')->count(); } catch (Exception) { return 'N/B'; } } private function getEmulatorStatusText(): string { try { $rcon = new RconService; if ($rcon->isConnected()) { return 'Online'; } $response = Http::timeout(2)->get('http://127.0.0.1:3000/api/status'); return $response->successful() ? 'Online' : 'Offline'; } catch (Exception) { return 'Offline'; } } private function getEmulatorStatusColor(): string { return $this->getEmulatorStatusText() === 'Online' ? '#22c55e' : '#ef4444'; } private function isDatabaseOnline(): bool { try { DB::connection('mysql')->select('SELECT 1'); return true; } catch (Exception) { return false; } } private function getServerLoad(): string { try { $load = sys_getloadavg(); return $load ? number_format($load[0], 2) : 'N/B'; } catch (Exception) { return 'N/B'; } } private function getEmulatorBranchesHtml(): string { $githubUrl = $this->getSetting('emulator_github_url', ''); $currentBranch = $this->data['emulator_github_branch'] ?? 'main'; $branches = app(GitHubService::class)->getBranches($githubUrl); $html = ''; foreach ($branches as $branch) { $selected = $branch === $currentBranch ? 'selected' : ''; $html .= ''; } return $html; } private function getNitroBranchesHtml(): string { $githubUrl = $this->getSetting('nitro_github_url', ''); $currentBranch = $this->data['nitro_github_branch'] ?? 'main'; $branches = app(GitHubService::class)->getBranches($githubUrl); $html = ''; foreach ($branches as $branch) { $selected = $branch === $currentBranch ? 'selected' : ''; $html .= ''; } return $html; } private function getRemoteCommit(string $githubUrl, string $branch = 'main'): string { $commit = app(GitHubService::class)->getLatestCommit($githubUrl, $branch); return $commit ?? 'N/A'; } private function getGitCommit(string $path): string { if (! $this->fileExists($path)) { return 'N/A'; } $subdirs = ['', 'Emulator', 'emulator', 'src', 'client']; foreach ($subdirs as $subdir) { $fullPath = $subdir !== '' ? $path . '/' . $subdir : $path; $gitPath = $fullPath . '/.git'; if ($this->dirExists($gitPath)) { $headFile = $gitPath . '/HEAD'; $headContent = $this->readFile($headFile); if ($headContent) { if (str_contains($headContent, 'ref:')) { preg_match('/ref: refs\/heads\/(\S+)/', $headContent, $matches); if (isset($matches[1])) { $branchRef = $gitPath . '/refs/heads/' . $matches[1]; $branchContent = $this->readFile($branchRef); if ($branchContent) { return trim($branchContent); } } } else { return trim($headContent); } } } } return 'N/A'; } private function checkPathExists(string $path): bool { return $this->fileExists($path); } public function sendHotelAlert(): void { $result = app(EmulatorControlAction::class)->sendAlert($this->data['hotel_alert_message'] ?? ''); $this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'], $result['success'] ? 'success' : 'danger'); if ($result['success']) { $this->data['hotel_alert_message'] = ''; } } public function startEmulator(): void { $result = app(EmulatorControlAction::class)->start(); $this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'], $result['success'] ? 'success' : 'danger'); } public function stopEmulator(): void { $result = app(EmulatorControlAction::class)->stop(); $this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'], $result['success'] ? 'success' : 'danger'); } public function restartEmulator(): void { $result = app(EmulatorControlAction::class)->restart(); $this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'], $result['success'] ? 'success' : 'danger'); } public function checkEmulator(): void { try { Artisan::call('monitor:emulator', ['--notify-online' => true]); $rconService = new RconService; if ($rconService->isConnected()) { $this->notify(__('commandocentrum.success'), __('commandocentrum.emulator_online'), 'success'); } else { $this->notify(__('commandocentrum.warning'), __('commandocentrum.emulator_unreachable'), 'warning'); } $this->fillForm(); } catch (Exception $e) { $this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger'); } } public function saveEmulator(): void { try { $settings = app(SettingsService::class); $settings->set('emulator_github_url', $this->data['emulator_github_url'] ?? ''); $settings->set('emulator_jar_direct_url', $this->data['emulator_jar_direct_url'] ?? ''); $settings->set('emulator_jar_path', $this->data['emulator_jar_path'] ?? '/root/emulator'); $settings->set('emulator_source_repo', $this->data['emulator_source_repo'] ?? ''); $settings->set('emulator_source_path', $this->data['emulator_source_path'] ?? '/var/www/emulator-source'); $settings->set('emulator_github_branch', $this->data['emulator_github_branch'] ?? 'main'); $settings->set('emulator_database_host', $this->data['emulator_database_host'] ?? '127.0.0.1'); $settings->set('emulator_database_name', $this->data['emulator_database_name'] ?? ''); $settings->set('emulator_service_name', $this->data['emulator_service_name'] ?? 'arcturus'); $this->notify(__('commandocentrum.success'), __('commandocentrum.emulator_settings_saved'), 'success'); } catch (Exception $e) { $this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger'); } } public function syncClothing(): void { try { $catalogService = app(CatalogService::class); $result = $catalogService->syncCatalogClothing(); $message = '👔 ' . __('commandocentrum.clothing_items') . ':' . PHP_EOL; $message .= '• Toegevoegd: ' . $result['inserted'] . PHP_EOL; $message .= '• Totaal: ' . $result['total']; $this->notify(__('commandocentrum.success'), $message, 'success'); } catch (Exception $e) { $this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger'); } } public function saveAlerts(): void { try { $settings = app(SettingsService::class); $settings->set('alert_email_enabled', ($this->data['alert_email_enabled'] ?? false) ? '1' : '0'); $settings->set('alert_email_address', $this->data['alert_email_address'] ?? ''); $settings->set('alert_discord_enabled', ($this->data['alert_discord_enabled'] ?? false) ? '1' : '0'); $settings->set('alert_discord_webhook_url', $this->data['alert_discord_webhook_url'] ?? ''); $settings->set('discord_webhook_ranks', json_encode($this->data['discord_webhook_ranks'] ?? [])); $this->notify(__('commandocentrum.success'), __('commandocentrum.alerts_saved'), 'success'); } catch (Exception $e) { $this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger'); } } public function testDiscord(): void { try { $webhookUrl = $this->data['alert_discord_webhook_url'] ?? ''; if (empty($webhookUrl)) { $this->notify(__('commandocentrum.error'), __('commandocentrum.webhook_empty'), 'danger'); return; } Http::post($webhookUrl, ['content' => '✅ Test van Atom CMS Commandocentrum']); $this->notify(__('commandocentrum.success'), __('commandocentrum.test_sent'), 'success'); } catch (Exception $e) { $this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger'); } } private function notify(string $title, string $message, string $color): void { Notification::make() ->title($title) ->body($message) ->color($color) ->send(); } public function refreshDiagnostics(): void { $this->runDiagnostics(); $this->notify(__('commandocentrum.success'), __('commandocentrum.diagnostics_refreshed'), 'success'); } private function runDiagnostics(): void { $runner = app(DiagnosticRunner::class); $this->diagnostics = array_map(fn (DiagnosticResult $r) => [ 'name' => $r->name, 'status' => $r->status, 'message' => $r->message, 'fix' => $r->fix, ], $runner->runAll()); } private function renderDiagnostics(): HtmlString { if ($this->diagnostics === []) { $this->runDiagnostics(); } $errors = array_filter($this->diagnostics, fn ($r) => $r['status'] === 'error'); $warnings = array_filter($this->diagnostics, fn ($r) => $r['status'] === 'warning'); $ok = array_filter($this->diagnostics, fn ($r) => $r['status'] === 'ok'); $errorCount = count($errors); $warningCount = count($warnings); $okCount = count($ok); $overallStatus = $errorCount > 0 ? 'error' : ($warningCount > 0 ? 'warning' : 'ok'); $overallColor = match ($overallStatus) { 'error' => '#ef4444', 'warning' => '#f59e0b', default => '#22c55e', }; $overallLabel = match ($overallStatus) { 'error' => __('commandocentrum.critical_issues'), 'warning' => __('commandocentrum.warnings'), default => __('commandocentrum.healthy'), }; $html = '
'; // Summary cards $html .= '
'; $html .= $this->getSummaryCardHtml(__('commandocentrum.healthy'), $okCount, '#22c55e', 'heroicon-o-check-circle'); $html .= $this->getSummaryCardHtml(__('commandocentrum.warnings'), $warningCount, '#f59e0b', 'heroicon-o-exclamation-triangle'); $html .= $this->getSummaryCardHtml(__('commandocentrum.errors'), $errorCount, '#ef4444', 'heroicon-o-x-circle'); $html .= '
'; // Overall status banner $html .= '
'; $html .= '
'; $html .= '' . __('commandocentrum.system_status') . ': {$overallLabel}'; $html .= '
'; // Detailed results if ($errorCount > 0 || $warningCount > 0) { $html .= '
'; foreach ($this->diagnostics as $result) { if ($result['status'] === 'ok') { continue; } $color = $result['status'] === 'error' ? '#ef4444' : '#f59e0b'; $icon = $result['status'] === 'error' ? '' : ''; $html .= '
'; $html .= '
' . $icon . '
'; $html .= '
'; $html .= '
' . e($result['name']) . '
'; $html .= '
' . e($result['message']) . '
'; if ($result['fix']) { $html .= '
💡 ' . e($result['fix']) . '
'; } $html .= '
'; } $html .= '
'; } $html .= '
'; return new HtmlString($html); } private function getSummaryCardHtml(string $label, int $count, string $color, string $icon): string { $iconSvg = match ($icon) { 'heroicon-o-check-circle' => '', 'heroicon-o-exclamation-triangle' => '', 'heroicon-o-x-circle' => '', default => '', }; return <<
{$iconSvg}
{$label}
{$count}
HTML; } private function runCommand(string $command, int $timeout = 10): ?string { $result = Process::timeout($timeout)->run($command); return $result->successful() ? trim($result->output()) : null; } private function fileExists(string $path): bool { return $this->runCommand('test -e ' . escapeshellarg($path) . ' && echo yes') === 'yes'; } private function dirExists(string $path): bool { return $this->runCommand('test -d ' . escapeshellarg($path) . ' && echo yes') === 'yes'; } private function readFile(string $path): ?string { return $this->runCommand('cat ' . escapeshellarg($path) . ' 2>/dev/null'); } }