*/
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'),
'auto_update_enabled' => $this->getSettingBool('auto_update_enabled'),
'auto_update_schedule' => $this->getSetting('auto_update_schedule', '03:00'),
'auto_update_days' => $this->getSetting('auto_update_days', '0,6'),
'nitro_client_path' => $this->getSetting('nitro_client_path', $paths['nitro_client_path']),
'nitro_renderer_path' => $this->getSetting('nitro_renderer_path', $paths['nitro_renderer_path']),
'nitro_build_path' => $this->getSetting('nitro_build_path', $paths['nitro_build_path']),
'nitro_webroot' => $this->getSetting('nitro_webroot', $paths['nitro_webroot']),
'gamedata_path' => $this->getSetting('gamedata_path', $paths['gamedata_path']),
'nitro_github_branch' => $this->getSetting('nitro_github_branch', 'main'),
'nitro_github_url' => $this->getSetting('nitro_github_url', ''),
'nitro_site_url' => $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl()),
'nitro_auto_update_configs' => $this->getSettingBool('nitro_auto_update_configs'),
'nitro_auto_update_enabled' => $this->getSettingBool('nitro_auto_update_enabled'),
'nitro_auto_update_schedule' => $this->getSetting('nitro_auto_update_schedule', '03:00'),
'nitro_auto_update_days' => $this->getSetting('nitro_auto_update_days', '0,6'),
'hotel_alert_message' => '',
];
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Section::make('🎯 Live Status')
->description('Real-time hotel statistieken')
->icon('heroicon-o-heart')
->columns(4)
->schema([
Placeholder::make('online_users')
->label('Online')
->content(fn (): HtmlString => $this->getCardHtml('Online', $this->getOnlineUsersCount(), '#22c55e', 'heroicon-o-users')),
Placeholder::make('emulator_status')
->label('Emulator')
->content(fn (): HtmlString => $this->getCardHtml('Emulator', $this->getEmulatorStatusText(), $this->getEmulatorStatusColor(), 'heroicon-o-server')),
Placeholder::make('database_status')
->label('Database')
->content(fn (): HtmlString => $this->getCardHtml('Database', $this->isDatabaseOnline() ? 'Online' : 'Offline', $this->isDatabaseOnline() ? '#22c55e' : '#ef4444', 'heroicon-o-circle-stack')),
Placeholder::make('server_load')
->label('Load')
->content(fn (): HtmlString => $this->getCardHtml('Load', $this->getServerLoad(), '#3b82f6', 'heroicon-o-cpu-chip')),
]),
Section::make('📊 Server Informatie')
->description('Gedetailleerde server status')
->icon('heroicon-o-information-circle')
->schema([
Placeholder::make('server_info')
->label('')
->content(fn (): HtmlString => $this->renderServerInfo()),
]),
Section::make('🩺 Systeem Gezondheid')
->description('Automatische systeem diagnostiek')
->icon('heroicon-o-heart')
->afterHeader([
Action::make('refresh_diagnostics')
->label('Vernieuwen')
->icon('heroicon-o-arrow-path')
->color('info')
->action('refreshDiagnostics'),
])
->schema([
Placeholder::make('diagnostics')
->label('')
->content(fn (): HtmlString => $this->renderDiagnostics()),
]),
Section::make('🏨 Hotel Status')
->description('Emulator en Nitro status')
->icon('heroicon-o-building-office')
->schema([
Placeholder::make('hotel_status')
->label('')
->content(fn (): HtmlString => $this->renderHotelStatus()),
]),
Section::make('📢 Hotel Alert')
->description('Stuur een bericht naar alle online gebruikers')
->icon('heroicon-o-megaphone')
->schema([
Placeholder::make('alert_form')
->label('')
->content(fn (): HtmlString => $this->renderAlertForm()),
]),
Section::make('📜 Emulator Logs')
->description('Live emulator log viewer')
->icon('heroicon-o-document-text')
->columnSpanFull()
->schema([
Placeholder::make('emulator_logs')
->label('')
->content(fn () => view('filament.components.emulator-log-viewer')),
]),
Section::make('🖥️ Emulator Control')
->description('Volledige emulator controle')
->icon('heroicon-o-server')
->columns(3)
->afterHeader([
Action::make('start_emulator')
->label('Start')
->icon('heroicon-o-play')
->color('success')
->action('startEmulator'),
Action::make('stop_emulator')
->label('Stop')
->icon('heroicon-o-stop')
->color('danger')
->action('stopEmulator'),
Action::make('restart_emulator')
->label('Restart')
->icon('heroicon-o-arrow-path')
->color('warning')
->action('restartEmulator'),
Action::make('check_emulator')
->label('Check')
->icon('heroicon-o-check-circle')
->color('info')
->action('checkEmulator'),
])
->schema([
Placeholder::make('emulator_info')
->label('')
->content(fn (): HtmlString => $this->renderEmulatorInfo()),
]),
Section::make('🔄 Emulator Updates')
->description('Configureer en update de emulator')
->icon('heroicon-o-arrow-down-circle')
->afterHeader([
Action::make('check_updates')
->label('Check Updates')
->color('info')
->action('checkEmulatorUpdates'),
Action::make('build_emulator')
->label('🔨 Build')
->color('success')
->action('buildEmulator'),
Action::make('run_sql')
->label('SQL Updates')
->color('purple')
->action('runSqlUpdates'),
Action::make('save_emulator')
->label('Opslaan')
->color('primary')
->action('saveEmulator'),
])
->schema([
Placeholder::make('emulator_settings')
->label('')
->content(fn (): HtmlString => $this->renderEmulatorSettings()),
]),
Section::make('💾 Emulator Backups')
->description('Bekijk en herstel emulator backups')
->icon('heroicon-s-archive-box')
->schema([
Placeholder::make('backups_list')
->label('')
->content(fn (): HtmlString => $this->renderBackupsList()),
]),
Section::make('📦 Nitro Client')
->description('Configureer en update Nitro')
->icon('heroicon-o-cloud-arrow-down')
->afterHeader([
Action::make('detect_paths')
->label('🔍 Auto Detect')
->color('success')
->action('detectAndSavePaths'),
Action::make('check_nitro')
->label('Check')
->color('info')
->action('checkNitroUpdates'),
Action::make('build_nitro')
->label('Build')
->color('pink')
->action('buildNitro'),
Action::make('generate_configs')
->label('Genereer Configs')
->color('indigo')
->action('generateNitroConfigs'),
Action::make('save_nitro')
->label('Opslaan')
->color('primary')
->action('saveNitro'),
])
->schema([
Placeholder::make('nitro_settings')
->label('')
->content(fn (): HtmlString => $this->renderNitroSettings()),
]),
Section::make('⚙️ Automatische Updates')
->description('Configureer automatische updates')
->icon('heroicon-o-clock')
->columns(2)
->afterHeader([
Action::make('save_auto')
->label('Opslaan')
->color('primary')
->action('saveAutoUpdate'),
])
->schema([
Toggle::make('auto_update_enabled')
->label('Automatische Updates Inschakelen'),
TextInput::make('auto_update_schedule')
->label('Schema (HH:MM)'),
TextInput::make('auto_update_days')
->label('Dagen (0-6)'),
]),
Section::make('👔 Kleding Sync')
->description('Sync catalogus kleding uit FigureMap')
->icon('heroicon-o-user')
->afterHeader([
Action::make('sync_clothing')
->label('🔄 Sync')
->color('success')
->action('syncClothing'),
])
->schema([
Placeholder::make('clothing_status')
->label('')
->content(fn (): HtmlString => $this->renderClothingStatus()),
]),
Section::make('🔔 Meldingen')
->description('E-mail en Discord alerts')
->icon('heroicon-o-bell')
->columns(2)
->afterHeader([
Action::make('save_alerts')
->label('Opslaan')
->color('primary')
->action('saveAlerts'),
Action::make('test_discord')
->label('Test Discord')
->color('info')
->action('testDiscord'),
])
->schema([
Toggle::make('alert_email_enabled')
->label('E-mail Meldingen'),
TextInput::make('alert_email_address')
->label('E-mail Adres')
->email()
->columnSpanFull(),
Toggle::make('alert_discord_enabled')
->label('Discord Meldingen'),
TextInput::make('alert_discord_webhook_url')
->label('Webhook URL')
->columnSpanFull(),
Select::make('discord_webhook_ranks')
->label('Ranks die Discord notificatie krijgen')
->multiple()
->options(fn () => WebsitePermission::query()->pluck('permission', 'min_rank')->mapWithKeys(fn ($perm, $rank) => [$rank => "Rank {$rank} ({$perm})"])->toArray())
->helperText('Laat leeg voor alleen staff (min_staff_rank)'),
]),
Section::make('📊 Update Geschiedenis')
->description('Laatste systeem updates')
->icon('heroicon-o-clock')
->schema([
Placeholder::make('history')
->label('')
->content(fn (): HtmlString => $this->getUpdateHistoryHtml()),
]),
Section::make('🔐 Social Login (v1.4)')
->description('Enable social login providers')
->icon('heroicon-o-user-circle')
->schema([
Toggle::make('social_login_google_enabled')
->label('Google Login')
->helperText('Allow users to login with Google'),
TextInput::make('social_login_google_client_id')
->label('Google Client ID')
->helperText('From Google Cloud Console'),
TextInput::make('social_login_google_client_secret')
->label('Google Client Secret')
->type('password'),
Toggle::make('social_login_discord_enabled')
->label('Discord Login')
->helperText('Allow users to login with Discord'),
TextInput::make('social_login_discord_client_id')
->label('Discord Client ID')
->helperText('From Discord Developer Portal'),
TextInput::make('social_login_discord_client_secret')
->label('Discord Client Secret')
->type('password'),
Toggle::make('social_login_github_enabled')
->label('GitHub Login')
->helperText('Allow users to login with GitHub'),
TextInput::make('social_login_github_client_id')
->label('GitHub Client ID')
->helperText('From GitHub Developer Settings'),
TextInput::make('social_login_github_client_secret')
->label('GitHub Client Secret')
->type('password'),
])
->columns(2),
Section::make('👔 Staff Activity Log')
->description('Recent staff activities in the housekeeping (v1.2)')
->icon('heroicon-o-user-group')
->schema([
Placeholder::make('staff_activity')
->label('')
->content(fn (): HtmlString => $this->getStaffActivityHtml()),
]),
]);
}
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(<<
{$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 renderServerInfo(): HtmlString
{
$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';
$load = sys_getloadavg();
$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';
return new HtmlString(<<
PHP
{$phpVersion}
Laravel
{$laravelVersion}
Memory
{$memoryUsage}MB / {$memoryLimit}
Disk
{$diskUsage}
HTML);
}
private function renderHotelStatus(): HtmlString
{
$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);
$onlineUsers = $this->getOnlineUsersCount();
$emulatorStatus = $this->getEmulatorStatusText();
$dbStatus = $this->isDatabaseOnline() ? 'Online' : 'Offline';
$dbColor = $dbStatus === 'Online' ? '#22c55e' : '#ef4444';
$clientColor = $clientExists ? '#22c55e' : '#ef4444';
$rendererColor = $rendererExists ? '#22c55e' : '#ef4444';
$clientText = $clientExists ? '✓ ' . substr($clientCommit, 0, 7) : '✗ Niet gevonden';
$rendererText = $rendererExists ? '✓ ' . substr($rendererCommit, 0, 7) : '✗ Niet gevonden';
$webrootText = $rendererExists ? '✓ ' . basename($nitroWebroot) : '✗ Niet gevonden';
$webrootColor = $rendererExists ? '#22c55e' : '#ef4444';
return new HtmlString(<<
Status
{$serviceStatus}
Online
{$emulatorStatus}
Gebruikers
{$onlineUsers}
Database
{$dbStatus}
Client
{$clientText}
Renderer
{$rendererText}
Webroot
{$webrootText}
HTML);
}
private function getEmulatorBranchesHtml(): string
{
$githubUrl = $this->getSetting('emulator_github_url', '');
$currentBranch = $this->data['emulator_github_branch'] ?? 'main';
$branches = $this->fetchGitHubBranches($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 = $this->fetchGitHubBranches($githubUrl);
$html = '';
foreach ($branches as $branch) {
$selected = $branch === $currentBranch ? 'selected' : '';
$html .= '';
}
return $html;
}
private function fetchGitHubBranches(string $githubUrl): array
{
if ($githubUrl === '' || $githubUrl === '0' || ! str_contains($githubUrl, 'github.com')) {
return [];
}
try {
$repo = $this->extractGitHubRepo($githubUrl);
$gitUrl = "https://github.com/{$repo}.git";
// Use git ls-remote to get all branches (doesn't hit rate limit)
$result = $this->runCommand('git ls-remote --heads ' . escapeshellarg($gitUrl) . ' 2>/dev/null', 30);
if ($result) {
$branches = [];
foreach (explode("\n", trim($result)) as $line) {
$parts = explode("\t", $line);
if (isset($parts[1]) && ($parts[1] !== '' && $parts[1] !== '0') && str_starts_with($parts[1], 'refs/heads/')) {
$branches[] = str_replace('refs/heads/', '', $parts[1]);
}
}
return $branches;
}
// Fallback to GitHub API
$response = Http::timeout(10)->get("https://api.github.com/repos/{$repo}/branches");
if ($response->successful()) {
$data = $response->json();
return array_column($data, 'name');
}
} catch (Exception) {
// Ignore
}
return [];
}
private function extractGitHubRepo(string $url): string
{
$url = trim($url, '/');
// Handle GitHub URLs with /tree/ or /blob/
if (str_contains($url, 'github.com/')) {
$parts = explode('github.com/', $url);
if (isset($parts[1])) {
$path = trim($parts[1], '/');
// Remove /tree/... or /blob/...
if (str_contains($path, '/tree/')) {
$path = explode('/tree/', $path)[0];
}
if (str_contains($path, '/blob/')) {
$path = explode('/blob/', $path)[0];
}
return $path;
}
}
return $url;
}
private function renderAlertForm(): HtmlString
{
return new HtmlString(<<<'HTML'
HTML);
}
private function renderEmulatorInfo(): HtmlString
{
$version = $this->getSetting('emulator_version', 'Onbekend');
$serviceName = $this->getSetting('emulator_service_name', 'arcturus');
$status = $this->getEmulatorStatusText();
$color = $status === 'Online' ? '#22c55e' : '#ef4444';
return new HtmlString(<<
HTML);
}
private function renderEmulatorSettings(): HtmlString
{
$status = $this->getEmulatorStatusHtml();
return new HtmlString(<<
{$status}
HTML);
}
private function getEmulatorStatusHtml(): string
{
$serviceName = $this->getSetting('emulator_service_name', 'arcturus');
$jarPath = $this->getSetting('emulator_jar_path', '/var/www/Emulator');
$emulatorOnline = $this->getEmulatorStatusText() === 'Online';
$jarExists = $this->fileExists($jarPath);
$onlineStatus = $emulatorOnline
? '✓ Online'
: '✗ Offline';
$jarStatus = $jarExists
? '✓ JAR OK'
: '✗ JAR ontbreekt';
$serviceStatus = 'Service: ' . e($serviceName) . '';
$updateInfo = $this->checkEmulatorUpdatesFromGitHub();
return ''
. '
' . $onlineStatus . $jarStatus . $serviceStatus . '
'
. '
' . $updateInfo . '
'
. '
';
}
private function checkEmulatorUpdatesFromGitHub(): string
{
try {
$githubUrl = $this->getSetting('emulator_github_url', '');
$sourcePath = $this->getSetting('emulator_source_path', '/var/www/emulator-source');
$jarPath = $this->getSetting('emulator_jar_path', '/var/www/Emulator');
$sourceExists = $this->fileExists($sourcePath);
$jarExists = $this->fileExists($jarPath);
$sourceCommit = $this->getGitCommit($sourcePath);
$remoteVersion = $githubUrl !== '' && $githubUrl !== '0' ? $this->getEmulatorRemoteVersion($githubUrl) : 'N/A';
$canBuild = false;
$pomPath = '';
$checkDirs = [
$sourcePath,
$sourcePath . '/Emulator',
$sourcePath . '/Emulator/Emulator',
$sourcePath . '/emulator',
$sourcePath . '/emulator/emulator',
];
foreach ($checkDirs as $dir) {
$check = $this->runCommand('test -f ' . escapeshellarg($dir . '/pom.xml') . ' && echo yes');
if ($check && trim($check) === 'yes') {
$canBuild = true;
$pomPath = $dir;
break;
}
}
$hasUpdate = $sourceCommit !== 'N/A' && $remoteVersion !== 'N/A' && substr($sourceCommit, 0, 7) !== $remoteVersion;
$updateColor = $hasUpdate ? '#f59e0b' : '#22c55e';
$updateText = $hasUpdate ? '🔄 Update beschikbaar' : '✓ Up-to-date';
$html = 'GitHub Status:
';
// Update status
$html .= '';
$html .= '
';
$html .= '
';
$html .= $updateText;
$html .= '
';
// Always show update button
$btnColor = $hasUpdate ? '#f59e0b' : '#3b82f6';
$btnGradient = $hasUpdate ? 'linear-gradient(135deg,#f59e0b,#d97706)' : 'linear-gradient(135deg,#3b82f6,#2563eb)';
$btnText = $hasUpdate ? '⚡ Updaten' : '🔄 Herbouwen';
$html .= '
';
$html .= '
';
// Version info - use short hash for comparison
$sourceCommitShort = substr($sourceCommit, 0, 7);
$html .= 'Latest:✓ ' . e($remoteVersion) . '
';
$html .= 'Source:✓ ' . $sourceCommitShort . '
';
// Build method
if ($jarExists) {
$jarSize = $this->runCommand('ls -lh ' . escapeshellarg($jarPath) . '/*.jar 2>/dev/null | head -1');
if ($jarSize) {
preg_match('/(\S+\.jar)/', $jarSize, $matches);
if (isset($matches[1])) {
$html .= 'JAR:✓ ' . basename($matches[1]) . '
';
}
}
}
if ($canBuild) {
$html .= 'Bouwen:✓ Maven (pom.xml)
';
} else {
$html .= 'Bouwen:⚠️ Geen pom.xml
';
}
// Auto-update method
$html .= '';
$html .= '
METHODE:
';
if ($jarExists) {
$html .= '
📦 JAR Download & Herstart
';
} elseif ($canBuild) {
$html .= '
🔨 Maven Build & Herstart
';
} else {
$html .= '
Handmatig: Download JAR van GitHub
';
}
return $html . '
';
} catch (Exception) {
return 'Kon emulator status niet ophalen';
}
}
private function getEmulatorRemoteVersion(string $githubUrl): string
{
try {
if ($githubUrl === '' || $githubUrl === '0') {
return 'N/A';
}
// Try to get latest commit using git ls-remote (doesn't hit rate limit)
$repo = $this->extractGitHubRepo($githubUrl);
// Use nitro branch if it's a nitro repo, otherwise use emulator branch
$isNitroRepo = str_contains($githubUrl, 'Nitro');
$branch = $isNitroRepo
? $this->getSetting('nitro_github_branch', 'main')
: $this->getSetting('emulator_github_branch', 'main');
$gitUrl = "https://github.com/{$repo}.git";
$result = $this->runCommand('git ls-remote ' . escapeshellarg($gitUrl) . ' ' . escapeshellarg($branch) . ' 2>/dev/null', 30);
if ($result) {
$parts = explode("\t", trim($result));
if ($parts[0] !== '' && $parts[0] !== '0') {
return substr($parts[0], 0, 7);
}
}
// Fallback to GitHub API
$response = Http::timeout(10)->get("https://api.github.com/repos/{$repo}/releases/latest");
if ($response->successful()) {
$data = $response->json();
if (! empty($data['tag_name'])) {
return $data['tag_name'];
}
}
$response = Http::timeout(10)->get("https://api.github.com/repos/{$repo}/commits?per_page=1");
if ($response->successful()) {
$data = $response->json();
if (! empty($data[0]['sha'])) {
return substr((string) $data[0]['sha'], 0, 7);
}
}
} catch (Exception) {
// Ignore
}
return 'N/A';
}
private function renderNitroSettings(): HtmlString
{
$status = $this->getNitroStatusHtml();
return new HtmlString(<<
{$status}
HTML);
}
private function getNitroStatusHtml(): string
{
$clientPath = $this->getSetting('nitro_client_path', '/var/www/atomcms/nitro-client');
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/atomcms/nitro-renderer');
$webroot = $this->getSetting('nitro_webroot', '/var/www/Client');
$clientExists = $this->checkPathExists($clientPath);
$rendererExists = $this->checkPathExists($rendererPath);
$webrootExists = $this->checkPathExists($webroot);
$clientStatus = $clientExists ? '✓ Client OK' : '✗ Client ontbreekt';
$rendererStatus = $rendererExists ? '✓ Renderer OK' : '✗ Renderer ontbreekt';
$webrootStatus = $webrootExists ? '✓ Webroot OK' : '✗ Webroot ontbreekt';
$updateInfo = $this->checkNitroUpdatesFromGitHub();
return ''
. $clientStatus . $rendererStatus . $webrootStatus
. '
' . $updateInfo . '
'
. '
';
}
private function checkNitroUpdatesFromGitHub(): string
{
try {
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
$clientGithubUrl = $this->getSetting('nitro_github_url', '');
// Use the renderer-specific repo (defaults to Nitro_Render_V3 if not set)
$rendererGithubUrl = $this->getSetting('nitro_renderer_github_url', 'https://github.com/duckietm/Nitro_Render_V3');
$clientCommit = $this->getGitCommit($clientPath);
$rendererCommit = $this->getGitCommit($rendererPath);
$clientRemote = $clientGithubUrl !== '' && $clientGithubUrl !== '0' ? $this->getEmulatorRemoteVersion($clientGithubUrl) : 'N/A';
$rendererRemote = $rendererGithubUrl !== '' && $rendererGithubUrl !== '0' ? $this->getEmulatorRemoteVersion($rendererGithubUrl) : 'N/A';
// Compare only first 7 characters (short hash)
$clientCommitShort = substr($clientCommit, 0, 7);
$rendererCommitShort = substr($rendererCommit, 0, 7);
$hasClientUpdate = $clientCommitShort !== 'N/A' && $clientRemote !== 'N/A' && $clientCommitShort !== $clientRemote;
$hasRendererUpdate = $rendererCommitShort !== 'N/A' && $rendererRemote !== 'N/A' && $rendererCommitShort !== $rendererRemote;
$hasUpdate = $hasClientUpdate || $hasRendererUpdate;
$updateColor = $hasUpdate ? '#f59e0b' : '#22c55e';
$updateText = $hasUpdate ? '🔄 Update beschikbaar' : '✓ Up-to-date';
$html = 'GitHub Status:
';
// Update status
$html .= '';
$html .= '
';
$html .= '
';
$html .= $updateText;
$html .= '
';
// Update button if update available
if ($hasUpdate) {
$html .= '
';
}
$html .= '
';
// Client info
$clientColor = $hasClientUpdate ? '#f59e0b' : '#22c55e';
$clientIcon = $hasClientUpdate ? '🔄' : '✓';
$html .= 'Client Remote:✓ ' . e($clientRemote) . '
';
$html .= 'Client Local:' . $clientIcon . ' ' . $clientCommitShort . '
';
// Renderer info
$rendererColor = $hasRendererUpdate ? '#f59e0b' : '#22c55e';
$rendererIcon = $hasRendererUpdate ? '🔄' : '✓';
$html .= 'Renderer Remote:✓ ' . e($rendererRemote) . '
';
return $html . ('Renderer Local:' . $rendererIcon . ' ' . $rendererCommitShort . '
');
} catch (Exception) {
return 'Kon updates niet ophalen';
}
}
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 getUpdateHistoryHtml(): HtmlString
{
try {
$history = app(UpdateHistoryService::class)->getRecent(10);
if (empty($history)) {
return new HtmlString('Geen updates gevonden
');
}
$html = '';
foreach ($history as $update) {
$statusColor = $update->status === 'success' ? '#22c55e' : ($update->status === 'pending' ? '#f59e0b' : '#ef4444');
$html .= '
';
$html .= '
' . e($update->type) . '' . e($update->message) . '
';
$html .= '
' . e($update->status) . '' . e($update->created_at) . '
';
$html .= '
';
}
$html .= '
';
return new HtmlString($html);
} catch (Exception) {
return new HtmlString('Kon historie niet laden
');
}
}
public function sendHotelAlert(): void
{
$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'] = '';
}
}
public function startEmulator(): void
{
$result = app(EmulatorControlAction::class)->start();
$this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger');
}
public function stopEmulator(): void
{
$result = app(EmulatorControlAction::class)->stop();
$this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger');
}
public function restartEmulator(): void
{
$result = app(EmulatorControlAction::class)->restart();
$this->notify($result['success'] ? 'Success' : '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('Success', 'Emulator is online en reageert!', 'success');
} else {
$this->notify('Warning', 'Emulator is niet bereikbaar via RCON', 'warning');
}
$this->fillForm();
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
public function checkEmulatorUpdates(): void
{
$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
{
$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 {
$backups = app(EmulatorControlAction::class)->getBackups();
if ($backups === []) {
return new HtmlString(<<<'HTML'
Nog geen backups beschikbaar
Backups worden automatisch aangemaakt bij elke emulator update
HTML);
}
$html = '';
foreach ($backups as $backup) {
$dateFormatted = str_replace('_', ' ', $backup['date']);
$html .= <<
{$dateFormatted}
HTML;
}
$html .= '';
return new HtmlString($html);
} catch (Exception $e) {
return new HtmlString('Kon backups niet laden: ' . e($e->getMessage()) . '
');
}
}
public function runSqlUpdates(): void
{
$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
{
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('Success', 'Emulator instellingen opgeslagen!', 'success');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
public function checkNitroUpdates(): void
{
$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');
$result = app(NitroControlAction::class)->pullUpdates($clientPath, $rendererPath, $branch);
$this->notify('Success', $result['message'], 'success');
$this->fillForm();
}
public function buildNitro(): void
{
$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');
$result = app(NitroControlAction::class)->build($clientPath, $rendererPath, $branch);
$this->notify($result['success'] ? 'Success' : 'Warning', $result['message'], $result['success'] ? 'success' : 'warning');
}
public function generateNitroConfigs(): void
{
$siteUrl = $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl());
$webroot = $this->getSetting('nitro_webroot', '/var/www/Client');
$gamedataPath = $this->getSetting('gamedata_path', '/var/www/Gamedata');
$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
{
try {
$count = DB::table('catalog_clothing')->count();
$html = '';
$html .= '
';
$html .= '
Kleding Items
';
$html .= '
' . number_format($count) . '
';
$html .= '
';
$html .= '
';
return new HtmlString($html);
} catch (Exception $e) {
return new HtmlString('Fout: ' . e($e->getMessage()) . '
');
}
}
public function syncClothing(): void
{
try {
$catalogService = app(CatalogService::class);
$result = $catalogService->syncCatalogClothing();
$message = '👔 Kleding Sync Resultaat:' . PHP_EOL;
$message .= '• Toegevoegd: ' . $result['inserted'] . PHP_EOL;
$message .= '• Totaal: ' . $result['total'];
$this->notify('Success', $message, 'success');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
public function detectAndSavePaths(): void
{
try {
$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']);
$settings->set('emulator_jar_path', $paths['emulator_jar_path']);
$settings->set('emulator_source_path', $paths['emulator_source_path']);
$this->fillForm();
$this->notify('Success', 'Paths gedetecteerd en opgeslagen!', 'success');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
public function saveNitro(): void
{
try {
$settings = app(SettingsService::class);
$settings->set('nitro_client_path', $this->data['nitro_client_path'] ?? '/var/www/atomcms/nitro-client');
$settings->set('nitro_renderer_path', $this->data['nitro_renderer_path'] ?? '/var/www/atomcms/nitro-renderer');
$settings->set('nitro_build_path', $this->data['nitro_build_path'] ?? '/var/www/atomcms/nitro-client/dist');
$settings->set('nitro_webroot', $this->data['nitro_webroot'] ?? '/var/www/Client');
$settings->set('gamedata_path', $this->data['gamedata_path'] ?? '/var/www/Gamedata');
$settings->set('nitro_github_url', $this->data['nitro_github_url'] ?? '');
$settings->set('nitro_github_branch', $this->data['nitro_github_branch'] ?? 'main');
$settings->set('nitro_site_url', $this->data['nitro_site_url'] ?? '');
$this->notify('Success', 'Nitro instellingen opgeslagen!', 'success');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
public function saveAutoUpdate(): void
{
try {
$settings = app(SettingsService::class);
$settings->set('auto_update_enabled', ($this->data['auto_update_enabled'] ?? false) ? '1' : '0');
$settings->set('auto_update_schedule', $this->data['auto_update_schedule'] ?? '03:00');
$settings->set('auto_update_days', $this->data['auto_update_days'] ?? '0,6');
$this->notify('Success', 'Auto update instellingen opgeslagen!', 'success');
} catch (Exception $e) {
$this->notify('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('Success', 'Meldingen opgeslagen!', 'success');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
private function getStaffActivityHtml(): HtmlString
{
try {
$activities = StaffActivity::with('user:id,username,look')
->orderByDesc('created_at')
->limit(20)
->get();
if ($activities->isEmpty()) {
return new HtmlString(<<<'HTML'
No staff activities recorded yet.
Staff actions will appear here automatically.
HTML);
}
$itemsHtml = '';
foreach ($activities as $activity) {
$icon = StaffActivity::getActionIcon($activity->action);
$color = StaffActivity::getActionColor($activity->action);
$timeAgo = $this->getTimeAgo($activity->created_at);
$username = $activity->user->username ?? 'Unknown';
$userLook = $activity->user->look ?? '';
$itemsHtml .= <<
{$icon}
{$username}
{$activity->description}
{$timeAgo}
HTML;
}
return new HtmlString(<<
Recent Staff Activities
Last 20 actions
{$itemsHtml}
HTML);
} catch (Exception $e) {
return new HtmlString(<<<'HTML'
Error loading staff activities
Make sure to run: php artisan migrate
HTML);
}
}
private function getTimeAgo($timestamp): string
{
try {
$carbon = Carbon::parse($timestamp);
$now = Carbon::now();
$diff = $now->diffInMinutes($carbon);
if ($diff < 1) {
return 'Just now';
} elseif ($diff < 60) {
return $diff . 'm ago';
} elseif ($diff < 1440) {
return floor($diff / 60) . 'h ago';
} else {
return floor($diff / 1440) . 'd ago';
}
} catch (Exception) {
return 'Unknown';
}
}
public function testDiscord(): void
{
try {
$webhookUrl = $this->data['alert_discord_webhook_url'] ?? '';
if (empty($webhookUrl)) {
$this->notify('Error', 'Webhook URL is leeg', 'danger');
return;
}
Http::post($webhookUrl, ['content' => '✅ Test van Atom CMS Commandocentrum']);
$this->notify('Success', 'Test bericht verzonden!', 'success');
} catch (Exception $e) {
$this->notify('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('Success', 'Diagnostiek vernieuwd', 'success');
}
private function runDiagnostics(): void
{
$runner = app(DiagnosticRunner::class);
$this->diagnostics = $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' => 'Kritieke Problemen',
'warning' => 'Waarschuwingen',
default => 'Gezond',
};
$html = '';
// Summary cards
$html .= '
';
$html .= $this->getSummaryCardHtml('Gezond', $okCount, '#22c55e', 'heroicon-o-check-circle');
$html .= $this->getSummaryCardHtml('Waarschuwingen', $warningCount, '#f59e0b', 'heroicon-o-exclamation-triangle');
$html .= $this->getSummaryCardHtml('Fouten', $errorCount, '#ef4444', 'heroicon-o-x-circle');
$html .= '
';
// Overall status banner
$html .= '
';
$html .= '
';
$html .= '
Systeem 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 <<
{$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');
}
}