Files
Atomcms-edit/app/Filament/Pages/Monitoring/AlertSettings.php
T

955 lines
41 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace App\Filament\Pages\Monitoring;
use App\Console\Commands\DDoSDetectionCommand;
use App\Enums\AlertSeverity;
use App\Enums\AlertType;
use App\Models\Miscellaneous\AlertLog;
use App\Models\Miscellaneous\WebsiteSetting;
use App\Services\AlertService;
use App\Services\RconService;
use App\Services\SettingsService;
use Carbon\Carbon;
use Filament\Actions\Action;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\HtmlString;
final class AlertSettings extends Page implements HasForms
{
use InteractsWithForms;
#[\Override]
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-command-line';
#[\Override]
protected static string|\UnitEnum|null $navigationGroup = 'Commandocentrum';
#[\Override]
protected static ?string $navigationLabel = 'Commandocentrum';
#[\Override]
protected static ?string $title = 'Commandocentrum';
#[\Override]
protected static ?string $slug = 'commandocentrum';
#[\Override]
protected string $view = 'filament.pages.monitoring.alert-settings';
public array $data = [];
public function mount(): void
{
$this->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 = '<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">';
$label .= '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Online Gebruikers</span>';
$label .= '<span style="height:1px;flex:1;background:linear-gradient(90deg,rgba(100,116,139,0.3),transparent);"></span>';
$label .= '</div>';
return new HtmlString($label . $this->getOnlineUsersHtml());
}),
Placeholder::make('uptime')
->label('')
->content(function () {
$label = '<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">';
$label .= '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Uptime</span>';
$label .= '<span style="height:1px;flex:1;background:linear-gradient(90deg,rgba(100,116,139,0.3),transparent);"></span>';
$label .= '</div>';
return new HtmlString($label . $this->getUptimeHtml());
}),
Placeholder::make('server_load')
->label('')
->content(function () {
$label = '<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">';
$label .= '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Server Load</span>';
$label .= '<span style="height:1px;flex:1;background:linear-gradient(90deg,rgba(100,116,139,0.3),transparent);"></span>';
$label .= '</div>';
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 = '<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">';
$label .= '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Huidige Status</span>';
$label .= '<span style="height:1px;flex:1;background:linear-gradient(90deg,rgba(100,116,139,0.3),transparent);"></span>';
$label .= '</div>';
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 = '<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">';
$label .= '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Geblokkeerde IPs</span>';
$label .= '<span style="height:1px;flex:1;background:linear-gradient(90deg,rgba(100,116,139,0.3),transparent);"></span>';
$label .= '</div>';
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('<span class="text-green-600 font-semibold">● ONLINE - Emulator is verbonden</span>');
}
return new HtmlString('<span class="text-red-600 font-semibold">● OFFLINE - Emulator is niet bereikbaar</span>');
}
public function getBlockedIpsHtml(): HtmlString
{
$blockedIps = DDoSDetectionCommand::getBlockedIps();
if ($blockedIps === []) {
return new HtmlString('<span class="text-gray-500">Geen geblokkeerde IPs</span>');
}
$list = implode(', ', $blockedIps);
return new HtmlString("<span class=\"text-red-600\">{$list}</span>");
}
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 = '<style>';
$html .= '.stats-card { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-radius: 12px; padding: 16px; color: white; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif; }';
$html .= '.stats-card .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }';
$html .= '.stats-card .grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }';
$html .= '.stats-card .stat-box { text-align: center; padding: 14px 8px; background: rgba(255,255,255,0.05); border-radius: 10px; }';
$html .= '.stats-card .stat-icon { font-size: 20px; margin-bottom: 6px; }';
$html .= '.stats-card .stat-number { font-size: 28px; font-weight: 700; line-height: 1; }';
$html .= '.stats-card .stat-label { font-size: 9px; text-transform: uppercase; letter-spacing: 1px; color: #64748b; margin-top: 6px; }';
$html .= '.stats-card .divider { height: 1px; background: rgba(255,255,255,0.08); margin: 12px 0; }';
$html .= '.stats-card .mini-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }';
$html .= '.stats-card .mini-box { text-align: center; padding: 10px 6px; background: rgba(255,255,255,0.04); border-radius: 8px; }';
$html .= '.stats-card .mini-number { font-size: 18px; font-weight: 700; }';
$html .= '.stats-card .mini-label { font-size: 9px; text-transform: uppercase; letter-spacing: 0.5px; color: #64748b; margin-top: 2px; }';
$html .= '.stats-card .section-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #64748b; margin-bottom: 10px; }';
$html .= '</style>';
$html .= '<div class="stats-card">';
// Main stats grid
$html .= '<div class="grid-3">';
$html .= '<div class="stat-box"><div class="stat-number" style="color:#60a5fa;">' . $total . '</div><div class="stat-label">Totaal Alerts</div></div>';
$html .= '<div class="stat-box"><div class="stat-number" style="color:' . ($unread > 0 ? '#fbbf24' : '#4ade80') . ';">' . $unread . '</div><div class="stat-label">Ongelezen</div></div>';
$html .= '<div class="stat-box"><div class="stat-number" style="color:' . ($critical > 0 ? '#f87171' : '#4ade80') . ';">' . $critical . '</div><div class="stat-label">Kritiek</div></div>';
$html .= '</div>';
// Divider
$html .= '<div class="divider"></div>';
// Secondary stats
$html .= '<div class="section-title">Laatste Periode</div>';
$html .= '<div class="mini-grid">';
$html .= '<div class="mini-box"><div class="mini-number" style="color:#60a5fa;">' . $today . '</div><div class="mini-label">Vandaag</div></div>';
$html .= '<div class="mini-box"><div class="mini-number" style="color:#818cf8;">' . $week . '</div><div class="mini-label">Deze Week</div></div>';
$html .= '<div class="mini-box"><div class="mini-number" style="color:#4ade80;">' . $resolved . '</div><div class="mini-label">Opgelost</div></div>';
$html .= '</div>';
$html .= '</div>';
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 = '<div class="text-2xl font-bold text-green-400">' . $count . '</div>';
if ($users->count() > 0) {
$html .= '<div class="text-xs text-gray-400 mt-1">';
foreach ($users as $u) {
$html .= $u->username . ', ';
}
$html = rtrim($html, ', ') . '</div>';
}
return new HtmlString($html);
} catch (\Exception) {
return new HtmlString('<span class="text-red-400">Could not load</span>');
}
}
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('<div class="text-sm text-green-400">' . $uptime . '</div>');
}
return new HtmlString('<span class="text-yellow-400">Not active</span>');
} catch (\Exception) {
return new HtmlString('<span class="text-red-400">Could not load</span>');
}
}
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 = '<div class="text-sm space-y-1">';
$html .= '<div><span class="text-gray-400">CPU Load:</span> <span class="text-green-400">' . $load[0] . '</span> (' . $cpuCount . ' cores)</div>';
if ($memoryUsage) {
$html .= '<div><span class="text-gray-400">Memory:</span> <span class="text-blue-400">' . trim($memoryUsage) . '</span></div>';
}
if ($diskUsage) {
$html .= '<div><span class="text-gray-400">Disk:</span> <span class="text-purple-400">' . trim($diskUsage) . '</span></div>';
}
$html .= '</div>';
return new HtmlString($html);
} catch (\Exception) {
return new HtmlString('<span class="text-red-400">Could not load</span>');
}
}
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 = '<div class="space-y-2">';
$html .= '<div class="grid grid-cols-24 gap-1 items-end" style="height: 150px; display: grid; grid-template-columns: repeat(24, 1fr); align-items: flex-end;">';
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 .= '<div class="flex flex-col items-center justify-end" style="height: 100%;">';
$html .= '<div style="width: 100%; height: ' . $height . 'px; background-color: ' . $color . '; border-radius: 2px;" title="' . $count . ' entries om ' . $hour . ':00"></div>';
$html .= '<span class="text-xs text-gray-400 mt-1">' . str_pad((string) $hour, 2, '0', STR_PAD_LEFT) . '</span>';
$html .= '</div>';
}
$html .= '</div>';
// Legend
$html .= '<div class="flex items-center justify-center gap-4 mt-4 text-xs text-gray-400">';
$html .= '<div class="flex items-center gap-1"><div class="w-3 h-3 rounded" style="background-color: #3b82f6;"></div> Laag</div>';
$html .= '<div class="flex items-center gap-1"><div class="w-3 h-3 rounded" style="background-color: #22c55e;"></div> Gemiddeld</div>';
$html .= '<div class="flex items-center gap-1"><div class="w-3 h-3 rounded" style="background-color: #eab308;"></div> Druk</div>';
$html .= '<div class="flex items-center gap-1"><div class="w-3 h-3 rounded" style="background-color: #ef4444;"></div> Heel druk</div>';
$html .= '</div>';
// Stats
$totalEntries = array_sum($data);
$busiestHour = array_search(max($data), $data);
$html .= '<div class="flex justify-between mt-4 text-sm text-gray-300">';
$html .= '<span>Totaal: <strong class="text-white">' . $totalEntries . '</strong> kamer bezoeken (30 dagen)</span>';
$html .= '<span>Drukste uur: <strong class="text-white">' . $busiestHour . ':00</strong></span>';
$html .= '</div>';
$html .= '</div>';
return new HtmlString($html);
} catch (\Exception $e) {
return new HtmlString('<span class="text-red-400">Kan heatmap niet laden: ' . $e->getMessage() . '</span>');
}
}
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();
}
}