You've already forked Atomcms-edit
574b5d6e17
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)
2855 lines
139 KiB
PHP
Executable File
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();
|
|
}
|
|
}
|