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 .= '
';
$html .= '
';
$html .= '
';
// Divider
$html .= '
';
// Secondary stats
$html .= '
Laatste Periode
';
$html .= '
';
$html .= '
';
$html .= '
';
$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 .= '
';
$html .= '
';
$html .= '
';
$html .= '
';
$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();
}
}