diff --git a/app/Actions/Commandocentrum/EmulatorControlAction.php b/app/Actions/Commandocentrum/EmulatorControlAction.php new file mode 100755 index 0000000..ee69904 --- /dev/null +++ b/app/Actions/Commandocentrum/EmulatorControlAction.php @@ -0,0 +1,87 @@ +settings->getOrDefault('emulator_service_name', 'arcturus'); + $result = Process::timeout(30)->run("systemctl start {$serviceName} 2>&1"); + + return [ + 'success' => $result->successful(), + 'message' => $result->successful() ? 'Emulator gestart!' : ($result->output() ?: 'Kon emulator niet starten'), + ]; + } + + public function stop(): array + { + $serviceName = $this->settings->getOrDefault('emulator_service_name', 'arcturus'); + $result = Process::timeout(30)->run("systemctl stop {$serviceName} 2>&1"); + + return [ + 'success' => $result->successful(), + 'message' => $result->successful() ? 'Emulator gestopt!' : ($result->output() ?: 'Kon emulator niet stoppen'), + ]; + } + + public function restart(): array + { + $serviceName = $this->settings->getOrDefault('emulator_service_name', 'arcturus'); + $result = Process::timeout(60)->run("systemctl restart {$serviceName} 2>&1"); + + return [ + 'success' => $result->successful(), + 'message' => $result->successful() ? 'Emulator herstart!' : ($result->output() ?: 'Kon emulator niet herstarten'), + ]; + } + + public function sendAlert(string $message): array + { + if (empty($message)) { + return ['success' => false, 'message' => 'Bericht mag niet leeg zijn']; + } + + app(RconService::class)->sendCommand('alert', ['message' => $message]); + + return ['success' => true, 'message' => 'Alert verstuurd naar alle gebruikers!']; + } + + public function build(): array + { + return $this->updateService->buildFromSource(); + } + + public function update(): array + { + return $this->updateService->updateEmulator(); + } + + public function runSqlUpdates(): array + { + return $this->updateService->runSqlUpdates(); + } + + public function getBackups(): array + { + return $this->updateService->getBackupList(); + } + + public function restoreBackup(string $backupName): array + { + return $this->updateService->restoreBackup($backupName); + } +} diff --git a/app/Actions/Commandocentrum/NitroControlAction.php b/app/Actions/Commandocentrum/NitroControlAction.php new file mode 100755 index 0000000..9159913 --- /dev/null +++ b/app/Actions/Commandocentrum/NitroControlAction.php @@ -0,0 +1,119 @@ +runGitPull($clientPath, $branch); + $this->runGitPull($rendererPath, $branch); + + return ['success' => true, 'message' => 'Nitro bijgewerkt van GitHub']; + } + + public function build(string $clientPath, string $rendererPath, string $branch): array + { + $this->runGitPull($clientPath, $branch); + $this->runGitPull($rendererPath, $branch); + + Process::timeout(120)->run('cd ' . escapeshellarg($clientPath) . ' && sudo -u www-data npm install 2>&1'); + Process::timeout(120)->run('cd ' . escapeshellarg($rendererPath) . ' && sudo -u www-data npm install 2>&1'); + + $exitCode = Artisan::call('build:theme'); + + return [ + 'success' => $exitCode === 0, + 'message' => $exitCode === 0 ? 'Nitro build succesvol!' : 'Build gestart - controleer handmatig', + ]; + } + + public function generateConfigs(string $siteUrl, string $webroot, string $gamedataPath): array + { + if (! filter_var($siteUrl, FILTER_VALIDATE_URL)) { + return ['success' => false, 'message' => 'Voer een geldige URL in']; + } + + $existingConfigs = $this->readExistingConfigs($webroot, $gamedataPath); + + $exitCode = Artisan::call('app:generate-nitro-configs', ['--site-url' => $siteUrl]); + + if ($existingConfigs !== [] && $exitCode === 0) { + $this->mergeExistingConfigs($webroot, $existingConfigs); + } + + $this->settings->set('nitro_last_checked', now()->toIso8601String()); + + return [ + 'success' => $exitCode === 0, + 'message' => $exitCode === 0 ? 'Configs gegenereerd & bestaande instellingen behouden!' : 'Config gegenereerd (controleer handmatig)', + ]; + } + + private function runGitPull(string $path, string $branch): void + { + Process::timeout(60)->run('cd ' . escapeshellarg($path) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1'); + } + + private function readExistingConfigs(string $webroot, string $gamedataPath): array + { + $configs = []; + $files = ['renderer-config.json', 'ui-config.json', 'UITexts.json']; + + foreach ($files as $file) { + $path = $webroot . '/' . $file; + $content = @file_get_contents($path); + if ($content) { + $decoded = json_decode($content, true); + if (json_last_error() === JSON_ERROR_NONE) { + $configs[$file] = $decoded; + } + } + } + + if ($gamedataPath !== '') { + $gamedataConfigs = [ + 'ExternalTexts.json' => $gamedataPath . '/config/ExternalTexts.json', + 'FurnitureData.json' => $gamedataPath . '/config/FurnitureData.json', + 'ProductData.json' => $gamedataPath . '/config/ProductData.json', + 'FigureData.json' => $gamedataPath . '/config/FigureData.json', + ]; + + foreach ($gamedataConfigs as $key => $path) { + $content = @file_get_contents($path); + if ($content) { + $decoded = json_decode($content, true); + if (json_last_error() === JSON_ERROR_NONE) { + $configs['gamedata.' . $key] = $decoded; + } + } + } + } + + return $configs; + } + + private function mergeExistingConfigs(string $webroot, array $existingConfigs): void + { + if (isset($existingConfigs['renderer-config.json'])) { + $newPath = $webroot . '/renderer-config.json'; + $newContent = @file_get_contents($newPath); + $newConfig = json_decode($newContent, true); + + if ($newConfig && json_last_error() === JSON_ERROR_NONE) { + $merged = array_merge($existingConfigs['renderer-config.json'], $newConfig); + @file_put_contents($newPath, json_encode($merged, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } + } + } +} diff --git a/app/Filament/Pages/Monitoring/Commandocentrum.php b/app/Filament/Pages/Monitoring/Commandocentrum.php index 0774c5b..624b969 100755 --- a/app/Filament/Pages/Monitoring/Commandocentrum.php +++ b/app/Filament/Pages/Monitoring/Commandocentrum.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace App\Filament\Pages\Monitoring; +use App\Actions\Commandocentrum\EmulatorControlAction; +use App\Actions\Commandocentrum\NitroControlAction; use App\Enums\AlertSeverity; use App\Models\Miscellaneous\WebsitePermission; use App\Models\StaffActivity; @@ -1267,64 +1269,29 @@ final class Commandocentrum extends Page implements HasForms public function sendHotelAlert(): void { - try { - $message = $this->data['hotel_alert_message'] ?? ''; - if (empty($message)) { - $this->notify('Error', 'Bericht mag niet leeg zijn', 'danger'); - - return; - } - app(RconService::class)->sendCommand('alert', ['message' => $message]); - $this->notify('Success', 'Alert verstuurd naar alle gebruikers!', 'success'); + $result = app(EmulatorControlAction::class)->sendAlert($this->data['hotel_alert_message'] ?? ''); + $this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger'); + if ($result['success']) { $this->data['hotel_alert_message'] = ''; - } catch (Exception $e) { - $this->notify('Error', $e->getMessage(), 'danger'); } } public function startEmulator(): void { - try { - $serviceName = $this->getSetting('emulator_service_name', 'arcturus'); - $result = Process::timeout(30)->run("sudo systemctl start {$serviceName} 2>&1"); - if ($result->successful()) { - $this->notify('Success', 'Emulator gestart!', 'success'); - } else { - $this->notify('Error', $result->output() ?: 'Kon emulator niet starten', 'danger'); - } - } catch (Exception $e) { - $this->notify('Error', $e->getMessage(), 'danger'); - } + $result = app(EmulatorControlAction::class)->start(); + $this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger'); } public function stopEmulator(): void { - try { - $serviceName = $this->getSetting('emulator_service_name', 'arcturus'); - $result = Process::timeout(30)->run("sudo systemctl stop {$serviceName} 2>&1"); - if ($result->successful()) { - $this->notify('Success', 'Emulator gestopt!', 'success'); - } else { - $this->notify('Error', $result->output() ?: 'Kon emulator niet stoppen', 'danger'); - } - } catch (Exception $e) { - $this->notify('Error', $e->getMessage(), 'danger'); - } + $result = app(EmulatorControlAction::class)->stop(); + $this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger'); } public function restartEmulator(): void { - try { - $serviceName = $this->getSetting('emulator_service_name', 'arcturus'); - $result = Process::timeout(60)->run("sudo systemctl restart {$serviceName} 2>&1"); - if ($result->successful()) { - $this->notify('Success', 'Emulator herstart!', 'success'); - } else { - $this->notify('Error', $result->output() ?: 'Kon emulator niet herstarten', 'danger'); - } - } catch (Exception $e) { - $this->notify('Error', $e->getMessage(), 'danger'); - } + $result = app(EmulatorControlAction::class)->restart(); + $this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger'); } public function checkEmulator(): void @@ -1345,95 +1312,22 @@ final class Commandocentrum extends Page implements HasForms public function checkEmulatorUpdates(): void { - try { - $this->notify('Info', 'π¨ Emulator wordt gebouwd vanaf source...', 'info'); - - $updateService = app(EmulatorUpdateService::class); - $result = $updateService->buildFromSource(); - - if ($result['success'] ?? false) { - $this->notify('Success', $result['message'] ?? 'β Emulator gebouwd!', 'success'); - } else { - $this->notify('Error', $result['error'] ?? 'Build mislukt', 'danger'); - } - - Cache::forget('all_updates_check'); - } catch (Exception $e) { - $this->notify('Error', $e->getMessage(), 'danger'); - } + $result = app(EmulatorControlAction::class)->update(); + $this->notify($result['success'] ?? false ? 'Success' : 'Error', $result['message'] ?? $result['error'] ?? 'Onbekende fout', ($result['success'] ?? false) ? 'success' : 'danger'); + Cache::forget('all_updates_check'); + $this->fillForm(); } public function buildEmulator(): void { - try { - $sourcePath = $this->getSetting('emulator_source_path', '/var/www/emulator-source'); - $githubUrl = $this->getSetting('emulator_github_url', ''); - $branch = $this->getSetting('emulator_github_branch', 'main'); - - if ($githubUrl === '' || $githubUrl === '0') { - $this->notify('Error', 'Configureer eerst de Emulator GitHub URL', 'danger'); - - return; - } - - // Pull latest from GitHub - $this->notify('Info', 'π Pulling latest changes from GitHub...', 'info'); - $pullResult = $this->runCommand('cd ' . escapeshellarg($sourcePath) . ' && git pull origin ' . escapeshellarg($branch) . ' 2>&1', 60); - - $mavenCheck = $this->runCommand('which mvn 2>/dev/null'); - if (! $mavenCheck || ! trim($mavenCheck)) { - $this->notify('Warning', 'Maven (mvn) is niet geΓ―nstalleerd - kan niet bouwen', 'warning'); - - return; - } - - // Find pom.xml and build - $pomDirs = [$sourcePath, $sourcePath . '/Emulator', $sourcePath . '/Emulator/Emulator']; - foreach ($pomDirs as $pomDir) { - $pomCheck = $this->runCommand('test -f ' . escapeshellarg($pomDir . '/pom.xml') . ' && echo yes'); - if ($pomCheck === 'yes') { - $this->notify('Info', 'π¨ Building emulator with Maven...', 'info'); - $buildResult = $this->runCommand('cd ' . escapeshellarg($pomDir) . ' && mvn clean package -DskipTests 2>&1', 600); - - if ($buildResult && str_contains($buildResult, 'BUILD SUCCESS')) { - $jarPath = $this->getSetting('emulator_jar_path', '/var/www/Emulator'); - $jarFind = $this->runCommand('find ' . escapeshellarg($pomDir . '/target') . ' -name "*jar-with-dependencies.jar" -type f 2>/dev/null | head -1', 30); - - if ($jarFind) { - $sourceJar = trim($jarFind); - $jarName = basename($sourceJar); - $destJar = $jarPath . '/' . $jarName; - - $this->runCommand('cp ' . escapeshellarg($sourceJar) . ' ' . escapeshellarg($destJar) . ' 2>&1', 30); - - $latestPath = $sourcePath . '/Emulator/Latest_Compiled_Version'; - if (is_dir($latestPath)) { - $this->runCommand('cp ' . escapeshellarg($sourceJar) . ' ' . escapeshellarg($latestPath . '/' . $jarName) . ' 2>&1', 30); - } - - $this->notify('Success', 'β Build succesvol! JAR verplaatst naar ' . $jarName . '. Herstart de emulator.', 'success'); - } else { - $this->notify('Success', 'β Build succesvol! Herstart de emulator.', 'success'); - } - } else { - $this->notify('Error', 'Build mislukt - controleer logs', 'danger'); - } - - return; - } - } - - $this->notify('Warning', 'Geen pom.xml gevonden - kan niet bouwen vanaf source', 'warning'); - } catch (Exception $e) { - $this->notify('Error', $e->getMessage(), 'danger'); - } + $result = app(EmulatorControlAction::class)->build(); + $this->notify($result['success'] ?? false ? 'Success' : 'Error', $result['message'] ?? $result['error'] ?? 'Onbekende fout', ($result['success'] ?? false) ? 'success' : 'danger'); } public function renderBackupsList(): HtmlString { try { - $service = new EmulatorUpdateService; - $backups = $service->getBackupList(); + $backups = app(EmulatorControlAction::class)->getBackups(); if ($backups === []) { return new HtmlString(<<<'HTML' @@ -1480,34 +1374,17 @@ final class Commandocentrum extends Page implements HasForms } } - public function restoreBackup(string $backupName): void - { - try { - $service = new EmulatorUpdateService; - $result = $service->restoreBackup($backupName); - - if ($result['success']) { - $this->notify('Success', $result['message'], 'success'); - } else { - $this->notify('Error', $result['error'], 'danger'); - } - } catch (Exception $e) { - $this->notify('Error', $e->getMessage(), 'danger'); - } - } - public function runSqlUpdates(): void { - try { - $exitCode = Artisan::call('update:auto', ['--force' => true]); - if ($exitCode === 0) { - $this->notify('Success', 'SQL updates toegepast!', 'success'); - } else { - $this->notify('Warning', 'Update controle voltooid', 'warning'); - } - } catch (Exception $e) { - $this->notify('Error', $e->getMessage(), 'danger'); - } + $result = app(EmulatorControlAction::class)->runSqlUpdates(); + $this->notify($result['success'] ?? false ? 'Success' : 'Error', $result['message'] ?? 'Onbekende fout', ($result['success'] ?? false) ? 'success' : 'danger'); + } + + public function restoreBackup(string $backupName): void + { + $result = app(EmulatorControlAction::class)->restoreBackup($backupName); + $this->notify($result['success'] ? 'Success' : 'Error', $result['message'] ?? $result['error'] ?? 'Onbekende fout', $result['success'] ? 'success' : 'danger'); + $this->fillForm(); } public function saveEmulator(): void @@ -1531,109 +1408,34 @@ final class Commandocentrum extends Page implements HasForms public function checkNitroUpdates(): void { - try { - $clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client'); - $rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer'); - $branch = $this->getSetting('nitro_github_branch', 'main'); + $clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client'); + $rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer'); + $branch = $this->getSetting('nitro_github_branch', 'main'); - // Get local commits before pull - $clientCommitBefore = $this->getGitCommit($clientPath); - $rendererCommitBefore = $this->getGitCommit($rendererPath); - - // Pull latest from GitHub using sudo - $this->notify('Info', 'π Pulling Nitro Client van GitHub...', 'info'); - $this->runCommand('cd ' . escapeshellarg($clientPath) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1', 60); - - $this->runCommand('cd ' . escapeshellarg($rendererPath) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1', 60); - - // Get local commits after pull - $clientCommitAfter = $this->getGitCommit($clientPath); - $rendererCommitAfter = $this->getGitCommit($rendererPath); - - // Check if anything was updated - $clientUpdated = $clientCommitBefore !== $clientCommitAfter; - $rendererUpdated = $rendererCommitBefore !== $rendererCommitAfter; - - if ($clientUpdated || $rendererUpdated) { - $this->notify('Success', 'β Nitro bijgewerkt! Build opnieuw met "Build" knop.', 'success'); - } else { - $this->notify('Success', 'β Nitro is al up-to-date!', 'success'); - } - } catch (Exception $e) { - $this->notify('Error', $e->getMessage(), 'danger'); - } + $result = app(NitroControlAction::class)->pullUpdates($clientPath, $rendererPath, $branch); + $this->notify('Success', $result['message'], 'success'); + $this->fillForm(); } public function buildNitro(): void { - try { - $clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client'); - $rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer'); - $branch = $this->getSetting('nitro_github_branch', 'main'); + $clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client'); + $rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer'); + $branch = $this->getSetting('nitro_github_branch', 'main'); - // Pull latest from GitHub - $this->notify('Info', 'π Pulling Nitro Client...', 'info'); - $this->runCommand('cd ' . escapeshellarg($clientPath) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1', 60); - - $this->runCommand('cd ' . escapeshellarg($rendererPath) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1', 60); - - $this->runCommand('cd ' . escapeshellarg($clientPath) . ' && sudo -u www-data npm install 2>&1', 120); - $this->runCommand('cd ' . escapeshellarg($rendererPath) . ' && sudo -u www-data npm install 2>&1', 120); - - // Build - $this->notify('Info', 'π¨ Building Nitro...', 'info'); - $exitCode = Artisan::call('build:theme'); - if ($exitCode === 0) { - $this->notify('Success', 'β Nitro build succesvol!', 'success'); - } else { - $this->notify('Warning', 'Build gestart - controleer handmatig', 'warning'); - } - } catch (Exception $e) { - $this->notify('Error', $e->getMessage(), 'danger'); - } + $result = app(NitroControlAction::class)->build($clientPath, $rendererPath, $branch); + $this->notify($result['success'] ? 'Success' : 'Warning', $result['message'], $result['success'] ? 'success' : 'warning'); } public function generateNitroConfigs(): void { - try { - $siteUrl = $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl()); + $siteUrl = $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl()); + $webroot = $this->getSetting('nitro_webroot', '/var/www/Client'); + $gamedataPath = $this->getSetting('gamedata_path', '/var/www/Gamedata'); - if ($siteUrl === '' || $siteUrl === '0' || ! filter_var($siteUrl, FILTER_VALIDATE_URL)) { - $this->notify('Error', 'Voer een geldige URL in (bijv. https://epicnabbo.nl)', 'danger'); - - return; - } - - $paths = $this->autoDetectPaths(); - $settings = app(SettingsService::class); - - $settings->set('nitro_client_path', $paths['nitro_client_path']); - $settings->set('nitro_renderer_path', $paths['nitro_renderer_path']); - $settings->set('nitro_build_path', $paths['nitro_build_path']); - $settings->set('nitro_webroot', $paths['nitro_webroot']); - $settings->set('gamedata_path', $paths['gamedata_path']); - - $webroot = $paths['nitro_webroot']; - $existingConfigs = $this->readExistingConfigs($webroot, $paths['gamedata_path']); - - $exitCode = Artisan::call('app:generate-nitro-configs', [ - '--site-url' => $siteUrl, - ]); - - if ($existingConfigs !== [] && $exitCode === 0) { - $this->mergeExistingConfigs($webroot, $existingConfigs); - } - - $settings->set('nitro_last_checked', now()->toIso8601String()); - - if ($exitCode === 0) { - $this->notify('Success', 'Configs gegenereerd & bestaande instellingen behouden!', 'success'); - } else { - $this->notify('Warning', 'Config gegenereerd (controleer handmatig)', 'warning'); - } - } catch (Exception $e) { - $this->notify('Error', $e->getMessage(), 'danger'); - } + $result = app(NitroControlAction::class)->generateConfigs($siteUrl, $webroot, $gamedataPath); + $this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger'); + $this->fillForm(); } private function renderClothingStatus(): HtmlString @@ -1669,58 +1471,6 @@ final class Commandocentrum extends Page implements HasForms } } - private function readExistingConfigs(string $webroot, string $gamedataPath = ''): array - { - $configs = []; - $files = ['renderer-config.json', 'ui-config.json', 'UITexts.json']; - - foreach ($files as $file) { - $path = $webroot . '/' . $file; - $content = $this->readFile($path); - if ($content) { - $decoded = json_decode($content, true); - if (json_last_error() === JSON_ERROR_NONE) { - $configs[$file] = $decoded; - } - } - } - - if ($gamedataPath !== '' && $gamedataPath !== '0') { - $gamedataConfigs = [ - 'ExternalTexts.json' => $gamedataPath . '/config/ExternalTexts.json', - 'FurnitureData.json' => $gamedataPath . '/config/FurnitureData.json', - 'ProductData.json' => $gamedataPath . '/config/ProductData.json', - 'FigureData.json' => $gamedataPath . '/config/FigureData.json', - ]; - - foreach ($gamedataConfigs as $key => $path) { - $result = $this->readFile($path); - if ($result) { - $decoded = json_decode($result, true); - if (json_last_error() === JSON_ERROR_NONE) { - $configs['gamedata.' . $key] = $decoded; - } - } - } - } - - return $configs; - } - - private function mergeExistingConfigs(string $webroot, array $existingConfigs): void - { - if (isset($existingConfigs['renderer-config.json'])) { - $newPath = $webroot . '/renderer-config.json'; - $newContent = $this->readFile($newPath); - $newConfig = json_decode($newContent, true); - - if ($newConfig && json_last_error() === JSON_ERROR_NONE) { - $merged = array_merge($existingConfigs['renderer-config.json'], $newConfig); - @file_put_contents($newPath, json_encode($merged, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - } - } - } - public function detectAndSavePaths(): void { try { diff --git a/resources/views/filament/components/commandocentrum/diagnostics.blade.php b/resources/views/filament/components/commandocentrum/diagnostics.blade.php new file mode 100755 index 0000000..2d0912d --- /dev/null +++ b/resources/views/filament/components/commandocentrum/diagnostics.blade.php @@ -0,0 +1,65 @@ +@props(['diagnostics']) + +@php + $errors = array_filter($diagnostics, fn ($r) => $r->status === 'error'); + $warnings = array_filter($diagnostics, fn ($r) => $r->status === 'warning'); + $ok = array_filter($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' => 'Kritieke Problemen', + 'warning' => 'Waarschuwingen', + default => 'Gezond', + }; +@endphp + +