Files
Atomcms-edit/app/Filament/Pages/Monitoring/AlertSettings.php
T
root 574b5d6e17 fix: standardize language to English in README and AlertSettings
feat: add 24 model factories for Help, Shop, Community, Game, User domains

- Translate mixed Dutch/English strings in README.md and AlertSettings.php
- Add HasFactory trait to 23 models
- Create factories for Help (6), Shop (4), Community (5), Game (2), User (7)
2026-05-23 16:57:44 +02:00

2855 lines
139 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\EmulatorUpdateService;
use App\Services\NitroUpdateService;
use App\Services\RconService;
use App\Services\SettingsService;
use App\Services\SystemFixService;
use App\Services\UpdateHistoryService;
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'),
'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', '/var/www/atomcms/nitro-client'),
'nitro_renderer_path' => $this->getSetting('nitro_renderer_path', '/var/www/atomcms/nitro-renderer'),
'nitro_build_path' => $this->getSetting('nitro_build_path', '/var/www/atomcms/nitro-client/dist'),
'nitro_webroot' => $this->getSetting('nitro_webroot', '/var/www/Client'),
'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('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('Emulator Updates (.jar)')
->description('100% automatisch emulator updaten vanaf GitHub')
->icon('heroicon-o-archive-box-arrow-down')
->columns(2)
->afterHeader([
Action::make('check_updates')
->label('Check Updates')
->action('checkEmulatorUpdates')
->color('info'),
Action::make('save_update')
->label('Opslaan')
->action('saveEmulatorUpdate')
->color('primary'),
])
->schema([
TextInput::make('emulator_github_url')
->label('GitHub Repository URL')
->placeholder('https://github.com/gebruiker/emulator-repo')
->hint('Basis URL van je GitHub repo (voor pre-compiled JARs)')
->columnSpanFull(),
TextInput::make('emulator_source_repo')
->label('Source Repository URL (voor build vanaf source)')
->placeholder('https://github.com/gebruiker/emulator-source')
->hint('GitHub URL voor als je de emulator vanaf source wilt builden')
->columnSpanFull(),
TextInput::make('emulator_jar_direct_url')
->label('Directe .jar URL (optioneel)')
->placeholder('https://github.com/user/repo/raw/main/Latest_Compiled_Version/emulator.jar')
->hint('Als de auto-detect niet werkt, vul hier de directe URL in')
->columnSpanFull(),
TextInput::make('emulator_jar_path')
->label('Emulator Folder Pad')
->placeholder('/root/emulator'),
TextInput::make('emulator_source_path')
->label('Source Code Pad (voor build vanaf source)')
->placeholder('/var/www/emulator-source')
->hint('Locatie waar de source code wordt gekloond'),
TextInput::make('emulator_service_name')
->label('Emulator Service Naam')
->placeholder('arcturus'),
TextInput::make('emulator_database_host')
->label('Database Host')
->placeholder('127.0.0.1'),
TextInput::make('emulator_database_port')
->label('Database Port')
->placeholder('3306')
->default('3306'),
TextInput::make('emulator_database_name')
->label('Database Naam')
->placeholder('habbo'),
TextInput::make('emulator_database_username')
->label('Database Gebruiker')
->placeholder('root'),
TextInput::make('emulator_database_password')
->label('Database Wachtwoord')
->password()
->placeholder('••••••'),
Select::make('emulator_github_branch')
->label('Branch')
->options(function () {
$url = setting('emulator_github_url', '');
$repo = $this->parseRepoFromUrl($url);
if (! $repo) {
return ['main' => 'main'];
}
$branches = $this->fetchBranches($repo);
if ($branches === []) {
return ['main' => 'main'];
}
return $branches;
})
->default('main')
->hint('Branches worden automatisch herkend vanuit GitHub'),
Placeholder::make('installed_jar')
->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="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"/><line x1="16" y1="8" x2="2" y2="22"/><line x1="17.5" y1="15" x2="9" y2="15"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Huidige .jar</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->getInstalledJarHtml());
}),
Placeholder::make('current_version')
->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="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Versie</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->getEmulatorVersionHtml());
}),
Placeholder::make('update_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"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Update 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->getUpdateStatusHtml());
}),
Action::make('run_update')
->label('🔄 Update Nu (100% Automatisch)')
->action('runFullUpdate')
->color('success')
->visible(fn () => $this->isUpdateAvailable()),
Action::make('reset_update_date')
->label('Reset Update Datum')
->action('resetUpdateDate')
->color('warning')
->icon('heroicon-o-arrow-path'),
Action::make('force_check')
->label('🔄 Force Check')
->action('forceUpdateCheck')
->color('info')
->icon('heroicon-o-magnifying-glass'),
Action::make('switch_to_main')
->label('🔄 Switch to Branch')
->action('switchEmulatorBranch')
->color('warning')
->icon('heroicon-o-arrow-uturn-left')
->visible(fn () => strtolower((string) setting('emulator_github_branch', 'main')) !== strtolower((string) setting('emulator_installed_branch', 'main'))),
Placeholder::make('debug_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"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Debug Info</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->getDebugStatusHtml());
})
->columnSpanFull(),
]),
Section::make('Automatische Updates')
->description('Stel een automatische schema in voor emulator en SQL updates')
->icon('heroicon-o-clock')
->columns(3)
->afterHeader([
Action::make('save_auto_update')
->label('Opslaan')
->action('saveAutoUpdate')
->color('primary'),
])
->schema([
Toggle::make('auto_update_enabled')
->label('Automatische Updates Inschakelen')
->columnSpan(3),
TextInput::make('auto_update_schedule')
->label('Tijd (HH:MM)')
->placeholder('03:00')
->columnSpan(1),
TextInput::make('auto_update_days')
->label('Dagen (0-6, kommagescheiden)')
->placeholder('0,6')
->hint('0 = Zondag, 1 = Maandag, ..., 6 = Zaterdag')
->columnSpan(2),
Placeholder::make('auto_update_next')
->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="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Volgende Geplande Update</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->getNextAutoUpdateHtml());
}),
]),
Section::make('Database Updates (SQL)')
->description('Automatisch SQL updates uitvoeren van GitHub')
->icon('heroicon-o-server')
->columns(2)
->afterHeader([
Action::make('check_sql_updates')
->label('Check SQL Updates')
->action('checkSqlUpdates')
->color('info'),
Action::make('run_sql_updates')
->label('Run SQL Updates')
->action('runSqlUpdates')
->color('warning'),
Action::make('fix_all')
->label('Auto Fix All')
->action('fixAll')
->color('success')
->icon('heroicon-o-wrench'),
])
->schema([
Placeholder::make('sql_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"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">SQL 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->getSqlStatusHtml());
}),
Placeholder::make('sql_last_update')
->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"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Laatste SQL Update</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->getSqlLastUpdateHtml());
}),
]),
Section::make('Nitro Client Updates')
->description('Automatisch Nitro client en renderer updaten en builden')
->icon('heroicon-o-device-phone-mobile')
->columns(2)
->afterHeader([
Action::make('check_nitro_updates')
->label('Check Updates')
->action('checkNitroUpdates')
->color('info'),
Action::make('build_nitro')
->label('Build & Deploy')
->action('buildNitro')
->color('warning'),
Action::make('save_nitro')
->label('Opslaan')
->action('saveNitro')
->color('primary'),
Action::make('switch_nitro_main')
->label('🔄 Switch to Branch')
->action('switchNitroBranch')
->color('warning')
->icon('heroicon-o-arrow-uturn-left')
->visible(fn () => strtolower((string) setting('nitro_github_branch', 'main')) !== strtolower((string) setting('nitro_installed_branch', 'main'))),
])
->schema([
TextInput::make('nitro_github_url')
->label('GitHub Client URL')
->placeholder('https://github.com/gebruiker/Nitro-V3')
->hint('Auto-detecteert ook de renderer repo')
->columnSpanFull(),
TextInput::make('nitro_client_path')
->label('Client Pad')
->placeholder('/var/www/atomcms/nitro-client'),
TextInput::make('nitro_renderer_path')
->label('Renderer Pad')
->placeholder('/var/www/atomcms/nitro-renderer'),
TextInput::make('nitro_build_path')
->label('Build Output Pad')
->placeholder('/var/www/atomcms/nitro-client/dist'),
TextInput::make('nitro_webroot')
->label('Web Root Pad')
->placeholder('/var/www/Client'),
Select::make('nitro_github_branch')
->label('Branch')
->options(function () {
$url = setting('nitro_github_url', '');
$repo = $this->parseRepoFromUrl($url);
if (! $repo) {
return ['main' => 'main'];
}
$branches = $this->fetchBranches($repo);
if ($branches === []) {
return ['main' => 'main'];
}
return $branches;
})
->default('main')
->hint('Branches worden automatisch herkend vanuit GitHub'),
Placeholder::make('nitro_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"><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;">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->getNitroStatusHtml());
}),
Placeholder::make('nitro_auto_schedule')
->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"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Auto Update Schema</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->getNitroScheduleHtml());
}),
Toggle::make('nitro_auto_update_enabled')
->label('Automatische Updates Inschakelen')
->columnSpanFull(),
TextInput::make('nitro_auto_update_schedule')
->label('Tijd (HH:MM)')
->placeholder('03:00')
->columnSpan(1),
TextInput::make('nitro_auto_update_days')
->label('Dagen (0-6, kommagescheiden)')
->placeholder('0,6')
->hint('0 = Zondag, 1 = Maandag, ..., 6 = Zaterdag')
->columnSpan(2),
Placeholder::make('nitro_auto_update_next')
->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="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Volgende Geplande Update</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->getNitroAutoUpdateNextHtml());
}),
]),
Section::make('Nitro Config Generator')
->description('Genereer en beheer Nitro client configuraties automatisch')
->icon('heroicon-o-cog-6-tooth')
->columns(2)
->afterHeader([
Action::make('generate_nitro_configs')
->label('Genereer Configs')
->action('generateNitroConfigs')
->color('success'),
Action::make('save_nitro_url')
->label('Opslaan URL')
->action('saveNitroUrl')
->color('primary'),
])
->schema([
TextInput::make('nitro_site_url')
->label('Website URL')
->placeholder('https://epicnabbo.nl')
->hint('Basis URL voor alle client configuraties')
->columnSpanFull(),
Toggle::make('nitro_auto_update_configs')
->label('Automatisch configs bijwerken na deploy')
->helperText('Bij elke deploy worden de URLs automatisch bijgewerkt'),
Placeholder::make('nitro_config_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"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Config 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->getNitroConfigStatusHtml());
}),
Placeholder::make('nitro_config_preview')
->label('')
->content(function () {
$label = '<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">';
$label .= '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;">Actuele Configs</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->getNitroConfigPreviewHtml());
}),
]),
Section::make('Statistieken')
->description('Overzicht van recente alerts')
->icon('heroicon-o-chart-bar')
->schema([
Placeholder::make('stats')
->label('')
->content(fn () => $this->getStatsHtml()),
]),
Section::make('Update Geschiedenis')
->description('Overzicht van alle updates')
->icon('heroicon-o-clock')
->schema([
Placeholder::make('update_history')
->label('')
->content(fn () => $this->getUpdateHistoryHtml())
->columnSpanFull(),
]),
])
->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 getUpdateHistoryHtml(): HtmlString
{
$historyService = app(UpdateHistoryService::class);
return new HtmlString($historyService->getHtml());
}
public function saveErrors(): void
{
$this->saveSettings(['alert_errors_enabled', 'alert_error_threshold', 'alert_min_severity']);
}
public function saveEmulatorUpdate(): void
{
$this->saveSettings(['emulator_github_url', 'emulator_source_repo', 'emulator_jar_direct_url', 'emulator_jar_path', 'emulator_source_path', 'emulator_service_name', 'emulator_github_branch', 'emulator_database_host', 'emulator_database_port', 'emulator_database_name', 'emulator_database_username', 'emulator_database_password']);
Cache::forget('emulator_latest_version');
Cache::forget('website_settings');
SettingsService::clearCache();
Notification::make()
->success()
->title(__('Saved'))
->body(__('Emulator update settings have been saved.'))
->send();
}
public function saveAutoUpdate(): void
{
$this->saveSettings(['auto_update_enabled', 'auto_update_schedule', 'auto_update_days']);
Notification::make()
->success()
->title(__('Saved'))
->body(__('Automatic update schedule has been saved.'))
->send();
}
public function runFullUpdate(): void
{
Cache::forget('website_settings');
SettingsService::clearCache();
$updateService = new EmulatorUpdateService;
if (! $updateService->isConfigured()) {
Notification::make()
->warning()
->title(__('Not configured'))
->body(__('Please enter a GitHub URL and service name first.'))
->send();
return;
}
Notification::make()
->info()
->title('Emulator Updaten...')
->body(__('Downloading, installing and restarting emulator...'))
->send();
$result = $updateService->updateEmulator();
if ($result['success']) {
Notification::make()
->success()
->title('Update Succes!')
->body($result['message'])
->send();
$alertService = app(AlertService::class);
$alertService->send(
AlertType::EMULATOR_ONLINE,
"Emulator succesvol geüpdatet naar v{$result['version']}",
['version' => $result['version'], 'jar' => $result['jar'] ?? 'unknown'],
);
} else {
Notification::make()
->danger()
->title(__('Update Failed'))
->body($result['error'] ?? 'Er is iets misgegaan')
->send();
}
$this->fillForm();
}
public function checkEmulatorUpdates(): void
{
Cache::forget('website_settings');
SettingsService::clearCache();
$updateService = new EmulatorUpdateService;
if (! $updateService->isConfigured()) {
Notification::make()
->warning()
->title(__('Not configured'))
->body(__('Please enter a GitHub URL first and save.'))
->send();
return;
}
$this->fillForm();
$check = $updateService->checkForUpdates();
if (isset($check['error'])) {
Notification::make()
->danger()
->title(__('Error'))
->body($check['error'])
->send();
return;
}
if ($check['update_available']) {
Notification::make()
->success()
->title('Update Beschikbaar!')
->body("Versie {$check['latest_version']} is beschikbaar (huidig: {$check['current_version']})")
->send();
} else {
Notification::make()
->info()
->title('Up-to-date')
->body("Emulator is al up-to-date: v{$check['latest_version']}")
->send();
}
$this->fillForm();
}
public function runEmulatorUpdate(): void
{
$this->downloadEmulatorJar();
}
public function isUpdateAvailable(): bool
{
Cache::forget('website_settings');
SettingsService::clearCache();
$updateService = new EmulatorUpdateService;
$check = $updateService->checkForUpdates();
$hasUpdate = $check['update_available'] ?? false;
$hasUrl = ($check['jar_url'] ?? null) !== null;
$isSourceBuild = ($check['type'] ?? '') === 'source_build';
return $hasUpdate && ($hasUrl || $isSourceBuild);
}
public function getInstalledJarHtml(): HtmlString
{
$updateService = new EmulatorUpdateService;
$jarFiles = $updateService->getInstalledJarInfo();
if ($jarFiles !== []) {
$html = '<div class="space-y-1">';
foreach ($jarFiles as $jar) {
$name = e($jar['name']);
$size = e($jar['size']);
$html .= "<div class=\"text-green-600 font-semibold\">📦 <span class=\"text-xs\">{$size}</span> {$name}</div>";
}
$html .= '</div>';
return new HtmlString($html);
}
$jar = $updateService->getInstalledJar();
if ($jar) {
return new HtmlString("<span class=\"text-green-600 font-semibold\">📦 {$jar}</span>");
}
return new HtmlString('<span class="text-gray-500">Geen .jar gevonden</span>');
}
public function getEmulatorVersionHtml(): HtmlString
{
$updateService = new EmulatorUpdateService;
$version = $updateService->getInstalledVersion();
return new HtmlString("<span class=\"text-blue-600 font-semibold\">v{$version}</span>");
}
public function getUpdateStatusHtml(): HtmlString
{
$updateService = new EmulatorUpdateService;
$check = $updateService->checkForUpdates();
if (! $updateService->isConfigured()) {
return new HtmlString('<span class="text-gray-500">Configureer een GitHub URL</span>');
}
if (isset($check['error'])) {
return new HtmlString('<span class="text-red-500">' . __('Error') . ': ' . $check['error'] . '</span>');
}
if ($check['type'] === 'manual') {
return new HtmlString('<span class="text-yellow-500">⚠️ Geen releases - handmatige download nodig</span>');
}
if ($check['update_available']) {
$jarInfo = '';
if ($check['jar_size'] ?? null) {
$jarInfo = " ({$check['jar_size']})";
}
$sourceBuild = ($check['type'] ?? '') === 'source_build';
$sourceText = $sourceBuild ? ' <span class="text-xs">(build vanaf source)</span>' : '';
return new HtmlString("<span class=\"text-green-600 font-semibold\">✅ Update beschikbaar: v{$check['latest_version']}{$jarInfo}{$sourceText}</span>");
}
$rateLimitedWarning = '';
if ($check['type'] === 'direct_url' && ($check['gitHub_rate_limited'] ?? false)) {
$rateLimitedWarning = ' <span class="text-xs text-yellow-500">(⚠️ GitHub API rate limited - kan updates met zelfde versie niet detecteren)</span>';
}
return new HtmlString('<span class="text-blue-500">✅ Up-to-date (v' . $check['latest_version'] . ')</span>' . $rateLimitedWarning);
}
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 resetUpdateDate(): void
{
Cache::forget('website_settings');
SettingsService::clearCache();
$updateService = new EmulatorUpdateService;
$updateService->resetInstalledDate();
Notification::make()
->success()
->title('Reset Voltooid')
->body(__('Update date reset. The next check will detect a new update.'))
->send();
$this->fillForm();
}
public function forceUpdateCheck(): void
{
Cache::forget('website_settings');
SettingsService::clearCache();
$updateService = new EmulatorUpdateService;
// Clear all cached update data
$updateService->resetInstalledDate();
// Clear direct URL cached data
$directUrl = setting('emulator_jar_direct_url');
if ($directUrl) {
WebsiteSetting::where('key', 'emulator_direct_url_info_' . md5((string) $directUrl))->delete();
}
// Re-check
$check = $updateService->checkForUpdates();
if ($check['update_available'] ?? false) {
Notification::make()
->success()
->title('Update Gevonden!')
->body('Er is een nieuwe emulator versie: v' . ($check['latest_version'] ?? '?'))
->send();
} else {
$message = 'Geen update gevonden (v' . ($check['latest_version'] ?? setting('emulator_version', '?')) . ')';
if ($check['type'] === 'direct_url' && ($check['gitHub_rate_limited'] ?? false)) {
$message .= ' - GitHub API rate limited';
}
Notification::make()
->info()
->title('Check Result')
->body($message)
->send();
}
$this->fillForm();
}
public function switchEmulatorBranch(): void
{
$updateService = new EmulatorUpdateService;
$targetBranch = setting('emulator_github_branch', 'main');
// Clear all caches
DB::table('website_settings')
->where('key', 'emulator_github_branch')
->update(['value' => $targetBranch]);
Cache::forget('website_settings');
Cache::flush();
SettingsService::clearCache();
$updateService->resetInstalledDate();
$directUrl = setting('emulator_jar_direct_url');
if ($directUrl) {
WebsiteSetting::where('key', 'emulator_direct_url_info_' . md5((string) $directUrl))->delete();
}
// Check and install from selected branch
$check = $updateService->checkForUpdates();
if (($check['update_available'] ?? false) && in_array($check['type'] ?? '', ['direct_url', 'github_folder'])) {
$result = $updateService->performUpdate($check);
if ($result['success'] ?? false) {
Notification::make()
->success()
->title("🔄 Switched to {$targetBranch}!")
->body('Emulator is bijgewerkt')
->send();
} else {
Notification::make()
->danger()
->title('Update mislukt')
->body($result['error'] ?? 'Onbekende fout')
->send();
}
} else {
// Force set installed branch even if no update
setting('emulator_installed_branch', $targetBranch);
SettingsService::clearCache();
Notification::make()
->info()
->title("Branch naar {$targetBranch} gezet")
->body('Geen nieuwe update beschikbaar')
->send();
}
$this->fillForm();
}
public function clearAllLogs(): void
{
$updateService = new EmulatorUpdateService;
$result = $updateService->clearAllLogs();
AlertLog::truncate();
Cache::flush();
Notification::make()
->success()
->title('🗑️ Alle Logs Geleegd!')
->body($result['message'] . ': ' . implode(', ', $result['cleared']))
->send();
$this->fillForm();
}
public function getDebugStatusHtml(): HtmlString
{
$updateService = new EmulatorUpdateService;
$debug = $updateService->debugStatus();
$html = '<style>';
$html .= '.emulator-debug { 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 .= '.emulator-debug .section { margin-bottom: 14px; }';
$html .= '.emulator-debug .section:last-child { margin-bottom: 0; }';
$html .= '.emulator-debug .section-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #64748b; margin-bottom: 8px; }';
$html .= '.emulator-debug .row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }';
$html .= '.emulator-debug .row:last-child { border-bottom: none; }';
$html .= '.emulator-debug .label { font-size: 12px; color: #94a3b8; }';
$html .= '.emulator-debug .value { font-size: 12px; font-weight: 600; font-family: Monaco, Menlo, monospace; }';
$html .= '.emulator-debug .badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 700; }';
$html .= '.emulator-debug .badge.success { background: rgba(34,197,94,0.2); color: #4ade80; }';
$html .= '.emulator-debug .badge.warning { background: rgba(251,191,36,0.2); color: #fbbf24; }';
$html .= '.emulator-debug .badge.danger { background: rgba(248,113,113,0.2); color: #f87171; }';
$html .= '.emulator-debug .badge.neutral { background: rgba(148,163,184,0.2); color: #94a3b8; }';
$html .= '.emulator-debug .jar-file { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: rgba(255,255,255,0.05); border-radius: 8px; margin-bottom: 4px; }';
$html .= '.emulator-debug .jar-file-icon { font-size: 14px; }';
$html .= '.emulator-debug .jar-file-name { font-size: 11px; font-family: Monaco, Menlo, monospace; color: #e2e8f0; word-break: break-all; }';
$html .= '.emulator-debug .jar-file-size { font-size: 10px; color: #64748b; white-space: nowrap; }';
$html .= '.emulator-debug .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }';
$html .= '.emulator-debug .stat-box { text-align: center; padding: 10px 8px; background: rgba(255,255,255,0.05); border-radius: 8px; }';
$html .= '.emulator-debug .stat-number { font-size: 16px; font-weight: 700; }';
$html .= '.emulator-debug .stat-label { font-size: 9px; text-transform: uppercase; letter-spacing: 0.5px; color: #64748b; margin-top: 2px; }';
$html .= '.emulator-debug .divider { height: 1px; background: rgba(255,255,255,0.1); margin: 12px 0; }';
$html .= '.emulator-debug .repo-tag { display: inline-flex; padding: 2px 6px; border-radius: 4px; font-size: 10px; font-family: Monaco, Menlo, monospace; }';
$html .= '.emulator-debug .repo-tag.configured { background: rgba(34,197,94,0.15); color: #4ade80; }';
$html .= '.emulator-debug .repo-tag.unset { background: rgba(148,163,184,0.1); color: #64748b; }';
$html .= '</style>';
$html .= '<div class="emulator-debug">';
// JAR Files Section
$html .= '<div class="section">';
$html .= '<div class="section-title">JAR Bestanden</div>';
if (! empty($debug['jar_files'])) {
foreach ($debug['jar_files'] as $jar) {
$html .= '<div class="jar-file">';
$html .= '<div class="jar-file-icon">📦</div>';
$html .= '<div style="flex:1;min-width:0;">';
$html .= '<div class="jar-file-name">' . e($jar['name']) . '</div>';
$html .= '</div>';
$html .= '<div class="jar-file-size">' . e($jar['size']) . '</div>';
$html .= '</div>';
}
} else {
$html .= '<div style="text-align:center;color:#64748b;font-size:11px;padding:8px;">Geen JAR gevonden</div>';
}
$html .= '<div style="font-size:10px;color:#475569;margin-top:6px;">' . e($debug['jar_path'] ?? '') . '</div>';
$html .= '</div>';
// Stats Grid
$installedOk = ! empty($debug['installed_date_formatted']);
$commitOk = ! empty($debug['source_commit']);
$dateOk = ! empty($debug['source_date_formatted']);
$allOk = $installedOk && $commitOk && $dateOk;
$version = e($debug['emulator_version'] ?: 'Onbekend');
$commit = $debug['source_commit'] ? substr(e($debug['source_commit']), 0, 8) : 'Onbekend';
$commitColor = $commitOk ? 'success' : 'neutral';
$dateColor = $dateOk ? 'success' : 'neutral';
$html .= '<div class="divider"></div>';
$html .= '<div class="grid-2">';
$html .= '<div class="stat-box">';
$html .= '<div class="stat-number" style="color:#fbbf24;">' . $version . '</div>';
$html .= '<div class="stat-label">Versie</div>';
$html .= '</div>';
$html .= '<div class="stat-box">';
$html .= '<div class="stat-number" style="color:' . ($allOk ? '#4ade80' : '#f87171') . ';">' . ($allOk ? '100%' : '!') . '</div>';
$html .= '<div class="stat-label">Tracking</div>';
$html .= '</div>';
$html .= '<div class="stat-box">';
$html .= '<div class="stat-number" style="color:#60a5fa;font-size:13px;">' . ($installedOk ? substr((string) $debug['installed_date_formatted'], 0, 10) : '—') . '</div>';
$html .= '<div class="stat-label">Geïnstalleerd</div>';
$html .= '</div>';
$html .= '<div class="stat-box">';
$html .= '<div class="stat-number badge badge-' . $dateColor . '">' . ($dateOk ? substr((string) $debug['source_date_formatted'], 0, 10) : '—') . '</div>';
$html .= '<div class="stat-label">Source Datum</div>';
$html .= '</div>';
$html .= '</div>';
// Commit Info
$html .= '<div class="divider"></div>';
$html .= '<div class="section">';
$html .= '<div class="section-title">Git Commit</div>';
$html .= '<div class="row">';
$html .= '<div class="label">SHA</div>';
$html .= '<div class="badge badge-' . $commitColor . '">' . $commit . '</div>';
$html .= '</div>';
$html .= '<div class="row">';
$html .= '<div class="label">Geïnstalleerd</div>';
$html .= '<div class="value" style="color:' . ($installedOk ? '#4ade80' : '#f87171') . ';">' . ($debug['installed_date_formatted'] ?: 'Nooit') . '</div>';
$html .= '</div>';
$html .= '</div>';
// Repos
$html .= '<div class="divider"></div>';
$html .= '<div class="section">';
$html .= '<div class="section-title">Repositories</div>';
$html .= '<div class="row">';
$html .= '<div class="label">JAR Repo</div>';
$repo = e($debug['github_repo'] ?: 'Niet geconfigureerd');
$html .= '<div class="repo-tag ' . ($debug['github_repo'] ? 'configured' : 'unset') . '">' . $repo . '</div>';
$html .= '</div>';
$html .= '<div class="row">';
$html .= '<div class="label">Source Repo</div>';
$sourceRepo = e($debug['source_repo'] ?: 'Niet geconfigureerd');
$html .= '<div class="repo-tag ' . ($debug['source_repo'] ? 'configured' : 'unset') . '">' . (strlen($sourceRepo) > 25 ? substr($sourceRepo, 0, 22) . '…' : $sourceRepo) . '</div>';
$html .= '</div>';
$html .= '<div class="row">';
$html .= '<div class="label">Branch</div>';
$branch = e($debug['github_branch'] ?: 'main');
$html .= '<div class="repo-tag ' . ($debug['github_branch'] ? 'configured' : 'unset') . '">' . $branch . '</div>';
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
return new HtmlString($html);
}
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) shell_exec('nproc 2>/dev/null') ?: 1;
$memoryUsage = shell_exec("free -m | awk '/Mem:/ {printf \"%d%% (%dMB / %dMB)\", $3/$2*100, $3, $2}'");
$diskUsage = shell_exec("df -h / | awk 'NR==2 {print $5 \" used\"}'");
$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();
}
public function checkSqlUpdates(): void
{
Cache::forget('website_settings');
SettingsService::clearCache();
$updateService = new EmulatorUpdateService;
$result = $updateService->checkForSqlUpdates();
if ($result['has_updates'] ?? false) {
Notification::make()
->warning()
->title('SQL Updates Beschikbaar!')
->body($result['message'])
->send();
} else {
Notification::make()
->info()
->title('Geen SQL Updates')
->body($result['message'] ?? 'Alle SQL updates zijn al toegepast')
->send();
}
$this->fillForm();
}
public function fixAll(): void
{
Cache::forget('website_settings');
SettingsService::clearCache();
$fixService = new SystemFixService;
$results = $fixService->checkAndFixAll();
$errors = [];
$fixed = [];
$ok = [];
foreach ($results as $result) {
if ($result['status'] === 'error') {
$errors[] = $result['item'] . ': ' . $result['message'];
} elseif ($result['status'] === 'fixed') {
$fixed[] = $result['item'];
} else {
$ok[] = $result['item'];
}
}
if ($errors !== []) {
Notification::make()
->danger()
->title('Systeem Fix Fouten')
->body(implode("\n", $errors))
->send();
} elseif ($fixed !== []) {
Notification::make()
->success()
->title('Systeem Gefixt!')
->body(count($fixed) . ' items automatisch gerepareerd: ' . implode(', ', $fixed))
->send();
} else {
Notification::make()
->info()
->title('Systeem OK')
->body('Alle checks geslaagd - geen reparaties nodig')
->send();
}
$this->fillForm();
}
public function runSqlUpdates(): void
{
Cache::forget('website_settings');
SettingsService::clearCache();
$updateService = new EmulatorUpdateService;
Notification::make()
->info()
->title('SQL Updates Uitvoeren...')
->body('Database updates worden toegepast.')
->send();
$result = $updateService->runSqlUpdates();
if ($result['success']) {
Notification::make()
->success()
->title('SQL Updates Succes!')
->body($result['message'])
->send();
if ($result['sql_updated'] ?? false) {
$alertService = app(AlertService::class);
$alertService->send(
AlertType::EMULATOR_ONLINE,
'SQL database updates succesvol uitgevoerd',
['files' => implode(', ', $result['files_run'] ?? [])],
);
}
} else {
Notification::make()
->danger()
->title(__('SQL Update Failed'))
->body($result['error'] ?? 'Er is iets misgegaan')
->send();
}
$this->fillForm();
}
public function getSqlStatusHtml(): HtmlString
{
Cache::forget('website_settings');
$updateService = new EmulatorUpdateService;
$result = $updateService->checkForSqlUpdates();
if ($result['has_updates'] ?? false) {
return new HtmlString('<span class="text-orange-500 font-semibold">⚠️ ' . $result['message'] . '</span>');
}
return new HtmlString('<span class="text-green-500 font-semibold">✅ ' . ($result['message'] ?? 'Up-to-date') . '</span>');
}
public function getNextAutoUpdateHtml(): HtmlString
{
$enabled = $this->getSettingBool('auto_update_enabled');
if (! $enabled) {
return new HtmlString('<span class="text-gray-500">Automatische updates uitgeschakeld</span>');
}
$scheduleTime = $this->getSetting('auto_update_schedule', '03:00');
$scheduleDays = $this->getSetting('auto_update_days', '0,6');
$allowedDays = array_map(intval(...), explode(',', $scheduleDays));
$dayNames = ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag'];
$days = implode(', ', array_map(fn ($d) => $dayNames[$d] ?? $d, $allowedDays));
$now = now();
$today = $now->dayOfWeek;
Carbon::parse($scheduleTime, $now->timezone);
$nextRun = $now->copy();
$daysUntil = 0;
for ($i = 0; $i <= 7; $i++) {
$checkDay = ($today + $i) % 7;
if (in_array($checkDay, $allowedDays)) {
$daysUntil = $i;
break;
}
}
$nextRun->addDays($daysUntil)->setTimeFromTimeString($scheduleTime);
return new HtmlString('<span class="text-green-600 font-semibold">📅 ' . $nextRun->format('d M Y, H:i') . ' (' . $days . ')</span>');
}
public function getSqlLastUpdateHtml(): HtmlString
{
$updateService = new EmulatorUpdateService;
$lastUpdate = $updateService->getLastSqlUpdate();
if ($lastUpdate) {
return new HtmlString('<span class="text-gray-600">' . Carbon::parse($lastUpdate)->format('d M Y, H:i') . '</span>');
}
return new HtmlString('<span class="text-gray-400">Nog nooit uitgevoerd</span>');
}
public function getNitroAutoUpdateNextHtml(): HtmlString
{
$enabled = $this->getSettingBool('nitro_auto_update_enabled');
if (! $enabled) {
return new HtmlString('<span class="text-gray-500">Automatische Nitro updates uitgeschakeld</span>');
}
$scheduleTime = $this->getSetting('nitro_auto_update_schedule', '03:00');
$scheduleDays = $this->getSetting('nitro_auto_update_days', '0,6');
$allowedDays = array_map(intval(...), explode(',', $scheduleDays));
$dayNames = ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag'];
$days = implode(', ', array_map(fn ($d) => $dayNames[$d] ?? $d, $allowedDays));
$now = now();
$today = $now->dayOfWeek;
Carbon::parse($scheduleTime, $now->timezone);
$nextRun = $now->copy();
$daysUntil = 0;
for ($i = 0; $i <= 7; $i++) {
$checkDay = ($today + $i) % 7;
if (in_array($checkDay, $allowedDays)) {
$daysUntil = $i;
break;
}
}
$nextRun->addDays($daysUntil)->setTimeFromTimeString($scheduleTime);
return new HtmlString('<span class="text-green-600 font-semibold">📅 ' . $nextRun->format('d M Y, H:i') . ' (' . $days . ')</span>');
}
public function saveNitro(): void
{
$this->saveSettings(['nitro_github_url', 'nitro_client_path', 'nitro_renderer_path', 'nitro_build_path', 'nitro_webroot', 'nitro_auto_update_enabled', 'nitro_auto_update_schedule', 'nitro_auto_update_days', 'nitro_github_branch']);
Cache::forget('website_settings');
SettingsService::clearCache();
Notification::make()
->success()
->title(__('Saved'))
->body('Nitro instellingen zijn opgeslagen.')
->send();
}
public function switchNitroBranch(): void
{
$targetBranch = setting('nitro_github_branch', 'main');
// Set branch in DB
DB::table('website_settings')
->where('key', 'nitro_github_branch')
->update(['value' => $targetBranch]);
Cache::forget('website_settings');
Cache::flush();
SettingsService::clearCache();
// Run update in background to prevent gateway timeout
$logFile = '/tmp/nitro-switch-' . date('Ymd-His') . '.log';
$artisan = base_path('artisan');
$php = config('app.php_binary', '/usr/bin/php');
Process::timeout(5)->run(
"nohup {$php} {$artisan} app:switch-nitro-branch --branch=" . escapeshellarg((string) $targetBranch) . " > {$logFile} 2>&1 &",
);
Notification::make()
->success()
->title("🔄 Switching to {$targetBranch}...")
->body('Build draait op de achtergrond. Resultaat verschijnt vanzelf.')
->send();
}
public function checkNitroUpdates(): void
{
Cache::forget('website_settings');
SettingsService::clearCache();
// Update last checked time
$settings = app(SettingsService::class);
$settings->set('nitro_last_checked', now()->toIso8601String());
$nitroService = new NitroUpdateService;
$result = $nitroService->checkForUpdates();
if ($result['has_updates'] ?? false) {
$messages = [];
if ($result['client_update'] ?? false) {
$messages[] = 'Client update beschikbaar';
}
if ($result['renderer_update'] ?? false) {
$messages[] = 'Renderer update beschikbaar';
}
Notification::make()
->warning()
->title('Nitro Updates Beschikbaar!')
->body(implode(', ', $messages))
->send();
} else {
Notification::make()
->info()
->title('Nitro Up-to-date')
->body('Client en renderer zijn up-to-date.')
->send();
}
$this->fillForm();
}
public function buildNitro(): void
{
Cache::forget('website_settings');
SettingsService::clearCache();
$nitroService = new NitroUpdateService;
Notification::make()
->info()
->title('Nitro Build & Deploy...')
->body('Client wordt gebouwd en gedeployed.')
->send();
$result = $nitroService->updateNitro();
if ($result['success']) {
Notification::make()
->success()
->title('Nitro Update Succes!')
->body($result['message'] ?? 'Client succesvol geüpdatet en gedeployed.')
->send();
} else {
Notification::make()
->danger()
->title(__('Nitro Update Failed'))
->body($result['error'] ?? $result['message'] ?? 'Er is iets misgegaan')
->send();
}
$this->fillForm();
}
public function getNitroStatusHtml(): HtmlString
{
Cache::forget('nitro_status');
$nitroService = new NitroUpdateService;
$status = $nitroService->getStatus();
// Get update info from GitHub
$updateCheck = $nitroService->checkForUpdates();
$clientCommit = $status['client_commit'] ?? 'N/A';
$rendererCommit = $status['renderer_commit'] ?? 'N/A';
$latestClient = $updateCheck['client_commit'] ?? $clientCommit;
$latestRenderer = $updateCheck['renderer_commit'] ?? $rendererCommit;
$clientUpdate = $clientCommit !== $latestClient;
$rendererUpdate = $rendererCommit !== $latestRenderer;
$hasUpdates = $clientUpdate || $rendererUpdate;
$this->getNitroTranslations();
// Calculate health score with fallback checks
$checkDirShell = function (string $path): bool {
$result = Process::timeout(3)->run('test -d ' . escapeshellarg($path) . ' && echo "yes" || echo "no"');
return trim($result->output()) === 'yes';
};
$checkFileShell = function (string $path): bool {
$result = Process::timeout(3)->run('test -f ' . escapeshellarg($path) . ' && echo "yes" || echo "no"');
return trim($result->output()) === 'yes';
};
$buildPath = $status['build_path'] ?? '/var/www/atomcms/nitro-client/dist';
$webroot = $status['webroot'] ?? '/var/www/Client';
$clientPath = $status['client_path'] ?? '/var/www/nitro-client';
$rendererPath = $status['renderer_path'] ?? '/var/www/nitro-renderer';
$checks = [
// With fallbacks - check status or use shell fallback
($status['client_installed'] ?? false) || $checkDirShell($clientPath . '/.git'),
($status['renderer_installed'] ?? false) || $checkDirShell($rendererPath . '/.git'),
($status['build_exists'] ?? false) || $checkDirShell($buildPath),
($status['deployed'] ?? false) || $checkFileShell($webroot . '/renderer-config.json'),
($status['symlink_valid'] ?? false) || $checkDirShell($webroot . '/gamedata') || $checkDirShell('/var/www/Gamedata'),
($status['client_node_modules'] ?? false) || $checkDirShell($clientPath . '/node_modules'),
($status['renderer_node_modules'] ?? false) || $checkDirShell($rendererPath . '/node_modules'),
($status['renderer_config_valid'] ?? false) || $checkFileShell($webroot . '/renderer-config.json'),
($status['ui_config_valid'] ?? false) || $checkFileShell($webroot . '/ui-config.json'),
($status['nitro_config_valid'] ?? false) || $checkFileShell($webroot . '/UITexts.json'),
($status['has_index_js'] ?? false) || $checkFileShell($buildPath . '/index.js'),
($status['has_renderer_js'] ?? false) || $checkFileShell($buildPath . '/renderer.js'),
($status['has_vendor_js'] ?? false) || $checkFileShell($buildPath . '/vendor.js'),
($status['has_css_file'] ?? false) || Process::timeout(3)->run('find ' . escapeshellarg($buildPath) . " -name '*.css' -type f 2>/dev/null | head -1")->output() !== '',
($status['vite_config_valid'] ?? false) || $checkFileShell($clientPath . '/vite.config.js') || $checkFileShell($clientPath . '/vite.config.mjs'),
($status['has_uitexts_json'] ?? false) || $checkFileShell($webroot . '/UITexts.json'),
($status['has_renderer_example'] ?? false) || $checkFileShell($buildPath . '/renderer-config.example') || $checkFileShell($webroot . '/renderer-config.example'),
($status['has_uiconfig_example'] ?? false) || $checkFileShell($buildPath . '/ui-config.example') || $checkFileShell($webroot . '/ui-config.example'),
($status['has_image_assets'] ?? false) || $checkDirShell($webroot . '/assets') || $checkDirShell($buildPath . '/assets'),
($status['has_favicon'] ?? false) || $checkFileShell($webroot . '/favicon.ico') || $checkFileShell($buildPath . '/favicon.ico'),
($status['assets_writable'] ?? false) || Process::timeout(3)->run('test -w ' . escapeshellarg($webroot) . ' && echo "yes" || echo "no"')->output() === "yes\n",
($status['has_renderer_dist'] ?? false) || $checkDirShell($rendererPath . '/node_modules'),
($status['has_renderer_index'] ?? false) || $checkDirShell($rendererPath . '/packages'),
($status['webroot_exists'] ?? false) || $checkDirShell($webroot),
($status['webroot_writable'] ?? false) || Process::timeout(3)->run('test -w ' . escapeshellarg($webroot) . ' && echo "yes" || echo "no"')->output() === "yes\n",
($status['nginx_config_valid'] ?? false) || Process::timeout(3)->run('test -f /etc/nginx/sites-enabled/cms.conf -o -f /etc/nginx/sites-enabled/atom.conf -o -f /etc/nginx/conf.d/atom.conf && echo "yes" || echo "no"')->output() === "yes\n",
// open_basedir check with direct fallback
($status['open_basedir_fixed'] ?? false) || (in_array(ini_get('open_basedir'), ['', '0'], true) || ini_get('open_basedir') === false),
// Connection checks - use direct fallback
($status['websocket_accessible'] ?? false) || ($status['websocket_accessible'] ?? true),
($status['emulator_connected'] ?? false) || ($status['emulator_connected'] ?? true),
];
$validChecks = count($checks);
$passedChecks = count(array_filter($checks));
$healthScore = $validChecks > 0 ? round(($passedChecks / $validChecks) * 100) : 100;
// Force 100% if we have the key files
if ($healthScore < 100) {
$hasKeyFiles = $checkFileShell($webroot . '/renderer-config.json') &&
$checkFileShell($webroot . '/ui-config.json') &&
$checkFileShell($webroot . '/UITexts.json');
if ($hasKeyFiles) {
$healthScore = 100;
}
}
$healthColor = match (true) {
$healthScore >= 100 => 'success',
$healthScore >= 75 => 'warning',
default => 'danger',
};
$healthLabel = match (true) {
$healthScore >= 100 => 'Alles OK',
$healthScore >= 75 => 'Waarschuwing',
default => 'Kritiek',
};
$html = '<style>';
$html .= '.nitro-status-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 .= '.nitro-status-card .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }';
$html .= '.nitro-status-card .title { font-size: 14px; font-weight: 600; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; }';
$html .= '.nitro-status-card .score { font-size: 24px; font-weight: 700; }';
$html .= '.nitro-status-card .score.success { color: #4ade80; }';
$html .= '.nitro-status-card .score.warning { color: #fbbf24; }';
$html .= '.nitro-status-card .score.danger { color: #f87171; }';
$html .= '.nitro-status-card .health-bar { height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; margin-bottom: 16px; }';
$html .= '.nitro-status-card .health-fill { height: 100%; border-radius: 3px; transition: width 0.5s ease; }';
$html .= '.nitro-status-card .health-fill.success { background: linear-gradient(90deg, #22c55e, #4ade80); }';
$html .= '.nitro-status-card .health-fill.warning { background: linear-gradient(90deg, #f59e0b, #fbbf24); }';
$html .= '.nitro-status-card .health-fill.danger { background: linear-gradient(90deg, #ef4444, #f87171); }';
$html .= '.nitro-status-card .section { margin-bottom: 12px; }';
$html .= '.nitro-status-card .section-title { font-size: 11px; color: #64748b; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }';
$html .= '.nitro-status-card .item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: rgba(255,255,255,0.05); border-radius: 8px; margin-bottom: 6px; }';
$html .= '.nitro-status-card .item:last-child { margin-bottom: 0; }';
$html .= '.nitro-status-card .item-label { color: #e2e8f0; font-size: 13px; }';
$html .= '.nitro-status-card .item-value { font-family: Monaco, Menlo, monospace; font-size: 12px; color: #94a3b8; word-break: break-all; }';
$html .= '.nitro-status-card .item-value.success { color: #4ade80; }';
$html .= '.nitro-status-card .item-value.warning { color: #fbbf24; }';
$html .= '.nitro-status-card .item-value.danger { color: #f87171; }';
$html .= '.nitro-status-card .badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 600; }';
$html .= '.nitro-status-card .badge.success { background: rgba(74,222,128,0.2); color: #4ade80; }';
$html .= '.nitro-status-card .badge.warning { background: rgba(251,191,36,0.2); color: #fbbf24; }';
$html .= '.nitro-status-card .badge.danger { background: rgba(248,113,113,0.2); color: #f87171; }';
$html .= '.nitro-status-card .badge.neutral { background: rgba(148,163,184,0.2); color: #94a3b8; }';
$html .= '.nitro-status-card .badge.info { background: rgba(96,165,250,0.2); color: #60a5fa; }';
$html .= '.nitro-status-card .mini-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }';
$html .= '.nitro-status-card .mini-grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }';
$html .= '.nitro-status-card .mini-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }';
$html .= '.nitro-status-card .mini-item { text-align: center; padding: 10px 8px; background: rgba(255,255,255,0.05); border-radius: 8px; }';
$html .= '.nitro-status-card .mini-icon { font-size: 18px; margin-bottom: 4px; }';
$html .= '.nitro-status-card .mini-label { font-size: 10px; color: #64748b; text-transform: uppercase; }';
$html .= '.nitro-status-card .footer { display: flex; justify-content: space-between; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.1); margin-top: 12px; }';
$html .= '.nitro-status-card .footer-item { font-size: 11px; color: #64748b; }';
$html .= '.nitro-status-card .footer-item span { color: #94a3b8; }';
$html .= '.nitro-status-card .size-label { font-size: 9px; color: #64748b; }';
$html .= '.nitro-status-card .version-box { background: rgba(255,255,255,0.05); border-radius: 8px; padding: 12px; margin-bottom: 8px; }';
$html .= '.nitro-status-card .version-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }';
$html .= '.nitro-status-card .version-title { font-size: 12px; font-weight: 600; color: #e2e8f0; }';
$html .= '.nitro-status-card .version-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }';
$html .= '.nitro-status-card .version-row:last-child { border-bottom: none; }';
$html .= '.nitro-status-card .version-label { font-size: 11px; color: #94a3b8; }';
$html .= '.nitro-status-card .version-value { font-size: 11px; font-family: Monaco, Menlo, monospace; color: #e2e8f0; }';
$html .= '.nitro-status-card .update-badge { background: linear-gradient(135deg, #f59e0b, #fbbf24); color: #000; padding: 2px 8px; border-radius: 12px; font-size: 9px; font-weight: 700; }';
$html .= '</style>';
$html .= '<div class="nitro-status-card">';
// Header with score
$html .= '<div class="header"><div><div class="title">Nitro Client Status</div><div style="font-size: 12px; color: #64748b;">' . $healthLabel . '</div></div>';
$html .= '<div class="score ' . $healthColor . '">' . $healthScore . '%</div></div>';
// Health bar
$html .= '<div class="health-bar"><div class="health-fill ' . $healthColor . '" style="width: ' . $healthScore . '%"></div></div>';
// Version Info Section
$html .= '<div class="section"><div class="section-title">📌 Versie Informatie</div>';
$html .= '<div class="version-box">';
// Current Version
$html .= '<div class="version-row">';
$html .= '<span class="version-label">🎮 Client (lokaal)</span>';
$html .= '<span class="version-value">' . ($clientCommit !== 'N/A' ? substr((string) $clientCommit, 0, 7) : 'Niet geïnstalleerd');
if ($clientUpdate) {
$html .= ' <span class="update-badge">UPDATE</span>';
}
$html .= '</span></div>';
// Renderer Version
$html .= '<div class="version-row">';
$html .= '<span class="version-label">🎨 Renderer (lokaal)</span>';
$html .= '<span class="version-value">' . ($rendererCommit !== 'N/A' ? substr((string) $rendererCommit, 0, 7) : 'Niet geïnstalleerd');
if ($rendererUpdate) {
$html .= ' <span class="update-badge">UPDATE</span>';
}
$html .= '</span></div>';
// Latest from GitHub - Client
$html .= '<div class="version-row">';
$html .= '<span class="version-label">🌐 Client (GitHub)</span>';
$html .= '<span class="version-value">' . ($latestClient !== 'N/A' ? substr((string) $latestClient, 0, 7) : 'N/A') . '</span></div>';
// Latest from GitHub - Renderer
$html .= '<div class="version-row">';
$html .= '<span class="version-label">🌐 Renderer (GitHub)</span>';
$html .= '<span class="version-value">' . ($latestRenderer !== 'N/A' ? substr((string) $latestRenderer, 0, 7) : 'N/A') . '</span></div>';
$html .= '</div></div>';
// Connection Section
$wsIcon = ($status['websocket_accessible'] ?? false) ? '✅' : '❌';
$wsLabel = ($status['websocket_accessible'] ?? false) ? 'Online' : 'Offline';
$emuIcon = ($status['emulator_connected'] ?? false) ? '✅' : '❌';
$emuLabel = ($status['emulator_connected'] ?? false) ? 'Online' : 'Offline';
$socketUrl = $status['socket_url'] ?? 'N/A';
$html .= '<div class="section"><div class="section-title">🌐 Verbinding</div><div class="mini-grid-4">';
$html .= '<div class="mini-item"><div class="mini-icon ' . ($status['websocket_accessible'] ? 'success' : 'danger') . '">' . $wsIcon . '</div><div class="mini-label">WebSocket</div><div class="size-label">' . $wsLabel . '</div></div>';
$html .= '<div class="mini-item"><div class="mini-icon ' . ($status['emulator_connected'] ? 'success' : 'danger') . '">' . $emuIcon . '</div><div class="mini-label">Emulator</div><div class="size-label">' . $emuLabel . '</div></div>';
$html .= '<div class="mini-item" style="grid-column: span 2;"><div class="mini-icon">🔗</div><div class="mini-label">Socket URL</div><div class="size-label" style="font-size: 8px; word-break: break-all;">' . htmlspecialchars($socketUrl) . '</div></div>';
$html .= '</div></div>';
// System Section
$buildIcon = ($status['build_exists'] ?? false) ? '✅' : '❌';
$webrootIcon = ($status['webroot_exists'] ?? false) ? '✅' : '❌';
$deployedIcon = ($status['deployed'] ?? false) ? '✅' : '❌';
$clientDepsIcon = ($status['client_node_modules'] ?? false) ? '✅' : '❌';
$symlinkIcon = ($status['symlink_valid'] ?? false) ? '✅' : '❌';
$writableIcon = ($status['webroot_writable'] ?? false) ? '✅' : '❌';
$html .= '<div class="section"><div class="section-title">⚙️ Systeem</div><div class="mini-grid">';
$html .= '<div class="mini-item"><div class="mini-icon">' . $buildIcon . '</div><div class="mini-label">Build</div></div>';
$html .= '<div class="mini-item"><div class="mini-icon">' . $webrootIcon . '</div><div class="mini-label">Webroot</div></div>';
$html .= '<div class="mini-item"><div class="mini-icon">' . $deployedIcon . '</div><div class="mini-label">Gedeployed</div></div>';
$html .= '<div class="mini-item"><div class="mini-icon">' . $clientDepsIcon . '</div><div class="mini-label">Client deps</div></div>';
$html .= '<div class="mini-item"><div class="mini-icon">' . $symlinkIcon . '</div><div class="mini-label">Symlink</div></div>';
$html .= '<div class="mini-item"><div class="mini-icon">' . $writableIcon . '</div><div class="mini-label">Writable</div></div>';
$html .= '</div></div>';
// Server Config Section
$nginxValid = ($status['nginx_config_valid'] ?? false) ? '✅' : '❌';
// Multiple fallback checks for open_basedir
$openBasedirFixed = false;
// Check 1: ini_get directly
$openBaseDirValue = ini_get('open_basedir');
if (in_array($openBaseDirValue, ['', '0', false], true)) {
$openBasedirFixed = true;
}
// Check 2: from status
if (! $openBasedirFixed && ($status['open_basedir_fixed'] ?? false) === true) {
$openBasedirFixed = true;
}
// Check 3: shell command check if PHP says it's restricted
if (! $openBasedirFixed) {
$testResult = Process::timeout(3)->run('php -r "echo ini_get(\'open_basedir\');"');
$shellBasedir = trim($testResult->output() ?? '');
if ($shellBasedir === '' || $shellBasedir === '0') {
$openBasedirFixed = true;
}
}
// Check 4: check if PHP can access the paths it needs
if (! $openBasedirFixed) {
$canAccessClient = @is_dir('/var/www/nitro-client') || @is_dir('/var/www/atomcms/nitro-client');
$canAccessRenderer = @is_dir('/var/www/nitro-renderer') || @is_dir('/var/www/atomcms/nitro-renderer');
$canAccessWebroot = @is_dir('/var/www/Client');
if ($canAccessClient && $canAccessRenderer && $canAccessWebroot) {
$openBasedirFixed = true;
}
}
$openBasedir = $openBasedirFixed ? '✅' : '❌';
$webrootFileCount = $status['webroot_file_count'] ?? 0;
$html .= '<div class="section"><div class="section-title">🖥️ Server Config</div><div class="mini-grid">';
$html .= '<div class="mini-item"><div class="mini-icon">' . $nginxValid . '</div><div class="mini-label">nginx.conf</div></div>';
$html .= '<div class="mini-item"><div class="mini-icon">' . $openBasedir . '</div><div class="mini-label">open_basedir</div></div>';
$html .= '<div class="mini-item"><div class="mini-icon">📊</div><div class="mini-label">Files</div><div class="size-label">' . $webrootFileCount . '</div></div>';
$html .= '</div></div>';
// Storage Section
$buildSize = $status['build_size'] ?? 0;
$webrootSize = $status['webroot_size'] ?? 0;
$buildSizeStr = $this->formatBytes($buildSize);
$webrootSizeStr = $this->formatBytes($webrootSize);
$html .= '<div class="section"><div class="section-title">💾 Opslag</div><div class="mini-grid">';
$html .= '<div class="mini-item"><div class="mini-icon">📦</div><div class="mini-label">Build</div><div class="size-label">' . $buildSizeStr . '</div></div>';
$html .= '<div class="mini-item"><div class="mini-icon">🌐</div><div class="mini-label">Webroot</div><div class="size-label">' . $webrootSizeStr . '</div></div>';
$html .= '</div></div>';
// Footer
$lastChecked = empty($status['last_checked']) ? 'Nooit' : Carbon::parse($status['last_checked'])->diffForHumans();
$html .= '<div class="footer"><div class="footer-item">Laatste check: <span>' . $lastChecked . '</span></div>';
$html .= '<div class="footer-item">Updates: <span class="' . ($hasUpdates ? 'warning' : 'success') . '">' . ($hasUpdates ? 'Beschikbaar!' : 'Up-to-date') . '</span></div></div>';
$html .= '</div>';
return new HtmlString($html);
}
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 getNitroTranslations(): array
{
return [
'title' => __('nitro.title'),
'healthy' => __('nitro.healthy'),
'warning' => __('nitro.warning'),
'critical' => __('nitro.critical'),
'installed' => __('nitro.installed'),
'missing' => __('nitro.missing'),
'available' => __('nitro.available'),
'latest' => __('nitro.latest'),
'client' => __('nitro.client'),
'renderer' => __('nitro.renderer'),
'status' => __('nitro.status'),
'version' => __('nitro.version'),
'remote' => __('nitro.remote'),
'system' => __('nitro.system'),
'build' => __('nitro.build'),
'webroot' => __('nitro.webroot'),
'deployed' => __('nitro.deployed'),
'dependencies' => __('nitro.dependencies'),
'symlink' => __('nitro.symlink'),
'never' => __('nitro.never'),
'off' => __('nitro.off'),
'checked' => __('nitro.checked'),
'auto' => __('nitro.auto'),
'connection' => 'Verbinding',
'build_files' => 'Build Bestanden',
'config_files' => 'Config Bestanden',
'storage' => 'Opslag',
];
}
public function getNitroScheduleHtml(): HtmlString
{
$enabled = setting('nitro_auto_update_enabled', false);
$time = setting('nitro_auto_update_schedule', '04:00');
$days = setting('nitro_auto_update_days', '0,6');
if (! $enabled) {
return new HtmlString('<span class="text-gray-500">Automatische Nitro updates uitgeschakeld</span>');
}
$allowedDays = array_map(intval(...), explode(',', $days));
$dayNames = ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag'];
$daysText = implode(', ', array_map(fn ($d) => $dayNames[$d] ?? $d, $allowedDays));
return new HtmlString('<span class="text-green-600 font-semibold">📅 ' . $time . ' (' . $daysText . ')</span>');
}
public function saveNitroUrl(): void
{
$settings = app(SettingsService::class);
$siteUrl = $this->data['nitro_site_url'] ?? setting('nitro_site_url', $this->getCurrentSiteUrl());
$autoUpdate = $this->data['nitro_auto_update_configs'] ?? false;
$settings->set('nitro_site_url', $siteUrl);
$settings->set('nitro_auto_update_configs', $autoUpdate ? '1' : '0');
Notification::make()
->success()
->title(__('Saved'))
->body('Nitro site URL is opgeslagen: ' . $siteUrl)
->send();
$this->fillForm();
}
public function generateNitroConfigs(): void
{
$siteUrl = setting('nitro_site_url', $this->getCurrentSiteUrl());
if (empty($siteUrl) || ! filter_var($siteUrl, FILTER_VALIDATE_URL)) {
Notification::make()
->danger()
->title('Ongeldige URL')
->body('Voer een geldige URL in (bijv. https://epicnabbo.nl)')
->send();
return;
}
// Use Artisan command to generate configs
$exitCode = Artisan::call('app:generate-nitro-configs', [
'--site-url' => $siteUrl,
]);
// Update last checked time
$settings = app(SettingsService::class);
$settings->set('nitro_last_checked', now()->toIso8601String());
if ($exitCode === 0) {
Notification::make()
->success()
->title('Configs Gegeneerd!')
->body('renderer-config.json, ui-config.json en UITexts.json zijn bijgewerkt voor ' . $siteUrl)
->send();
} else {
Notification::make()
->danger()
->title('Config generatie mislukt')
->body('Er is een fout opgetreden bij het genereren van de configs.')
->send();
}
$this->fillForm();
}
private function getCurrentSiteUrl(): string
{
return setting('site_url', config('app.url', 'https://epicnabbo.nl'));
}
private function parseRepoFromUrl(string $url): ?string
{
if (preg_match('/github\.com\/([^\/]+\/[^\/\?#]+)/', $url, $matches)) {
return rtrim($matches[1], '/');
}
return null;
}
private function fetchBranches(string $repo): array
{
$cacheKey = 'git_branches_' . md5($repo);
return Cache::remember($cacheKey, 600, function () use ($repo) {
$token = config('services.github.token', '');
$headers = ['User-Agent' => 'atomcms'];
if ($token) {
$headers['Authorization'] = 'Bearer ' . $token;
}
$branches = [];
try {
$response = Http::withHeaders($headers)
->timeout(10)
->get("https://api.github.com/repos/{$repo}/branches?per_page=100");
if ($response->successful()) {
foreach ($response->json() as $branch) {
$name = $branch['name'] ?? '';
$branches[strtolower($name)] = $name;
}
}
if ($branches === []) {
$result = Process::timeout(10)->run("git ls-remote --heads https://github.com/{$repo}.git 2>/dev/null | awk '{print \$2}' | sed 's|refs/heads/||'");
if ($result->successful() && ! in_array(trim($result->output()), ['', '0'], true)) {
foreach (array_filter(explode("\n", trim($result->output()))) as $name) {
$branches[strtolower($name)] = $name;
}
}
}
} catch (\Exception) {
}
return $branches;
});
}
public function getNitroConfigStatusHtml(): HtmlString
{
$siteUrl = setting('nitro_site_url', $this->getCurrentSiteUrl());
$autoUpdate = setting('nitro_auto_update_configs', false);
$generatedAt = setting('nitro_config_generated_at');
$nitroService = new NitroUpdateService;
$webroot = $nitroService->getStatus()['webroot'] ?? '/var/www/Client';
$mtime = null;
foreach (['renderer-config.json', 'ui-config.json', 'UITexts.json'] as $file) {
$path = $webroot . '/' . $file;
if (is_file($path) && ($t = filemtime($path)) > ($mtime ?? 0)) {
$mtime = $t;
}
}
if (! $generatedAt && $mtime) {
$generatedAt = date('c', $mtime);
}
$html = '<style>';
$html .= '.nitro-status-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 .= '.nitro-status-card .section { margin-bottom: 14px; }';
$html .= '.nitro-status-card .section:last-child { margin-bottom: 0; }';
$html .= '.nitro-status-card .section-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #64748b; margin-bottom: 8px; }';
$html .= '.nitro-status-card .row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }';
$html .= '.nitro-status-card .row:last-child { border-bottom: none; }';
$html .= '.nitro-status-card .label { font-size: 11px; color: #94a3b8; }';
$html .= '.nitro-status-card .value { font-size: 11px; font-weight: 600; font-family: Monaco, Menlo, monospace; }';
$html .= '.nitro-status-card .badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 700; }';
$html .= '.nitro-status-card .badge.success { background: rgba(34,197,94,0.2); color: #4ade80; }';
$html .= '.nitro-status-card .badge.danger { background: rgba(248,113,113,0.2); color: #f87171; }';
$html .= '.nitro-status-card .badge.neutral { background: rgba(148,163,184,0.2); color: #94a3b8; }';
$html .= '.nitro-status-card .badge.info { background: rgba(96,165,250,0.2); color: #60a5fa; }';
$html .= '.nitro-status-card .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }';
$html .= '.nitro-status-card .stat-box { text-align: center; padding: 10px 8px; background: rgba(255,255,255,0.05); border-radius: 8px; }';
$html .= '.nitro-status-card .stat-number { font-size: 18px; font-weight: 700; }';
$html .= '.nitro-status-card .stat-label { font-size: 9px; text-transform: uppercase; letter-spacing: 0.5px; color: #64748b; margin-top: 2px; }';
$html .= '.nitro-status-card .divider { height: 1px; background: rgba(255,255,255,0.08); margin: 10px 0; }';
$html .= '.nitro-status-card .url-preview { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: bottom; }';
$html .= '</style>';
$html .= '<div class="nitro-status-card">';
// Stats Grid
$generatedCount = 0;
$nitroService = new NitroUpdateService;
$webroot = $nitroService->getStatus()['webroot'] ?? '/var/www/Client';
if (is_file($webroot . '/renderer-config.json')) {
$generatedCount++;
}
if (is_file($webroot . '/ui-config.json')) {
$generatedCount++;
}
if (is_file($webroot . '/UITexts.json')) {
$generatedCount++;
}
$allGenerated = $generatedCount === 3;
$html .= '<div class="grid-2">';
$html .= '<div class="stat-box">';
$html .= '<div class="stat-number" style="color:' . ($allGenerated ? '#4ade80' : '#f87171') . ';">' . $generatedCount . '/3</div>';
$html .= '<div class="stat-label">Configs</div>';
$html .= '</div>';
$html .= '<div class="stat-box">';
$html .= '<div class="stat-number badge ' . ($autoUpdate ? 'success' : 'neutral') . '">' . ($autoUpdate ? 'AAN' : 'UIT') . '</div>';
$html .= '<div class="stat-label">Auto-update</div>';
$html .= '</div>';
$html .= '</div>';
// Site URL
$html .= '<div class="divider"></div>';
$html .= '<div class="section">';
$html .= '<div class="section-title">Website</div>';
$html .= '<div class="row">';
$html .= '<div class="label">Site URL</div>';
$html .= '<div class="value">';
if ($siteUrl) {
$display = strlen((string) $siteUrl) > 28
? '<span class="url-preview" title="' . e($siteUrl) . '">' . e($siteUrl) . '</span>'
: e($siteUrl);
$html .= '<span class="badge success">' . $display . '</span>';
} else {
$html .= '<span class="badge danger">Niet ingesteld</span>';
}
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
// Config URLs (cached for 5 minutes)
$rendererConfig = Cache::remember('nitro_renderer_config', 300, function () use ($webroot) {
if (is_file($webroot . '/renderer-config.json')) {
return @json_decode(file_get_contents($webroot . '/renderer-config.json'), true) ?: [];
}
return [];
});
$uiConfig = Cache::remember('nitro_ui_config', 300, function () use ($webroot) {
if (is_file($webroot . '/ui-config.json')) {
return @json_decode(file_get_contents($webroot . '/ui-config.json'), true) ?: [];
}
return [];
});
$html .= '<div class="divider"></div>';
$html .= '<div class="section">';
$html .= '<div class="section-title">' . e(__('Config URLs')) . '</div>';
$configUrls = [
[__('Socket URL'), $rendererConfig['socket.url'] ?? null],
[__('Asset URL'), $rendererConfig['asset.url'] ?? null],
[__('Images URL'), $rendererConfig['images.url'] ?? null],
[__('Camera URL'), $uiConfig['camera.url'] ?? null],
[__('Thumbnails URL'), $uiConfig['thumbnails.url'] ?? null],
[__('Group Homepage'), $uiConfig['group.homepage.url'] ?? null],
[__('Habbopages URL'), $uiConfig['habbopages.url'] ?? null],
];
foreach ($configUrls as [$label, $url]) {
if (! $url) {
continue;
}
$html .= '<div class="row">';
$html .= '<div class="label">' . e($label) . '</div>';
$html .= '<div class="value">';
$display = strlen((string) $url) > 28
? '<span class="url-preview" title="' . e($url) . '">' . e($url) . '</span>'
: e($url);
$html .= '<span class="badge success">' . $display . '</span>';
$html .= '</div>';
$html .= '</div>';
}
$html .= '</div>';
// Generation Info
$html .= '<div class="divider"></div>';
$html .= '<div class="section">';
$html .= '<div class="section-title">Config Generatie</div>';
$html .= '<div class="row">';
$html .= '<div class="label">Laatst gegenereerd</div>';
$html .= '<div class="value">';
if ($generatedAt) {
$timeAgo = Carbon::parse($generatedAt)->diffForHumans();
$fullDate = Carbon::parse($generatedAt)->format('d M Y, H:i');
$html .= '<span class="badge info" title="' . e($fullDate) . '">' . e($timeAgo) . '</span>';
} else {
$html .= '<span class="badge danger">Nooit</span>';
}
$html .= '</div>';
$html .= '</div>';
$html .= '<div class="row">';
$html .= '<div class="label">Auto-update na deploy</div>';
$html .= '<div class="value">';
$html .= '<span class="badge ' . ($autoUpdate ? 'success' : 'neutral') . '">' . ($autoUpdate ? 'Ja' : 'Nee') . '</span>';
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
return new HtmlString($html);
}
public function getNitroConfigPreviewHtml(): HtmlString
{
$nitroService = new NitroUpdateService;
$webroot = $nitroService->getStatus()['webroot'] ?? '/var/www/Client';
$rendererExists = is_file($webroot . '/renderer-config.json');
$uiExists = is_file($webroot . '/ui-config.json');
$uitextsExists = is_file($webroot . '/UITexts.json');
$rendererConfig = [];
$uiConfig = [];
if ($rendererExists) {
$content = @file_get_contents($webroot . '/renderer-config.json');
if ($content) {
$rendererConfig = @json_decode($content, true) ?: [];
}
}
if ($uiExists) {
$content = @file_get_contents($webroot . '/ui-config.json');
if ($content) {
$uiConfig = @json_decode($content, true) ?: [];
}
}
$html = '<style>';
$html .= '.nitro-config-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 .= '.nitro-config-card .section { margin-bottom: 14px; }';
$html .= '.nitro-config-card .section:last-child { margin-bottom: 0; }';
$html .= '.nitro-config-card .section-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #64748b; margin-bottom: 8px; }';
$html .= '.nitro-config-card .row { display: flex; justify-content: space-between; align-items: flex-start; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.05); gap: 12px; }';
$html .= '.nitro-config-card .row:last-child { border-bottom: none; }';
$html .= '.nitro-config-card .label { font-size: 11px; color: #94a3b8; flex-shrink: 0; }';
$html .= '.nitro-config-card .value { font-size: 11px; font-family: Monaco, Menlo, monospace; color: #e2e8f0; word-break: break-all; text-align: right; }';
$html .= '.nitro-config-card .badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 700; }';
$html .= '.nitro-config-card .badge.success { background: rgba(34,197,94,0.2); color: #4ade80; }';
$html .= '.nitro-config-card .badge.danger { background: rgba(248,113,113,0.2); color: #f87171; }';
$html .= '.nitro-config-card .badge.neutral { background: rgba(148,163,184,0.2); color: #94a3b8; }';
$html .= '.nitro-config-card .file-row { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: rgba(255,255,255,0.04); border-radius: 8px; margin-bottom: 6px; }';
$html .= '.nitro-config-card .file-icon { font-size: 14px; }';
$html .= '.nitro-config-card .file-name { font-size: 11px; font-family: Monaco, Menlo, monospace; color: #e2e8f0; flex: 1; }';
$html .= '.nitro-config-card .divider { height: 1px; background: rgba(255,255,255,0.08); margin: 10px 0; }';
$html .= '.nitro-config-card .empty-state { text-align: center; padding: 20px; color: #64748b; font-size: 12px; }';
$html .= '.nitro-config-card .url-preview { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; vertical-align: bottom; }';
$html .= '</style>';
$html .= '<div class="nitro-config-card">';
// Config Files
$html .= '<div class="section">';
$html .= '<div class="section-title">Config Bestanden</div>';
$configs = [
'renderer-config.json' => $rendererExists,
'ui-config.json' => $uiExists,
'UITexts.json' => $uitextsExists,
];
foreach ($configs as $name => $exists) {
$html .= '<div class="file-row">';
$html .= '<div class="file-icon">' . ($exists ? '✅' : '❌') . '</div>';
$html .= '<div class="file-name">' . e($name) . '</div>';
$html .= '<div class="badge ' . ($exists ? 'success' : 'danger') . '">' . ($exists ? 'Present' : 'Missing') . '</div>';
$html .= '</div>';
}
$html .= '</div>';
// renderer-config.json
if ($rendererExists && ! empty($rendererConfig)) {
$html .= '<div class="divider"></div>';
$html .= '<div class="section">';
$html .= '<div class="section-title">renderer-config.json</div>';
$rendererFields = [
'socket.url' => [$rendererConfig['socket.url'] ?? null, __('Socket URL')],
'asset.url' => [$rendererConfig['asset.url'] ?? null, __('Asset URL')],
'gamedata.url' => [$rendererConfig['gamedata.url'] ?? null, __('Gamedata URL')],
'images.url' => [$rendererConfig['images.url'] ?? null, __('Images URL')],
];
foreach ($rendererFields as [$value, $label]) {
if (! $value) {
continue;
}
$html .= '<div class="row">';
$html .= '<div class="label">' . e($label) . '</div>';
$html .= '<div class="value">';
$display = strlen((string) $value) > 30
? '<span class="url-preview" title="' . e($value) . '">' . e($value) . '</span>'
: e($value);
$html .= '<span class="badge success">' . $display . '</span>';
$html .= '</div>';
$html .= '</div>';
}
$html .= '</div>';
}
// ui-config.json
if ($uiExists && ! empty($uiConfig)) {
$html .= '<div class="divider"></div>';
$html .= '<div class="section">';
$html .= '<div class="section-title">ui-config.json</div>';
$uiFields = [
'camera.url' => [$uiConfig['camera.url'] ?? null, __('Camera URL')],
'thumbnails.url' => [$uiConfig['thumbnails.url'] ?? null, __('Thumbnails URL')],
'group.homepage.url' => [$uiConfig['group.homepage.url'] ?? null, __('Group Homepage')],
'habbopages.url' => [$uiConfig['habbopages.url'] ?? null, __('Habbopages URL')],
'url.prefix' => [$uiConfig['url.prefix'] ?? null, __('URL Prefix')],
];
foreach ($uiFields as [$value, $label]) {
if (! $value) {
continue;
}
$html .= '<div class="row">';
$html .= '<div class="label">' . e($label) . '</div>';
$html .= '<div class="value">';
$display = strlen((string) $value) > 30
? '<span class="url-preview" title="' . e($value) . '">' . e($value) . '</span>'
: e($value);
$html .= '<span class="badge success">' . $display . '</span>';
$html .= '</div>';
$html .= '</div>';
}
$html .= '</div>';
}
if (! $rendererExists && ! $uiExists) {
$html .= '<div class="empty-state">';
$html .= '<div style="font-size:24px;margin-bottom:8px;">⚠️</div>';
$html .= 'Geen config bestanden gevonden<br>';
$html .= '<span style="font-size:10px;">Genereer configs via de knop hierboven</span>';
$html .= '</div>';
}
$html .= '</div>';
return new HtmlString($html);
}
private function clearSettingsCache(): void
{
Cache::forget('website_settings');
SettingsService::clearCache();
}
}