fillForm(); } protected function fillForm(): void { $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', ''), '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' => (int) $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', '/root/emulator'), 'emulator_source_path' => $this->getSetting('emulator_source_path', '/var/www/emulator-source'), '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('E-mail Meldingen') ->description('Ontvang alerts via e-mail') ->icon('heroicon-o-envelope') ->columns(2) ->afterHeader([ Action::make('test_email') ->label('Test E-mail') ->action('testEmail') ->color('info'), Action::make('save_email') ->label('Opslaan') ->action('saveEmail') ->color('primary'), ]) ->schema([ Toggle::make('alert_email_enabled') ->label('E-mail Meldingen Inschakelen'), TextInput::make('alert_email_address') ->label('E-mail Adres') ->email() ->placeholder('admin@example.com') ->columnSpanFull(), ]), Section::make('Discord Webhook') ->description('Ontvang alerts via Discord') ->icon('heroicon-o-globe-alt') ->columns(2) ->afterHeader([ Action::make('test_discord') ->label('Test Discord') ->action('testDiscord') ->color('info'), Action::make('save_discord') ->label('Opslaan') ->action('saveDiscord') ->color('primary'), ]) ->schema([ Toggle::make('alert_discord_enabled') ->label('Discord Meldingen Inschakelen'), TextInput::make('alert_discord_webhook_url') ->label('Webhook URL') ->url() ->columnSpanFull() ->placeholder('https://discord.com/api/webhooks/...'), ]), Section::make('📊 Status Dashboard') ->description('Live hotel statistieken') ->icon('heroicon-o-chart-bar') ->columns(3) ->afterHeader([ Action::make('refresh_dashboard') ->label('Vernieuwen') ->icon('heroicon-o-arrow-path') ->action('refreshDashboard') ->color('info'), ]) ->schema([ Placeholder::make('online_users') ->label('') ->content(function () { $label = '
'; $label .= ''; $label .= 'Online Gebruikers'; $label .= ''; $label .= '
'; return new HtmlString($label . $this->getOnlineUsersHtml()); }), Placeholder::make('uptime') ->label('') ->content(function () { $label = '
'; $label .= ''; $label .= 'Uptime'; $label .= ''; $label .= '
'; return new HtmlString($label . $this->getUptimeHtml()); }), Placeholder::make('server_load') ->label('') ->content(function () { $label = '
'; $label .= ''; $label .= 'Server Load'; $label .= ''; $label .= '
'; return new HtmlString($label . $this->getServerLoadHtml()); }), ]), Section::make('📢 Hotel Alert') ->description('Stuur een bericht naar alle online gebruikers') ->icon('heroicon-o-megaphone') ->columns(2) ->afterHeader([ Action::make('send_hotel_alert') ->label('Verstuur Alert') ->icon('heroicon-o-paper-airplane') ->action('sendHotelAlert') ->color('danger') ->requiresConfirmation() ->modalHeading('Hotel Alert Versturen') ->modalDescription('Dit bericht wordt naar ALLE online gebruikers gestuurd. Doorgaan?'), ]) ->schema([ TextInput::make('hotel_alert_message') ->label('Bericht') ->placeholder('Typ hier je alert bericht...') ->columnSpanFull(), ]), Section::make('📊 Activity Heatmap') ->description('Activiteit per uur (laatste 30 dagen)') ->icon('heroicon-o-chart-bar') ->columnSpanFull() ->afterHeader([ Action::make('refresh_heatmap') ->label('Vernieuwen') ->icon('heroicon-o-arrow-path') ->action('refreshDashboard') ->color('info'), ]) ->schema([ Placeholder::make('activity_heatmap') ->label('') ->content(fn () => $this->getActivityHeatmapHtml()), ]), Section::make('Emulator Monitoring') ->description('Monitor de emulator status en ontvang alerts bij problemen') ->icon('heroicon-o-server') ->columns(2) ->afterHeader([ Action::make('check_emulator') ->label('Check Nu') ->action('checkEmulator') ->color('warning'), Action::make('save_emulator') ->label('Opslaan') ->action('saveEmulator') ->color('primary'), Action::make('start_emulator') ->label('Start') ->icon('heroicon-o-play') ->color('success') ->requiresConfirmation() ->action('startEmulator'), Action::make('stop_emulator') ->label('Stop') ->icon('heroicon-o-stop') ->color('danger') ->requiresConfirmation() ->action('stopEmulator'), Action::make('restart_emulator') ->label('Restart') ->icon('heroicon-o-arrow-path') ->color('warning') ->requiresConfirmation() ->action('restartEmulator'), ]) ->schema([ Toggle::make('alert_emulator_enabled') ->label('Emulator Monitoring Inschakelen') ->helperText('Stuurt een alert wanneer de emulator offline gaat'), Placeholder::make('emulator_status') ->label('') ->content(function () { $label = '
'; $label .= ''; $label .= 'Huidige Status'; $label .= ''; $label .= '
'; return new HtmlString($label . $this->getEmulatorStatusHtml()); }), ]), Section::make('📜 Emulator Logs') ->description('Bekijk live logs van de emulator') ->icon('heroicon-o-document-text') ->columnSpanFull() ->schema([ Placeholder::make('log_viewer') ->label('') ->content(fn () => view('filament.components.emulator-log-viewer')), ]), Section::make('DDoS Detectie') ->description('Monitor verkeer en detecteer mogelijke DDoS aanvallen') ->icon('heroicon-o-shield-exclamation') ->columns(2) ->afterHeader([ Action::make('run_ddos_check') ->label('Run Check') ->action('runDdosCheck') ->color('warning'), Action::make('save_ddos') ->label('Opslaan') ->action('saveDdos') ->color('primary'), ]) ->schema([ Toggle::make('alert_ddos_enabled') ->label('DDoS Monitoring Inschakelen'), TextInput::make('alert_ddos_threshold') ->label('Drempel (requests per IP)') ->numeric() ->minValue(10) ->default(100), Toggle::make('alert_ddos_auto_block') ->label('Automatisch IPs Blokkeren') ->helperText('Blokkeert verdachte IPs automatisch via iptables'), Placeholder::make('blocked_ips') ->label('') ->content(function () { $label = '
'; $label .= ''; $label .= 'Geblokkeerde IPs'; $label .= ''; $label .= '
'; return new HtmlString($label . $this->getBlockedIpsHtml()); }), ]), Section::make('Error Monitoring') ->description('Monitor kritieke errors en exceptions') ->icon('heroicon-o-exclamation-circle') ->columns(2) ->afterHeader([ Action::make('save_errors') ->label('Opslaan') ->action('saveErrors') ->color('primary'), ]) ->schema([ Toggle::make('alert_errors_enabled') ->label('Error Monitoring Inschakelen'), TextInput::make('alert_error_threshold') ->label('Error Drempel (per 5 min)') ->numeric() ->minValue(1) ->default(10), Select::make('alert_min_severity') ->label('Minimale Ernst') ->options([ AlertSeverity::INFO->value => 'Info', AlertSeverity::WARNING->value => 'Warning', AlertSeverity::ERROR->value => 'Error', AlertSeverity::CRITICAL->value => 'Critical', ]) ->default(AlertSeverity::ERROR->value), ]), Section::make('Statistieken') ->description('Overzicht van recente alerts') ->icon('heroicon-o-chart-bar') ->schema([ Placeholder::make('stats') ->label('') ->content(fn () => $this->getStatsHtml()), ]), ]) ->statePath('data'); } private function getSetting(string $key, string $default = ''): string { return setting($key, $default); } private function getSettingBool(string $key, bool $default = false): bool { return (bool) setting($key, $default); } public function getEmulatorStatusHtml(): HtmlString { $rconService = new RconService; $isConnected = $rconService->isConnected(); if ($isConnected) { return new HtmlString('● ONLINE - Emulator is verbonden'); } return new HtmlString('● OFFLINE - Emulator is niet bereikbaar'); } public function getBlockedIpsHtml(): HtmlString { $blockedIps = DDoSDetectionCommand::getBlockedIps(); if ($blockedIps === []) { return new HtmlString('Geen geblokkeerde IPs'); } $list = implode(', ', $blockedIps); return new HtmlString("{$list}"); } public function getStatsHtml(): HtmlString { $total = AlertLog::count(); $unread = AlertLog::unread()->count(); $critical = AlertLog::critical()->count(); $resolved = AlertLog::where('is_read', true)->count(); $today = AlertLog::where('created_at', '>=', now()->startOfDay())->count(); $week = AlertLog::where('created_at', '>=', now()->startOfWeek())->count(); $html = ''; $html .= '
'; // Main stats grid $html .= '
'; $html .= '
' . $total . '
Totaal Alerts
'; $html .= '
' . $unread . '
Ongelezen
'; $html .= '
' . $critical . '
Kritiek
'; $html .= '
'; // Divider $html .= '
'; // Secondary stats $html .= '
Laatste Periode
'; $html .= '
'; $html .= '
' . $today . '
Vandaag
'; $html .= '
' . $week . '
Deze Week
'; $html .= '
' . $resolved . '
Opgelost
'; $html .= '
'; $html .= '
'; return new HtmlString($html); } public function saveEmail(): void { $this->saveSettings(['alert_email_enabled', 'alert_email_address']); } public function saveDiscord(): void { $this->saveSettings(['alert_discord_enabled', 'alert_discord_webhook_url']); } public function saveEmulator(): void { $this->saveSettings(['alert_emulator_enabled']); } public function saveDdos(): void { $this->saveSettings(['alert_ddos_enabled', 'alert_ddos_threshold', 'alert_ddos_auto_block']); } public function saveErrors(): void { $this->saveSettings(['alert_errors_enabled', 'alert_error_threshold', 'alert_min_severity']); } private function saveSettings(array $keys): void { foreach ($keys as $key) { $value = $this->data[$key] ?? null; WebsiteSetting::updateOrCreate( ['key' => $key], ['value' => is_bool($value) ? ($value ? '1' : '0') : (string) $value], ); } Notification::make() ->success() ->title(__('Saved')) ->body(__('Alert settings have been saved successfully.')) ->send(); } public function clearAllLogs(): void { AlertLog::truncate(); Cache::flush(); Notification::make() ->success() ->title('🗑️ Alle Logs Geleegd!') ->body('Alle logs zijn gewist.') ->send(); $this->fillForm(); } public function getOnlineUsersHtml(): HtmlString { try { $count = DB::connection('mysql')->table('users')->where('online', '1')->count(); $users = DB::connection('mysql')->table('users') ->where('online', '1') ->orderByDesc('last_login') ->limit(5) ->get(['username', 'rank']); $html = '
' . $count . '
'; if ($users->count() > 0) { $html .= '
'; foreach ($users as $u) { $html .= $u->username . ', '; } $html = rtrim($html, ', ') . '
'; } return new HtmlString($html); } catch (\Exception) { return new HtmlString('Could not load'); } } public function getUptimeHtml(): HtmlString { try { $serviceName = setting('emulator_service_name', 'emulator'); $result = Process::timeout(5)->run("systemctl show {$serviceName} --property=ActiveEnterTimestamp --no-pager 2>/dev/null | cut -d= -f2"); if ($result->successful() && ! in_array(trim($result->output()), ['', '0'], true)) { $startTime = trim($result->output()); $startTimestamp = strtotime($startTime); $now = time(); $diff = $now - $startTimestamp; $hours = floor($diff / 3600); $minutes = floor(($diff % 3600) / 60); $seconds = $diff % 60; if ($hours > 0) { $uptime = "{$hours}u {$minutes}m {$seconds}s"; } elseif ($minutes > 0) { $uptime = "{$minutes}m {$seconds}s"; } else { $uptime = "{$seconds}s"; } return new HtmlString('
' . $uptime . '
'); } return new HtmlString('Not active'); } catch (\Exception) { return new HtmlString('Could not load'); } } public function getServerLoadHtml(): HtmlString { try { $load = sys_getloadavg(); $cpuCount = (int) Process::run('nproc 2>/dev/null')->output() ?: 1; $memoryUsage = Process::run("free -m | awk '/Mem:/ {printf \"%d%% (%dMB / %dMB)\", $3/$2*100, $3, $2}'")->output(); $diskUsage = Process::run("df -h / | awk 'NR==2 {print $5 \" used\"}'")->output(); $html = '
'; $html .= '
CPU Load: ' . $load[0] . ' (' . $cpuCount . ' cores)
'; if ($memoryUsage) { $html .= '
Memory: ' . trim($memoryUsage) . '
'; } if ($diskUsage) { $html .= '
Disk: ' . trim($diskUsage) . '
'; } $html .= '
'; return new HtmlString($html); } catch (\Exception) { return new HtmlString('Could not load'); } } public function refreshDashboard(): void { Notification::make() ->success() ->title('Dashboard') ->body('Statistieken vernieuwd') ->send(); } public function sendHotelAlert(): void { if (empty($this->data['hotel_alert_message'])) { Notification::make() ->warning() ->title('Bericht Vereist') ->body('Vul eerst een bericht in.') ->send(); return; } try { $rcon = new RconService; $result = $rcon->sendCommand('hotelalert', ['message' => $this->data['hotel_alert_message']]); Notification::make() ->success() ->title('Hotel Alert') ->body('Bericht verzonden naar alle online gebruikers!') ->send(); $this->data['hotel_alert_message'] = ''; } catch (\Exception $e) { Notification::make() ->danger() ->title('Fout') ->body('Kon hotel alert niet versturen: ' . $e->getMessage()) ->send(); } } public function getActivityHeatmapHtml(): HtmlString { try { $result = DB::connection('mysql')->select(' SELECT HOUR(FROM_UNIXTIME(timestamp)) as hour, COUNT(*) as count FROM room_enter_log WHERE timestamp > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY)) GROUP BY hour ORDER BY hour '); $data = []; $maxCount = 0; foreach ($result as $r) { $data[(int) $r->hour] = (int) $r->count; if ((int) $r->count > $maxCount) { $maxCount = (int) $r->count; } } $html = '
'; $html .= '
'; for ($hour = 0; $hour < 24; $hour++) { $count = $data[$hour] ?? 0; $height = $maxCount > 0 ? max(5, ($count / $maxCount) * 120) : 5; $color = $this->getHeatmapColor($count, $maxCount); $html .= '
'; $html .= '
'; $html .= '' . str_pad((string) $hour, 2, '0', STR_PAD_LEFT) . ''; $html .= '
'; } $html .= '
'; // Legend $html .= '
'; $html .= '
Laag
'; $html .= '
Gemiddeld
'; $html .= '
Druk
'; $html .= '
Heel druk
'; $html .= '
'; // Stats $totalEntries = array_sum($data); $busiestHour = array_search(max($data), $data); $html .= '
'; $html .= 'Totaal: ' . $totalEntries . ' kamer bezoeken (30 dagen)'; $html .= 'Drukste uur: ' . $busiestHour . ':00'; $html .= '
'; $html .= '
'; return new HtmlString($html); } catch (\Exception $e) { return new HtmlString('Kan heatmap niet laden: ' . $e->getMessage() . ''); } } private function getHeatmapColor(int $count, int $max): string { if ($max === 0) { return '#374151'; } $ratio = $count / $max; if ($ratio < 0.25) { return '#3b82f6'; } if ($ratio < 0.5) { return '#22c55e'; } if ($ratio < 0.75) { return '#eab308'; } return '#ef4444'; } public function testEmail(): void { if (empty($this->data['alert_email_address'])) { Notification::make() ->warning() ->title('E-mail Adres Vereist') ->body('Vul eerst een e-mail adres in.') ->send(); return; } if (empty($this->data['alert_email_enabled'])) { Notification::make() ->warning() ->title('E-mail Uitgeschakeld') ->body('Schakel eerst e-mail meldingen in.') ->send(); return; } try { Cache::forget('website_settings'); $alertService = app(AlertService::class); $result = $alertService->testAlert(); if (empty($result)) { Notification::make() ->warning() ->title('Geen Kanalen Geconfigureerd') ->body('Schakel eerst e-mail alerts in en vul een e-mail adres in.') ->send(); return; } if (isset($result['email']) && $result['email'] === 'success') { Notification::make() ->success() ->title('Test Verzonden') ->body('Test e-mail is verzonden naar ' . $this->data['alert_email_address']) ->send(); } elseif (isset($result['email']) && str_contains($result['email'], 'failed')) { Notification::make() ->danger() ->title(__('Test Failed')) ->body(str_replace('failed: ', '', $result['email'])) ->send(); } else { Notification::make() ->danger() ->title(__('Test Failed')) ->body('E-mail alerts zijn niet ingeschakeld of niet geconfigureerd.') ->send(); } } catch (\Exception $e) { Notification::make() ->danger() ->title(__('Error')) ->body($e->getMessage()) ->send(); } } public function testDiscord(): void { if (empty($this->data['alert_discord_webhook_url'])) { Notification::make() ->warning() ->title('Webhook URL Vereist') ->body('Vul eerst een Discord webhook URL in.') ->send(); return; } if (empty($this->data['alert_discord_enabled'])) { Notification::make() ->warning() ->title('Discord Uitgeschakeld') ->body('Schakel eerst Discord meldingen in.') ->send(); return; } try { Cache::forget('website_settings'); $alertService = app(AlertService::class); $response = Http::post($this->data['alert_discord_webhook_url'], [ 'username' => 'Atom CMS Alerts', 'embeds' => [ [ 'title' => 'Test Alert', 'description' => 'Dit is een testmelding van het Atom CMS Alert Systeem.', 'color' => 3447003, 'footer' => ['text' => 'Atom CMS Alert System'], 'timestamp' => now()->toIso8601String(), ], ], ]); if ($response->successful()) { Notification::make() ->success() ->title('Test Verzonden') ->body('Testbericht is succesvol naar Discord gestuurd.') ->send(); } else { Notification::make() ->danger() ->title(__('Error')) ->body('Kon bericht niet versturen. Status: ' . $response->status()) ->send(); } } catch (\Exception $e) { Notification::make() ->danger() ->title(__('Error')) ->body($e->getMessage()) ->send(); } } public function checkEmulator(): void { Artisan::call('monitor:emulator', ['--notify-online' => true]); $rconService = new RconService; if ($rconService->isConnected()) { Notification::make() ->success() ->title('Emulator Online') ->body('De emulator is online en reageert.') ->send(); } else { Notification::make() ->danger() ->title('Emulator Offline') ->body('De emulator is niet bereikbaar via RCON.') ->send(); } $this->fillForm(); } public function startEmulator(): void { $serviceName = setting('emulator_service_name', 'arcturus'); $result = Process::timeout(30)->run("sudo systemctl start {$serviceName} 2>&1"); if ($result->successful()) { Notification::make() ->success() ->title('Emulator Gestart') ->body("Service '{$serviceName}' is gestart.") ->send(); } else { Notification::make() ->danger() ->title('Start Mislukt') ->body($result->output() ?: 'Kon de emulator niet starten.') ->send(); } } public function stopEmulator(): void { $serviceName = setting('emulator_service_name', 'arcturus'); $result = Process::timeout(30)->run("sudo systemctl stop {$serviceName} 2>&1"); if ($result->successful()) { Notification::make() ->success() ->title('Emulator Gestopt') ->body("Service '{$serviceName}' is gestopt.") ->send(); } else { Notification::make() ->danger() ->title('Stop Mislukt') ->body($result->output() ?: 'Kon de emulator niet stoppen.') ->send(); } } public function restartEmulator(): void { $serviceName = setting('emulator_service_name', 'arcturus'); $result = Process::timeout(60)->run("sudo systemctl restart {$serviceName} 2>&1"); if ($result->successful()) { Notification::make() ->success() ->title('Emulator Herstart') ->body("Service '{$serviceName}' is herstart.") ->send(); } else { Notification::make() ->danger() ->title('Herstart Mislukt') ->body($result->output() ?: 'Kon de emulator niet herstarten.') ->send(); } } public function runDdosCheck(): void { Artisan::call('monitor:ddos', [ '--threshold' => $this->data['alert_ddos_threshold'] ?? 100, ]); Notification::make() ->info() ->title('DDoS Check Gestart') ->body('De DDoS detectie check is uitgevoerd.') ->send(); } private function formatBytes(int $bytes): string { if ($bytes >= 1073741824) { return round($bytes / 1073741824, 2) . ' GB'; } elseif ($bytes >= 1048576) { return round($bytes / 1048576, 2) . ' MB'; } elseif ($bytes >= 1024) { return round($bytes / 1024, 2) . ' KB'; } return $bytes . ' B'; } private function getCurrentSiteUrl(): string { return setting('site_url', config('app.url', 'https://epicnabbo.nl')); } private function clearSettingsCache(): void { Cache::forget('website_settings'); SettingsService::clearCache(); } }