Files
Atomcms-edit/app/Filament/Pages/Monitoring/Commandocentrum.php
T
root 0bb35d6c8a refactor: centralize GitHub logic into GitHubService
- Create GitHubService with parseUrl, extractRepo, getBranches, getLatestCommit, getLatestRelease, hasUpdates
- Replace duplicated GitHub parsing in EmulatorConfiguration with GitHubService
- Replace fetchGitHubBranches, extractGitHubRepo, getEmulatorRemoteVersion in Commandocentrum
- Reduce code duplication across services and controllers
2026-05-19 21:07:16 +02:00

1680 lines
89 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace App\Filament\Pages\Monitoring;
use App\Actions\Commandocentrum\EmulatorControlAction;
use App\Actions\Commandocentrum\NitroControlAction;
use App\Enums\AlertSeverity;
use App\Models\Miscellaneous\WebsitePermission;
use App\Models\StaffActivity;
use App\Services\AutoDetectService;
use App\Services\CatalogService;
use App\Services\Diagnostics\DiagnosticRunner;
use App\Services\EmulatorUpdateService;
use App\Services\GitHubService;
use App\Services\RconService;
use App\Services\SettingsService;
use App\Services\UpdateHistoryService;
use BackedEnum;
use Carbon\Carbon;
use Exception;
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;
use UnitEnum;
final class Commandocentrum 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.commandocentrum';
public array $data = [];
public string $catalogSyncUrl = '';
/** @var array<\App\Services\Diagnostics\DiagnosticResult> */
public array $diagnostics = [];
public function mount(): void
{
$this->fillForm();
$this->runDiagnostics();
}
protected function fillForm(): void
{
$paths = $this->autoDetectPaths();
$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', ''),
'discord_webhook_ranks' => json_decode($this->getSetting('discord_webhook_ranks', '[]'), true) ?? [],
'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' => $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', $paths['emulator_jar_path']),
'emulator_source_path' => $this->getSetting('emulator_source_path', $paths['emulator_source_path']),
'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', $paths['nitro_client_path']),
'nitro_renderer_path' => $this->getSetting('nitro_renderer_path', $paths['nitro_renderer_path']),
'nitro_build_path' => $this->getSetting('nitro_build_path', $paths['nitro_build_path']),
'nitro_webroot' => $this->getSetting('nitro_webroot', $paths['nitro_webroot']),
'gamedata_path' => $this->getSetting('gamedata_path', $paths['gamedata_path']),
'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('🎯 Live Status')
->description('Real-time hotel statistieken')
->icon('heroicon-o-heart')
->columns(4)
->schema([
Placeholder::make('online_users')
->label('Online')
->content(fn (): HtmlString => $this->getCardHtml('Online', $this->getOnlineUsersCount(), '#22c55e', 'heroicon-o-users')),
Placeholder::make('emulator_status')
->label('Emulator')
->content(fn (): HtmlString => $this->getCardHtml('Emulator', $this->getEmulatorStatusText(), $this->getEmulatorStatusColor(), 'heroicon-o-server')),
Placeholder::make('database_status')
->label('Database')
->content(fn (): HtmlString => $this->getCardHtml('Database', $this->isDatabaseOnline() ? 'Online' : 'Offline', $this->isDatabaseOnline() ? '#22c55e' : '#ef4444', 'heroicon-o-circle-stack')),
Placeholder::make('server_load')
->label('Load')
->content(fn (): HtmlString => $this->getCardHtml('Load', $this->getServerLoad(), '#3b82f6', 'heroicon-o-cpu-chip')),
]),
Section::make('📊 Server Informatie')
->description('Gedetailleerde server status')
->icon('heroicon-o-information-circle')
->schema([
Placeholder::make('server_info')
->label('')
->content(fn (): HtmlString => $this->renderServerInfo()),
]),
Section::make('🩺 Systeem Gezondheid')
->description('Automatische systeem diagnostiek')
->icon('heroicon-o-heart')
->afterHeader([
Action::make('refresh_diagnostics')
->label('Vernieuwen')
->icon('heroicon-o-arrow-path')
->color('info')
->action('refreshDiagnostics'),
])
->schema([
Placeholder::make('diagnostics')
->label('')
->content(fn (): HtmlString => $this->renderDiagnostics()),
]),
Section::make('🏨 Hotel Status')
->description('Emulator en Nitro status')
->icon('heroicon-o-building-office')
->schema([
Placeholder::make('hotel_status')
->label('')
->content(fn (): HtmlString => $this->renderHotelStatus()),
]),
Section::make('📢 Hotel Alert')
->description('Stuur een bericht naar alle online gebruikers')
->icon('heroicon-o-megaphone')
->schema([
Placeholder::make('alert_form')
->label('')
->content(fn (): HtmlString => $this->renderAlertForm()),
]),
Section::make('📜 Emulator Logs')
->description('Live emulator log viewer')
->icon('heroicon-o-document-text')
->columnSpanFull()
->schema([
Placeholder::make('emulator_logs')
->label('')
->content(fn () => view('filament.components.emulator-log-viewer')),
]),
Section::make('🖥️ Emulator Control')
->description('Volledige emulator controle')
->icon('heroicon-o-server')
->columns(3)
->afterHeader([
Action::make('start_emulator')
->label('Start')
->icon('heroicon-o-play')
->color('success')
->action('startEmulator'),
Action::make('stop_emulator')
->label('Stop')
->icon('heroicon-o-stop')
->color('danger')
->action('stopEmulator'),
Action::make('restart_emulator')
->label('Restart')
->icon('heroicon-o-arrow-path')
->color('warning')
->action('restartEmulator'),
Action::make('check_emulator')
->label('Check')
->icon('heroicon-o-check-circle')
->color('info')
->action('checkEmulator'),
])
->schema([
Placeholder::make('emulator_info')
->label('')
->content(fn (): HtmlString => $this->renderEmulatorInfo()),
]),
Section::make('🔄 Emulator Updates')
->description('Configureer en update de emulator')
->icon('heroicon-o-arrow-down-circle')
->afterHeader([
Action::make('check_updates')
->label('Check Updates')
->color('info')
->action('checkEmulatorUpdates'),
Action::make('build_emulator')
->label('🔨 Build')
->color('success')
->action('buildEmulator'),
Action::make('run_sql')
->label('SQL Updates')
->color('purple')
->action('runSqlUpdates'),
Action::make('save_emulator')
->label('Opslaan')
->color('primary')
->action('saveEmulator'),
])
->schema([
Placeholder::make('emulator_settings')
->label('')
->content(fn (): HtmlString => $this->renderEmulatorSettings()),
]),
Section::make('💾 Emulator Backups')
->description('Bekijk en herstel emulator backups')
->icon('heroicon-s-archive-box')
->schema([
Placeholder::make('backups_list')
->label('')
->content(fn (): HtmlString => $this->renderBackupsList()),
]),
Section::make('📦 Nitro Client')
->description('Configureer en update Nitro')
->icon('heroicon-o-cloud-arrow-down')
->afterHeader([
Action::make('detect_paths')
->label('🔍 Auto Detect')
->color('success')
->action('detectAndSavePaths'),
Action::make('check_nitro')
->label('Check')
->color('info')
->action('checkNitroUpdates'),
Action::make('build_nitro')
->label('Build')
->color('pink')
->action('buildNitro'),
Action::make('generate_configs')
->label('Genereer Configs')
->color('indigo')
->action('generateNitroConfigs'),
Action::make('save_nitro')
->label('Opslaan')
->color('primary')
->action('saveNitro'),
])
->schema([
Placeholder::make('nitro_settings')
->label('')
->content(fn (): HtmlString => $this->renderNitroSettings()),
]),
Section::make('⚙️ Automatische Updates')
->description('Configureer automatische updates')
->icon('heroicon-o-clock')
->columns(2)
->afterHeader([
Action::make('save_auto')
->label('Opslaan')
->color('primary')
->action('saveAutoUpdate'),
])
->schema([
Toggle::make('auto_update_enabled')
->label('Automatische Updates Inschakelen'),
TextInput::make('auto_update_schedule')
->label('Schema (HH:MM)'),
TextInput::make('auto_update_days')
->label('Dagen (0-6)'),
]),
Section::make('👔 Kleding Sync')
->description('Sync catalogus kleding uit FigureMap')
->icon('heroicon-o-user')
->afterHeader([
Action::make('sync_clothing')
->label('🔄 Sync')
->color('success')
->action('syncClothing'),
])
->schema([
Placeholder::make('clothing_status')
->label('')
->content(fn (): HtmlString => $this->renderClothingStatus()),
]),
Section::make('🔔 Meldingen')
->description('E-mail en Discord alerts')
->icon('heroicon-o-bell')
->columns(2)
->afterHeader([
Action::make('save_alerts')
->label('Opslaan')
->color('primary')
->action('saveAlerts'),
Action::make('test_discord')
->label('Test Discord')
->color('info')
->action('testDiscord'),
])
->schema([
Toggle::make('alert_email_enabled')
->label('E-mail Meldingen'),
TextInput::make('alert_email_address')
->label('E-mail Adres')
->email()
->columnSpanFull(),
Toggle::make('alert_discord_enabled')
->label('Discord Meldingen'),
TextInput::make('alert_discord_webhook_url')
->label('Webhook URL')
->columnSpanFull(),
Select::make('discord_webhook_ranks')
->label('Ranks die Discord notificatie krijgen')
->multiple()
->options(fn () => WebsitePermission::query()->pluck('permission', 'min_rank')->mapWithKeys(fn ($perm, $rank) => [$rank => "Rank {$rank} ({$perm})"])->toArray())
->helperText('Laat leeg voor alleen staff (min_staff_rank)'),
]),
Section::make('📊 Update Geschiedenis')
->description('Laatste systeem updates')
->icon('heroicon-o-clock')
->schema([
Placeholder::make('history')
->label('')
->content(fn (): HtmlString => $this->getUpdateHistoryHtml()),
]),
Section::make('🔐 Social Login (v1.4)')
->description('Enable social login providers')
->icon('heroicon-o-user-circle')
->schema([
Toggle::make('social_login_google_enabled')
->label('Google Login')
->helperText('Allow users to login with Google'),
TextInput::make('social_login_google_client_id')
->label('Google Client ID')
->helperText('From Google Cloud Console'),
TextInput::make('social_login_google_client_secret')
->label('Google Client Secret')
->type('password'),
Toggle::make('social_login_discord_enabled')
->label('Discord Login')
->helperText('Allow users to login with Discord'),
TextInput::make('social_login_discord_client_id')
->label('Discord Client ID')
->helperText('From Discord Developer Portal'),
TextInput::make('social_login_discord_client_secret')
->label('Discord Client Secret')
->type('password'),
Toggle::make('social_login_github_enabled')
->label('GitHub Login')
->helperText('Allow users to login with GitHub'),
TextInput::make('social_login_github_client_id')
->label('GitHub Client ID')
->helperText('From GitHub Developer Settings'),
TextInput::make('social_login_github_client_secret')
->label('GitHub Client Secret')
->type('password'),
])
->columns(2),
Section::make('👔 Staff Activity Log')
->description('Recent staff activities in the housekeeping (v1.2)')
->icon('heroicon-o-user-group')
->schema([
Placeholder::make('staff_activity')
->label('')
->content(fn (): HtmlString => $this->getStaffActivityHtml()),
]),
]);
}
private function getSetting(string $key, string $default = ''): string
{
try {
return app(SettingsService::class)->getOrDefault($key, $default);
} catch (Exception) {
return $default;
}
}
private function getSettingBool(string $key, bool $default = false): bool
{
try {
$value = app(SettingsService::class)->getOrDefault($key, $default ? '1' : '0');
return in_array($value, ['1', 'true', 'yes'], true);
} catch (Exception) {
return $default;
}
}
private function getCurrentSiteUrl(): string
{
try {
return config('app.url', 'https://epicnabbo.nl');
} catch (Exception) {
return 'https://epicnabbo.nl';
}
}
private function autoDetectPaths(): array
{
$autoDetect = AutoDetectService::getInstance();
$autoDetect->clearCache();
return [
'nitro_client_path' => $autoDetect->detectNitroClientPath(),
'nitro_renderer_path' => $autoDetect->detectNitroRendererPath(),
'nitro_build_path' => $autoDetect->detectNitroBuildPath(),
'nitro_webroot' => $autoDetect->detectNitroWebroot(),
'gamedata_path' => $autoDetect->detectGamedataPath(),
'emulator_jar_path' => $autoDetect->detectEmulatorJarPath(),
'emulator_source_path' => $autoDetect->detectEmulatorSourcePath(),
];
}
private function getCardHtml(string $label, string $value, string $color, string $icon): HtmlString
{
$iconSvg = match ($icon) {
'heroicon-o-users' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />',
'heroicon-o-server' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />',
'heroicon-o-circle-stack' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />',
'heroicon-o-cpu-chip' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />',
default => '',
};
return new HtmlString(<<<HTML
<div style="background:#fff;border-radius:16px;padding:24px;border:1px solid #e2e8f0;box-shadow:0 1px 3px rgba(0,0,0,0.06);transition:all 0.3s ease;cursor:default;" onmouseover="this.style.transform='translateY(-4px)';this.style.boxShadow='0 12px 24px rgba(0,0,0,0.1)';this.style.borderColor='{{$color}}40';" onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='0 1px 3px rgba(0,0,0,0.06)';this.style.borderColor='#e2e8f0';">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:10px;">
<div style="background:{$color}15;padding:10px;border-radius:12px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="{$color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
{$iconSvg}
</svg>
</div>
<span style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#94a3b8;">{$label}</span>
</div>
<div style="width:8px;height:8px;border-radius:50%;background:{$color};animation:pulse 2s infinite;"></div>
</div>
<div style="font-size:36px;font-weight:800;color:#1e293b;line-height:1;">{$value}</div>
</div>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
HTML);
}
private function getOnlineUsersCount(): string
{
try {
return (string) DB::connection('mysql')->table('users')->where('online', '=', '1')->count();
} catch (Exception) {
return 'N/B';
}
}
private function getEmulatorStatusText(): string
{
try {
$rcon = new RconService;
if ($rcon->isConnected()) {
return 'Online';
}
$response = Http::timeout(2)->get('http://127.0.0.1:3000/api/status');
return $response->successful() ? 'Online' : 'Offline';
} catch (Exception) {
return 'Offline';
}
}
private function getEmulatorStatusColor(): string
{
return $this->getEmulatorStatusText() === 'Online' ? '#22c55e' : '#ef4444';
}
private function isDatabaseOnline(): bool
{
try {
DB::connection('mysql')->select('SELECT 1');
return true;
} catch (Exception) {
return false;
}
}
private function getServerLoad(): string
{
try {
$load = sys_getloadavg();
return $load ? number_format($load[0], 2) : 'N/B';
} catch (Exception) {
return 'N/B';
}
}
private function renderServerInfo(): HtmlString
{
$phpVersion = phpversion();
$laravelVersion = app()->version();
$memoryUsage = round(memory_get_usage() / 1024 / 1024, 2);
$memoryLimit = ini_get('memory_limit');
$diskUsage = $this->runCommand("df -h /var/www | tail -1 | awk '{print \$3 \"/\" \$2}'") ?: 'N/B';
$uptime = $this->runCommand('uptime -p 2>/dev/null') ?: 'N/B';
$load = sys_getloadavg();
$load1 = $load ? number_format($load[0], 2) : 'N/A';
$load5 = $load ? number_format($load[1], 2) : 'N/A';
$load15 = $load ? number_format($load[2], 2) : 'N/A';
return new HtmlString(<<<HTML
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:16px;">
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;">
<div style="background:#6366f115;padding:10px;border-radius:12px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="2"><path d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/></svg>
</div>
<span style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#94a3b8;">PHP & Laravel</span>
</div>
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #f1f5f9;">
<span style="color:#64748b;">PHP</span>
<span style="font-weight:600;color:#6366f1;">{$phpVersion}</span>
</div>
<div style="display:flex;justify-content:space-between;padding:8px 0;">
<span style="color:#64748b;">Laravel</span>
<span style="font-weight:600;color:#6366f1;">{$laravelVersion}</span>
</div>
</div>
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;">
<div style="background:#ec489915;padding:10px;border-radius:12px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ec4899" stroke-width="2"><path d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>
</div>
<span style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#94a3b8;">Memory & Disk</span>
</div>
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #f1f5f9;">
<span style="color:#64748b;">Memory</span>
<span style="font-weight:600;color:#ec4899;">{$memoryUsage}MB / {$memoryLimit}</span>
</div>
<div style="display:flex;justify-content:space-between;padding:8px 0;">
<span style="color:#64748b;">Disk</span>
<span style="font-weight:600;color:#ec4899;">{$diskUsage}</span>
</div>
</div>
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;">
<div style="background:#22c55e15;padding:10px;border-radius:12px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
</div>
<span style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#94a3b8;">Uptime</span>
</div>
<div style="font-size:16px;font-weight:700;color:#22c55e;">{$uptime}</div>
</div>
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;">
<div style="background:#f59e0b15;padding:10px;border-radius:12px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
</div>
<span style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#94a3b8;">Load</span>
</div>
<div style="display:flex;gap:8px;">
<div style="flex:1;text-align:center;padding:8px;background:#f8fafc;border-radius:8px;">
<div style="font-size:10px;color:#94a3b8;">1m</div>
<div style="font-weight:700;color:#22c55e;">{$load1}</div>
</div>
<div style="flex:1;text-align:center;padding:8px;background:#f8fafc;border-radius:8px;">
<div style="font-size:10px;color:#94a3b8;">5m</div>
<div style="font-weight:700;color:#f59e0b;">{$load5}</div>
</div>
<div style="flex:1;text-align:center;padding:8px;background:#f8fafc;border-radius:8px;">
<div style="font-size:10px;color:#94a3b8;">15m</div>
<div style="font-weight:700;color:#ef4444;">{$load15}</div>
</div>
</div>
</div>
</div>
HTML);
}
private function renderHotelStatus(): HtmlString
{
$serviceName = $this->getSetting('emulator_service_name', 'emulator');
$serviceStatus = $this->runCommand('systemctl is-active ' . escapeshellarg($serviceName) . ' 2>/dev/null') ?: 'inactive';
$serviceColor = $serviceStatus === 'active' ? '#22c55e' : '#ef4444';
$nitroClientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
$nitroRendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
$nitroWebroot = $this->getSetting('nitro_webroot', '/var/www/Client');
$clientCommit = $this->getGitCommit($nitroClientPath);
$rendererCommit = $this->getGitCommit($nitroRendererPath);
$clientExists = $this->checkPathExists($nitroClientPath);
$rendererExists = $this->checkPathExists($nitroRendererPath);
$onlineUsers = $this->getOnlineUsersCount();
$emulatorStatus = $this->getEmulatorStatusText();
$dbStatus = $this->isDatabaseOnline() ? 'Online' : 'Offline';
$dbColor = $dbStatus === 'Online' ? '#22c55e' : '#ef4444';
$clientColor = $clientExists ? '#22c55e' : '#ef4444';
$rendererColor = $rendererExists ? '#22c55e' : '#ef4444';
$clientText = $clientExists ? '✓ ' . substr($clientCommit, 0, 7) : '✗ Niet gevonden';
$rendererText = $rendererExists ? '✓ ' . substr($rendererCommit, 0, 7) : '✗ Niet gevonden';
$webrootText = $rendererExists ? '✓ ' . basename($nitroWebroot) : '✗ Niet gevonden';
$webrootColor = $rendererExists ? '#22c55e' : '#ef4444';
return new HtmlString(<<<HTML
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:16px;">
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;">
<div style="background:#3b82f615;padding:10px;border-radius:12px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2"><path d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/></svg>
</div>
<span style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#94a3b8;">Emulator</span>
</div>
<div style="display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid #f1f5f9;">
<span style="color:#64748b;">Status</span>
<span style="font-weight:600;color:{$serviceColor};background:{$serviceColor}15;padding:2px 10px;border-radius:20px;font-size:12px;">{$serviceStatus}</span>
</div>
<div style="display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid #f1f5f9;">
<span style="color:#64748b;">Online</span>
<span style="font-weight:600;color:#1e293b;">{$emulatorStatus}</span>
</div>
<div style="display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid #f1f5f9;">
<span style="color:#64748b;">Gebruikers</span>
<span style="font-weight:700;color:#22c55e;font-size:18px;">{$onlineUsers}</span>
</div>
<div style="display:flex;justify-content:space-between;padding:10px 0;">
<span style="color:#64748b;">Database</span>
<span style="font-weight:600;color:{$dbColor};">{$dbStatus}</span>
</div>
</div>
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;">
<div style="background:#8b5cf615;padding:10px;border-radius:12px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#8b5cf6" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
</div>
<span style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#94a3b8;">Nitro</span>
</div>
<div style="display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid #f1f5f9;">
<span style="color:#64748b;">Client</span>
<span style="font-weight:600;color:{$clientColor};background:{$clientColor}15;padding:2px 10px;border-radius:20px;font-size:12px;">{$clientText}</span>
</div>
<div style="display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid #f1f5f9;">
<span style="color:#64748b;">Renderer</span>
<span style="font-weight:600;color:{$rendererColor};background:{$rendererColor}15;padding:2px 10px;border-radius:20px;font-size:12px;">{$rendererText}</span>
</div>
<div style="display:flex;justify-content:space-between;padding:10px 0;">
<span style="color:#64748b;">Webroot</span>
<span style="font-weight:600;color:{$webrootColor};background:{$webrootColor}15;padding:2px 10px;border-radius:20px;font-size:12px;">{$webrootText}</span>
</div>
</div>
</div>
HTML);
}
private function getEmulatorBranchesHtml(): string
{
$githubUrl = $this->getSetting('emulator_github_url', '');
$currentBranch = $this->data['emulator_github_branch'] ?? 'main';
$branches = app(GitHubService::class)->getBranches($githubUrl);
$html = '<option value="main">main</option>';
foreach ($branches as $branch) {
$selected = $branch === $currentBranch ? 'selected' : '';
$html .= '<option value="' . $branch . '" ' . $selected . '>' . $branch . '</option>';
}
return $html;
}
private function getNitroBranchesHtml(): string
{
$githubUrl = $this->getSetting('nitro_github_url', '');
$currentBranch = $this->data['nitro_github_branch'] ?? 'main';
$branches = app(GitHubService::class)->getBranches($githubUrl);
$html = '<option value="main">main</option>';
foreach ($branches as $branch) {
$selected = $branch === $currentBranch ? 'selected' : '';
$html .= '<option value="' . $branch . '" ' . $selected . '>' . $branch . '</option>';
}
return $html;
}
private function renderAlertForm(): HtmlString
{
return new HtmlString(<<<'HTML'
<div style="display:flex;gap:16px;align-items:center;background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:16px;">
<input
type="text"
wire:model="data.hotel_alert_message"
placeholder="Typ hier je alert bericht..."
style="flex:1;padding:14px 16px;border:2px solid #e2e8f0;border-radius:10px;font-size:14px;transition:border-color 0.2s;"
onfocus="this.style.borderColor='#3b82f6'"
onblur="this.style.borderColor='#e2e8f0'"
/>
<button
wire:click="sendHotelAlert"
style="background:linear-gradient(135deg,#ef4444,#dc2626);padding:14px 28px;border-radius:10px;color:white;border:none;cursor:pointer;font-weight:600;font-size:14px;box-shadow:0 4px 12px rgba(239,68,68,0.3);transition:transform 0.2s,box-shadow 0.2s;"
onmouseover="this.style.transform='translateY(-2px)';this.style.boxShadow='0 6px 16px rgba(239,68,68,0.4)'"
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='0 4px 12px rgba(239,68,68,0.3)'"
>
📢 Verstuur Alert
</button>
</div>
HTML);
}
private function renderEmulatorInfo(): HtmlString
{
$version = $this->getSetting('emulator_version', 'Onbekend');
$serviceName = $this->getSetting('emulator_service_name', 'arcturus');
$status = $this->getEmulatorStatusText();
$color = $status === 'Online' ? '#22c55e' : '#ef4444';
return new HtmlString(<<<HTML
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:16px;">
<div style="background:#f8fafc;border-radius:12px;padding:16px;border:1px solid #e2e8f0;">
<div style="font-size:12px;color:#64748b;margin-bottom:4px;">Versie</div>
<div style="font-size:20px;font-weight:700;color:#1e293b;">{$version}</div>
</div>
<div style="background:#f8fafc;border-radius:12px;padding:16px;border:1px solid #e2e8f0;">
<div style="font-size:12px;color:#64748b;margin-bottom:4px;">Service</div>
<div style="font-size:20px;font-weight:700;color:#1e293b;">{$serviceName}</div>
</div>
<div style="background:{$color}15;border-radius:12px;padding:16px;border:1px solid {$color}30;grid-column:span 2;">
<div style="font-size:12px;color:#64748b;margin-bottom:4px;">Status</div>
<div style="font-size:24px;font-weight:700;color:{$color};">● {$status}</div>
</div>
</div>
HTML);
}
private function renderEmulatorSettings(): HtmlString
{
$status = $this->getEmulatorStatusHtml();
return new HtmlString(<<<HTML
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">GitHub URL</label>
<input type="text" wire:model="data.emulator_github_url" placeholder="https://github.com/..." style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">JAR Direct URL</label>
<input type="text" wire:model="data.emulator_jar_direct_url" placeholder="https://..." style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">JAR Pad</label>
<input type="text" wire:model="data.emulator_jar_path" placeholder="/root/emulator" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">Source Repo</label>
<input type="text" wire:model="data.emulator_source_repo" placeholder="user/repo" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">Source Pad</label>
<input type="text" wire:model="data.emulator_source_path" placeholder="/var/www/emulator" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">Branch</label>
<select wire:model="data.emulator_github_branch" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;background:#fff;">
{$this->getEmulatorBranchesHtml()}
</select>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">DB Host</label>
<input type="text" wire:model="data.emulator_database_host" placeholder="127.0.0.1" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">DB Naam</label>
<input type="text" wire:model="data.emulator_database_name" placeholder="habbo" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">Service Naam</label>
<input type="text" wire:model="data.emulator_service_name" placeholder="arcturus" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div style="grid-column:span 3;">
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">Status</label>
{$status}
</div>
</div>
HTML);
}
private function getEmulatorStatusHtml(): string
{
$serviceName = $this->getSetting('emulator_service_name', 'arcturus');
$jarPath = $this->getSetting('emulator_jar_path', '/var/www/Emulator');
$emulatorOnline = $this->getEmulatorStatusText() === 'Online';
$jarExists = $this->fileExists($jarPath);
$onlineStatus = $emulatorOnline
? '<span style="color:#22c55e;">✓ Online</span>'
: '<span style="color:#ef4444;">✗ Offline</span>';
$jarStatus = $jarExists
? '<span style="color:#22c55e;">✓ JAR OK</span>'
: '<span style="color:#ef4444;">✗ JAR ontbreekt</span>';
$serviceStatus = '<span style="color:#3b82f6;">Service: ' . e($serviceName) . '</span>';
$updateInfo = $this->checkEmulatorUpdatesFromGitHub();
return '<div style="display:flex;flex-direction:column;gap:8px;font-size:13px;">'
. '<div style="display:flex;gap:16px;">' . $onlineStatus . $jarStatus . $serviceStatus . '</div>'
. '<div style="padding-top:8px;border-top:1px solid #e2e8f0;">' . $updateInfo . '</div>'
. '</div>';
}
private function checkEmulatorUpdatesFromGitHub(): string
{
try {
$githubUrl = $this->getSetting('emulator_github_url', '');
$sourcePath = $this->getSetting('emulator_source_path', '/var/www/emulator-source');
$jarPath = $this->getSetting('emulator_jar_path', '/var/www/Emulator');
$sourceExists = $this->fileExists($sourcePath);
$jarExists = $this->fileExists($jarPath);
$sourceCommit = $this->getGitCommit($sourcePath);
$remoteVersion = $githubUrl !== '' && $githubUrl !== '0' ? $this->getRemoteCommit($githubUrl, $this->getSetting('emulator_github_branch', 'main')) : 'N/A';
$canBuild = false;
$pomPath = '';
$checkDirs = [
$sourcePath,
$sourcePath . '/Emulator',
$sourcePath . '/Emulator/Emulator',
$sourcePath . '/emulator',
$sourcePath . '/emulator/emulator',
];
foreach ($checkDirs as $dir) {
$check = $this->runCommand('test -f ' . escapeshellarg($dir . '/pom.xml') . ' && echo yes');
if ($check && trim($check) === 'yes') {
$canBuild = true;
$pomPath = $dir;
break;
}
}
$hasUpdate = $sourceCommit !== 'N/A' && $remoteVersion !== 'N/A' && substr($sourceCommit, 0, 7) !== $remoteVersion;
$updateColor = $hasUpdate ? '#f59e0b' : '#22c55e';
$updateText = $hasUpdate ? '🔄 Update beschikbaar' : '✓ Up-to-date';
$html = '<div style="font-weight:600;color:#475569;margin-bottom:8px;">GitHub Status:</div>';
// Update status
$html .= '<div style="background:' . ($hasUpdate ? '#fef3c7' : '#dcfce7') . ';padding:12px;border-radius:8px;margin-bottom:12px;">';
$html .= '<div style="display:flex;align-items:center;justify-content:space-between;">';
$html .= '<div style="display:flex;align-items:center;gap:8px;font-weight:600;color:' . $updateColor . ';">';
$html .= $updateText;
$html .= '</div>';
// Always show update button
$btnColor = $hasUpdate ? '#f59e0b' : '#3b82f6';
$btnGradient = $hasUpdate ? 'linear-gradient(135deg,#f59e0b,#d97706)' : 'linear-gradient(135deg,#3b82f6,#2563eb)';
$btnText = $hasUpdate ? '⚡ Updaten' : '🔄 Herbouwen';
$html .= '<button
wire:click="checkEmulatorUpdates"
style="background:' . $btnGradient . ';padding:8px 16px;border-radius:6px;color:white;border:none;cursor:pointer;font-weight:600;font-size:12px;box-shadow:0 2px 8px rgba(0,0,0,0.2);transition:transform 0.2s,box-shadow 0.2s;"
onmouseover="this.style.transform=\'translateY(-1px)\';this.style.boxShadow=\'0 4px 12px rgba(0,0,0,0.3)\'"
onmouseout="this.style.transform=\'translateY(0)\';this.style.boxShadow=\'0 2px 8px rgba(0,0,0,0.2)\'"
>
' . $btnText . '
</button>';
$html .= '</div></div>';
// Version info - use short hash for comparison
$sourceCommitShort = substr($sourceCommit, 0, 7);
$html .= '<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f1f5f9;"><span>Latest:</span><span style="color:#22c55e;font-weight:600;">✓ ' . e($remoteVersion) . '</span></div>';
$html .= '<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f1f5f9;"><span>Source:</span><span style="color:#22c55e;font-weight:600;">✓ ' . $sourceCommitShort . '</span></div>';
// Build method
if ($jarExists) {
$jarSize = $this->runCommand('ls -lh ' . escapeshellarg($jarPath) . '/*.jar 2>/dev/null | head -1');
if ($jarSize) {
preg_match('/(\S+\.jar)/', $jarSize, $matches);
if (isset($matches[1])) {
$html .= '<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f1f5f9;"><span>JAR:</span><span style="color:#22c55e;font-weight:600;">✓ ' . basename($matches[1]) . '</span></div>';
}
}
}
if ($canBuild) {
$html .= '<div style="display:flex;justify-content:space-between;padding:6px 0;"><span>Bouwen:</span><span style="color:#22c55e;font-weight:600;">✓ Maven (pom.xml)</span></div>';
} else {
$html .= '<div style="display:flex;justify-content:space-between;padding:6px 0;"><span>Bouwen:</span><span style="color:#f59e0b;font-weight:600;">⚠️ Geen pom.xml</span></div>';
}
// Auto-update method
$html .= '<div style="margin-top:12px;padding:12px;background:#f8fafc;border-radius:8px;">';
$html .= '<div style="font-size:11px;color:#64748b;font-weight:600;margin-bottom:8px;">METHODE:</div>';
if ($jarExists) {
$html .= '<div style="color:#3b82f6;font-weight:600;">📦 JAR Download & Herstart</div>';
} elseif ($canBuild) {
$html .= '<div style="color:#3b82f6;font-weight:600;">🔨 Maven Build & Herstart</div>';
} else {
$html .= '<div style="color:#64748b;">Handmatig: Download JAR van GitHub</div>';
}
return $html . '</div>';
} catch (Exception) {
return '<span style="color:#64748b;">Kon emulator status niet ophalen</span>';
}
}
private function getRemoteCommit(string $githubUrl, string $branch = 'main'): string
{
$commit = app(GitHubService::class)->getLatestCommit($githubUrl, $branch);
return $commit ?? 'N/A';
}
private function renderNitroSettings(): HtmlString
{
$status = $this->getNitroStatusHtml();
return new HtmlString(<<<HTML
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:12px;">
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">Client Pad</label>
<input type="text" wire:model="data.nitro_client_path" placeholder="/var/www/atomcms/nitro-client" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">Renderer Pad</label>
<input type="text" wire:model="data.nitro_renderer_path" placeholder="/var/www/atomcms/nitro-renderer" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">Build Pad</label>
<input type="text" wire:model="data.nitro_build_path" placeholder="/var/www/atomcms/nitro-client/dist" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">Webroot</label>
<input type="text" wire:model="data.nitro_webroot" placeholder="/var/www/Client" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">GitHub URL</label>
<input type="text" wire:model="data.nitro_github_url" placeholder="https://github.com/..." style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">Branch</label>
<select wire:model="data.nitro_github_branch" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;background:#fff;">
{$this->getNitroBranchesHtml()}
</select>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">Site URL</label>
<input type="text" wire:model="data.nitro_site_url" placeholder="https://hotel.nl" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">Status</label>
{$status}
</div>
</div>
HTML);
}
private function getNitroStatusHtml(): string
{
$clientPath = $this->getSetting('nitro_client_path', '/var/www/atomcms/nitro-client');
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/atomcms/nitro-renderer');
$webroot = $this->getSetting('nitro_webroot', '/var/www/Client');
$clientExists = $this->checkPathExists($clientPath);
$rendererExists = $this->checkPathExists($rendererPath);
$webrootExists = $this->checkPathExists($webroot);
$clientStatus = $clientExists ? '<span style="color:#22c55e;">✓ Client OK</span>' : '<span style="color:#ef4444;">✗ Client ontbreekt</span>';
$rendererStatus = $rendererExists ? '<span style="color:#22c55e;">✓ Renderer OK</span>' : '<span style="color:#ef4444;">✗ Renderer ontbreekt</span>';
$webrootStatus = $webrootExists ? '<span style="color:#22c55e;">✓ Webroot OK</span>' : '<span style="color:#ef4444;">✗ Webroot ontbreekt</span>';
$updateInfo = $this->checkNitroUpdatesFromGitHub();
return '<div style="display:flex;flex-direction:column;gap:4px;font-size:13px;">'
. $clientStatus . $rendererStatus . $webrootStatus
. '<div style="margin-top:8px;padding-top:8px;border-top:1px solid #e2e8f0;">' . $updateInfo . '</div>'
. '</div>';
}
private function checkNitroUpdatesFromGitHub(): string
{
try {
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
$clientGithubUrl = $this->getSetting('nitro_github_url', '');
// Use the renderer-specific repo (defaults to Nitro_Render_V3 if not set)
$rendererGithubUrl = $this->getSetting('nitro_renderer_github_url', 'https://github.com/duckietm/Nitro_Render_V3');
$clientCommit = $this->getGitCommit($clientPath);
$rendererCommit = $this->getGitCommit($rendererPath);
$clientRemote = $clientGithubUrl !== '' && $clientGithubUrl !== '0' ? $this->getRemoteCommit($clientGithubUrl, $this->getSetting('nitro_github_branch', 'main')) : 'N/A';
$rendererRemote = $rendererGithubUrl !== '' && $rendererGithubUrl !== '0' ? $this->getRemoteCommit($rendererGithubUrl, $this->getSetting('nitro_renderer_github_branch', 'main')) : 'N/A';
// Compare only first 7 characters (short hash)
$clientCommitShort = substr($clientCommit, 0, 7);
$rendererCommitShort = substr($rendererCommit, 0, 7);
$hasClientUpdate = $clientCommitShort !== 'N/A' && $clientRemote !== 'N/A' && $clientCommitShort !== $clientRemote;
$hasRendererUpdate = $rendererCommitShort !== 'N/A' && $rendererRemote !== 'N/A' && $rendererCommitShort !== $rendererRemote;
$hasUpdate = $hasClientUpdate || $hasRendererUpdate;
$updateColor = $hasUpdate ? '#f59e0b' : '#22c55e';
$updateText = $hasUpdate ? '🔄 Update beschikbaar' : '✓ Up-to-date';
$html = '<div style="font-weight:600;color:#475569;margin-bottom:8px;">GitHub Status:</div>';
// Update status
$html .= '<div style="background:' . ($hasUpdate ? '#fef3c7' : '#dcfce7') . ';padding:12px;border-radius:8px;margin-bottom:12px;">';
$html .= '<div style="display:flex;align-items:center;justify-content:space-between;">';
$html .= '<div style="display:flex;align-items:center;gap:8px;font-weight:600;color:' . $updateColor . ';">';
$html .= $updateText;
$html .= '</div>';
// Update button if update available
if ($hasUpdate) {
$html .= '<button
wire:click="checkNitroUpdates"
style="background:linear-gradient(135deg,#ec4899,#db2777);padding:8px 16px;border-radius:6px;color:white;border:none;cursor:pointer;font-weight:600;font-size:12px;box-shadow:0 2px 8px rgba(236,72,153,0.4);transition:transform 0.2s,box-shadow 0.2s;"
onmouseover="this.style.transform=\'translateY(-1px)\';this.style.boxShadow=\'0 4px 12px rgba(236,72,153,0.5)\'"
onmouseout="this.style.transform=\'translateY(0)\';this.style.boxShadow=\'0 2px 8px rgba(236,72,153,0.4)\'"
>
⚡ Updaten
</button>';
}
$html .= '</div></div>';
// Client info
$clientColor = $hasClientUpdate ? '#f59e0b' : '#22c55e';
$clientIcon = $hasClientUpdate ? '🔄' : '✓';
$html .= '<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f1f5f9;"><span>Client Remote:</span><span style="color:#22c55e;font-weight:600;">✓ ' . e($clientRemote) . '</span></div>';
$html .= '<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f1f5f9;"><span>Client Local:</span><span style="color:' . $clientColor . ';font-weight:600;">' . $clientIcon . ' ' . $clientCommitShort . '</span></div>';
// Renderer info
$rendererColor = $hasRendererUpdate ? '#f59e0b' : '#22c55e';
$rendererIcon = $hasRendererUpdate ? '🔄' : '✓';
$html .= '<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f1f5f9;"><span>Renderer Remote:</span><span style="color:#22c55e;font-weight:600;">✓ ' . e($rendererRemote) . '</span></div>';
return $html . ('<div style="display:flex;justify-content:space-between;padding:6px 0;"><span>Renderer Local:</span><span style="color:' . $rendererColor . ';font-weight:600;">' . $rendererIcon . ' ' . $rendererCommitShort . '</span></div>');
} catch (Exception) {
return '<span style="color:#64748b;">Kon updates niet ophalen</span>';
}
}
private function getGitCommit(string $path): string
{
if (! $this->fileExists($path)) {
return 'N/A';
}
$subdirs = ['', 'Emulator', 'emulator', 'src', 'client'];
foreach ($subdirs as $subdir) {
$fullPath = $subdir !== '' ? $path . '/' . $subdir : $path;
$gitPath = $fullPath . '/.git';
if ($this->dirExists($gitPath)) {
$headFile = $gitPath . '/HEAD';
$headContent = $this->readFile($headFile);
if ($headContent) {
if (str_contains($headContent, 'ref:')) {
preg_match('/ref: refs\/heads\/(\S+)/', $headContent, $matches);
if (isset($matches[1])) {
$branchRef = $gitPath . '/refs/heads/' . $matches[1];
$branchContent = $this->readFile($branchRef);
if ($branchContent) {
return trim($branchContent);
}
}
} else {
return trim($headContent);
}
}
}
}
return 'N/A';
}
private function checkPathExists(string $path): bool
{
return $this->fileExists($path);
}
public function getUpdateHistoryHtml(): HtmlString
{
try {
$history = app(UpdateHistoryService::class)->getRecent(10);
if (empty($history)) {
return new HtmlString('<div style="padding:20px;text-align:center;color:#64748b;">Geen updates gevonden</div>');
}
$html = '<div style="display:flex;flex-direction:column;gap:8px;">';
foreach ($history as $update) {
$statusColor = $update->status === 'success' ? '#22c55e' : ($update->status === 'pending' ? '#f59e0b' : '#ef4444');
$html .= '<div style="display:flex;align-items:center;justify-content:space-between;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:12px 16px;">';
$html .= '<div><span style="font-weight:600;color:#1e293b;">' . e($update->type) . '</span><span style="color:#64748b;margin-left:8px;">' . e($update->message) . '</span></div>';
$html .= '<div style="display:flex;align-items:center;gap:8px;"><span style="color:' . $statusColor . ';font-weight:600;">' . e($update->status) . '</span><span style="color:#94a3b8;font-size:12px;">' . e($update->created_at) . '</span></div>';
$html .= '</div>';
}
$html .= '</div>';
return new HtmlString($html);
} catch (Exception) {
return new HtmlString('<div style="padding:20px;text-align:center;color:#64748b;">Kon historie niet laden</div>');
}
}
public function sendHotelAlert(): void
{
$result = app(EmulatorControlAction::class)->sendAlert($this->data['hotel_alert_message'] ?? '');
$this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger');
if ($result['success']) {
$this->data['hotel_alert_message'] = '';
}
}
public function startEmulator(): void
{
$result = app(EmulatorControlAction::class)->start();
$this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger');
}
public function stopEmulator(): void
{
$result = app(EmulatorControlAction::class)->stop();
$this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger');
}
public function restartEmulator(): void
{
$result = app(EmulatorControlAction::class)->restart();
$this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger');
}
public function checkEmulator(): void
{
try {
Artisan::call('monitor:emulator', ['--notify-online' => true]);
$rconService = new RconService;
if ($rconService->isConnected()) {
$this->notify('Success', 'Emulator is online en reageert!', 'success');
} else {
$this->notify('Warning', 'Emulator is niet bereikbaar via RCON', 'warning');
}
$this->fillForm();
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
public function checkEmulatorUpdates(): void
{
$result = app(EmulatorControlAction::class)->update();
$this->notify($result['success'] ?? false ? 'Success' : 'Error', $result['message'] ?? $result['error'] ?? 'Onbekende fout', ($result['success'] ?? false) ? 'success' : 'danger');
Cache::forget('all_updates_check');
$this->fillForm();
}
public function buildEmulator(): void
{
$result = app(EmulatorControlAction::class)->build();
$this->notify($result['success'] ?? false ? 'Success' : 'Error', $result['message'] ?? $result['error'] ?? 'Onbekende fout', ($result['success'] ?? false) ? 'success' : 'danger');
}
public function renderBackupsList(): HtmlString
{
try {
$backups = app(EmulatorControlAction::class)->getBackups();
if ($backups === []) {
return new HtmlString(<<<'HTML'
<div style="text-align:center;padding:32px;color:#64748b;background:#f8fafc;border-radius:12px;border:1px solid #e2e8f0;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="2" style="margin:0 auto 16px;display:block;">
<path d="M21 8v13H3V8M1 3h22v5H1zM10 12h4"/>
</svg>
<div style="font-size:14px;">Nog geen backups beschikbaar</div>
<div style="font-size:12px;margin-top:8px;">Backups worden automatisch aangemaakt bij elke emulator update</div>
</div>
HTML);
}
$html = '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;">';
foreach ($backups as $backup) {
$dateFormatted = str_replace('_', ' ', $backup['date']);
$html .= <<<HTML
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:8px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2">
<path d="M21 8v13H3V8M1 3h22v5H1zM10 12h4"/>
</svg>
<span style="font-weight:600;color:#1e293b;font-size:14px;">{$backup['jar']}</span>
</div>
</div>
<div style="font-size:12px;color:#64748b;margin-bottom:12px;">{$dateFormatted}</div>
<button
wire:click="restoreBackup('{$backup['name']}')"
style="width:100%;background:linear-gradient(135deg,#3b82f6,#2563eb);padding:10px;border-radius:8px;color:white;border:none;cursor:pointer;font-weight:600;font-size:13px;box-shadow:0 2px 8px rgba(37,99,235,0.3);transition:transform 0.2s;"
onmouseover="this.style.transform='translateY(-1px)'"
onmouseout="this.style.transform='translateY(0)'"
>
🔄 Herstellen
</button>
</div>
HTML;
}
$html .= '</div>';
return new HtmlString($html);
} catch (Exception $e) {
return new HtmlString('<div style="color:#ef4444;">Kon backups niet laden: ' . e($e->getMessage()) . '</div>');
}
}
public function runSqlUpdates(): void
{
$result = app(EmulatorControlAction::class)->runSqlUpdates();
$this->notify($result['success'] ?? false ? 'Success' : 'Error', $result['message'] ?? 'Onbekende fout', ($result['success'] ?? false) ? 'success' : 'danger');
}
public function restoreBackup(string $backupName): void
{
$result = app(EmulatorControlAction::class)->restoreBackup($backupName);
$this->notify($result['success'] ? 'Success' : 'Error', $result['message'] ?? $result['error'] ?? 'Onbekende fout', $result['success'] ? 'success' : 'danger');
$this->fillForm();
}
public function saveEmulator(): void
{
try {
$settings = app(SettingsService::class);
$settings->set('emulator_github_url', $this->data['emulator_github_url'] ?? '');
$settings->set('emulator_jar_direct_url', $this->data['emulator_jar_direct_url'] ?? '');
$settings->set('emulator_jar_path', $this->data['emulator_jar_path'] ?? '/root/emulator');
$settings->set('emulator_source_repo', $this->data['emulator_source_repo'] ?? '');
$settings->set('emulator_source_path', $this->data['emulator_source_path'] ?? '/var/www/emulator-source');
$settings->set('emulator_github_branch', $this->data['emulator_github_branch'] ?? 'main');
$settings->set('emulator_database_host', $this->data['emulator_database_host'] ?? '127.0.0.1');
$settings->set('emulator_database_name', $this->data['emulator_database_name'] ?? '');
$settings->set('emulator_service_name', $this->data['emulator_service_name'] ?? 'arcturus');
$this->notify('Success', 'Emulator instellingen opgeslagen!', 'success');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
public function checkNitroUpdates(): void
{
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
$branch = $this->getSetting('nitro_github_branch', 'main');
$result = app(NitroControlAction::class)->pullUpdates($clientPath, $rendererPath, $branch);
$this->notify('Success', $result['message'], 'success');
$this->fillForm();
}
public function buildNitro(): void
{
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
$branch = $this->getSetting('nitro_github_branch', 'main');
$result = app(NitroControlAction::class)->build($clientPath, $rendererPath, $branch);
$this->notify($result['success'] ? 'Success' : 'Warning', $result['message'], $result['success'] ? 'success' : 'warning');
}
public function generateNitroConfigs(): void
{
$siteUrl = $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl());
$webroot = $this->getSetting('nitro_webroot', '/var/www/Client');
$gamedataPath = $this->getSetting('gamedata_path', '/var/www/Gamedata');
$result = app(NitroControlAction::class)->generateConfigs($siteUrl, $webroot, $gamedataPath);
$this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger');
$this->fillForm();
}
private function renderClothingStatus(): HtmlString
{
try {
$count = DB::table('catalog_clothing')->count();
$html = '<div style="display:flex;gap:16px;align-items:center;">';
$html .= '<div style="background:#f8fafc;border-radius:12px;padding:16px;border:1px solid #e2e8f0;flex:1;">';
$html .= '<div style="font-size:12px;color:#64748b;margin-bottom:4px;">Kleding Items</div>';
$html .= '<div style="font-size:24px;font-weight:700;color:#1e293b;">' . number_format($count) . '</div>';
$html .= '</div>';
$html .= '</div>';
return new HtmlString($html);
} catch (Exception $e) {
return new HtmlString('<div style="color:#ef4444;">Fout: ' . e($e->getMessage()) . '</div>');
}
}
public function syncClothing(): void
{
try {
$catalogService = app(CatalogService::class);
$result = $catalogService->syncCatalogClothing();
$message = '👔 Kleding Sync Resultaat:' . PHP_EOL;
$message .= '• Toegevoegd: ' . $result['inserted'] . PHP_EOL;
$message .= '• Totaal: ' . $result['total'];
$this->notify('Success', $message, 'success');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
public function detectAndSavePaths(): void
{
try {
$paths = $this->autoDetectPaths();
$settings = app(SettingsService::class);
$settings->set('nitro_client_path', $paths['nitro_client_path']);
$settings->set('nitro_renderer_path', $paths['nitro_renderer_path']);
$settings->set('nitro_build_path', $paths['nitro_build_path']);
$settings->set('nitro_webroot', $paths['nitro_webroot']);
$settings->set('gamedata_path', $paths['gamedata_path']);
$settings->set('emulator_jar_path', $paths['emulator_jar_path']);
$settings->set('emulator_source_path', $paths['emulator_source_path']);
$this->fillForm();
$this->notify('Success', 'Paths gedetecteerd en opgeslagen!', 'success');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
public function saveNitro(): void
{
try {
$settings = app(SettingsService::class);
$settings->set('nitro_client_path', $this->data['nitro_client_path'] ?? '/var/www/atomcms/nitro-client');
$settings->set('nitro_renderer_path', $this->data['nitro_renderer_path'] ?? '/var/www/atomcms/nitro-renderer');
$settings->set('nitro_build_path', $this->data['nitro_build_path'] ?? '/var/www/atomcms/nitro-client/dist');
$settings->set('nitro_webroot', $this->data['nitro_webroot'] ?? '/var/www/Client');
$settings->set('gamedata_path', $this->data['gamedata_path'] ?? '/var/www/Gamedata');
$settings->set('nitro_github_url', $this->data['nitro_github_url'] ?? '');
$settings->set('nitro_github_branch', $this->data['nitro_github_branch'] ?? 'main');
$settings->set('nitro_site_url', $this->data['nitro_site_url'] ?? '');
$this->notify('Success', 'Nitro instellingen opgeslagen!', 'success');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
public function saveAutoUpdate(): void
{
try {
$settings = app(SettingsService::class);
$settings->set('auto_update_enabled', ($this->data['auto_update_enabled'] ?? false) ? '1' : '0');
$settings->set('auto_update_schedule', $this->data['auto_update_schedule'] ?? '03:00');
$settings->set('auto_update_days', $this->data['auto_update_days'] ?? '0,6');
$this->notify('Success', 'Auto update instellingen opgeslagen!', 'success');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
public function saveAlerts(): void
{
try {
$settings = app(SettingsService::class);
$settings->set('alert_email_enabled', ($this->data['alert_email_enabled'] ?? false) ? '1' : '0');
$settings->set('alert_email_address', $this->data['alert_email_address'] ?? '');
$settings->set('alert_discord_enabled', ($this->data['alert_discord_enabled'] ?? false) ? '1' : '0');
$settings->set('alert_discord_webhook_url', $this->data['alert_discord_webhook_url'] ?? '');
$settings->set('discord_webhook_ranks', json_encode($this->data['discord_webhook_ranks'] ?? []));
$this->notify('Success', 'Meldingen opgeslagen!', 'success');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
private function getStaffActivityHtml(): HtmlString
{
try {
$activities = StaffActivity::with('user:id,username,look')
->orderByDesc('created_at')
->limit(20)
->get();
if ($activities->isEmpty()) {
return new HtmlString(<<<'HTML'
<div style="text-align:center;padding:40px;color:#9ca3af;">
<svg width="48" height="48" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="margin:0 auto 16px;opacity:0.5;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<p>No staff activities recorded yet.</p>
<p style="font-size:12px;margin-top:8px;">Staff actions will appear here automatically.</p>
</div>
HTML);
}
$itemsHtml = '';
foreach ($activities as $activity) {
$icon = StaffActivity::getActionIcon($activity->action);
$color = StaffActivity::getActionColor($activity->action);
$timeAgo = $this->getTimeAgo($activity->created_at);
$username = $activity->user->username ?? 'Unknown';
$userLook = $activity->user->look ?? '';
$itemsHtml .= <<<HTML
<div style="display:flex;align-items:center;gap:12px;padding:12px;border-bottom:1px solid #e2e8f0;">
<div style="width:36px;height:36px;border-radius:50%;background:#f1f5f9;display:flex;align-items:center;justify-content:center;font-size:18px;">
{$icon}
</div>
<div style="flex:1;">
<div style="font-weight:600;color:#1e293b;font-size:14px;">{$username}</div>
<div style="color:#64748b;font-size:12px;">{$activity->description}</div>
</div>
<div style="text-align:right;">
<span style="font-size:11px;color:#9ca3af;">{$timeAgo}</span>
</div>
</div>
HTML;
}
return new HtmlString(<<<HTML
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:12px;overflow:hidden;">
<div style="padding:16px;border-bottom:1px solid #e2e8f0;display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:600;color:#1e293b;">Recent Staff Activities</span>
<span style="font-size:12px;color:#9ca3af;">Last 20 actions</span>
</div>
<div style="max-height:400px;overflow-y:auto;">
{$itemsHtml}
</div>
</div>
HTML);
} catch (Exception $e) {
return new HtmlString(<<<'HTML'
<div style="text-align:center;padding:40px;color:#ef4444;">
<p>Error loading staff activities</p>
<p style="font-size:12px;margin-top:8px;">Make sure to run: php artisan migrate</p>
</div>
HTML);
}
}
private function getTimeAgo($timestamp): string
{
try {
$carbon = Carbon::parse($timestamp);
$now = Carbon::now();
$diff = $now->diffInMinutes($carbon);
if ($diff < 1) {
return 'Just now';
} elseif ($diff < 60) {
return $diff . 'm ago';
} elseif ($diff < 1440) {
return floor($diff / 60) . 'h ago';
} else {
return floor($diff / 1440) . 'd ago';
}
} catch (Exception) {
return 'Unknown';
}
}
public function testDiscord(): void
{
try {
$webhookUrl = $this->data['alert_discord_webhook_url'] ?? '';
if (empty($webhookUrl)) {
$this->notify('Error', 'Webhook URL is leeg', 'danger');
return;
}
Http::post($webhookUrl, ['content' => '✅ Test van Atom CMS Commandocentrum']);
$this->notify('Success', 'Test bericht verzonden!', 'success');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
private function notify(string $title, string $message, string $color): void
{
Notification::make()
->title($title)
->body($message)
->color($color)
->send();
}
public function refreshDiagnostics(): void
{
$this->runDiagnostics();
$this->notify('Success', 'Diagnostiek vernieuwd', 'success');
}
private function runDiagnostics(): void
{
$runner = app(DiagnosticRunner::class);
$this->diagnostics = $runner->runAll();
}
private function renderDiagnostics(): HtmlString
{
if ($this->diagnostics === []) {
$this->runDiagnostics();
}
$errors = array_filter($this->diagnostics, fn ($r) => $r->status === 'error');
$warnings = array_filter($this->diagnostics, fn ($r) => $r->status === 'warning');
$ok = array_filter($this->diagnostics, fn ($r) => $r->status === 'ok');
$errorCount = count($errors);
$warningCount = count($warnings);
$okCount = count($ok);
$overallStatus = $errorCount > 0 ? 'error' : ($warningCount > 0 ? 'warning' : 'ok');
$overallColor = match ($overallStatus) {
'error' => '#ef4444',
'warning' => '#f59e0b',
default => '#22c55e',
};
$overallLabel = match ($overallStatus) {
'error' => 'Kritieke Problemen',
'warning' => 'Waarschuwingen',
default => 'Gezond',
};
$html = '<div style="display:flex;flex-direction:column;gap:16px;">';
// Summary cards
$html .= '<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">';
$html .= $this->getSummaryCardHtml('Gezond', $okCount, '#22c55e', 'heroicon-o-check-circle');
$html .= $this->getSummaryCardHtml('Waarschuwingen', $warningCount, '#f59e0b', 'heroicon-o-exclamation-triangle');
$html .= $this->getSummaryCardHtml('Fouten', $errorCount, '#ef4444', 'heroicon-o-x-circle');
$html .= '</div>';
// Overall status banner
$html .= '<div style="background:{$overallColor}15;border:1px solid {$overallColor}30;border-radius:12px;padding:16px;display:flex;align-items:center;gap:12px;">';
$html .= '<div style="width:12px;height:12px;border-radius:50%;background:{$overallColor};"></div>';
$html .= '<span style="font-weight:700;color:{$overallColor};font-size:16px;">Systeem Status: {$overallLabel}</span>';
$html .= '</div>';
// Detailed results
if ($errorCount > 0 || $warningCount > 0) {
$html .= '<div style="display:flex;flex-direction:column;gap:8px;">';
foreach ($this->diagnostics as $result) {
if ($result->status === 'ok') {
continue;
}
$color = $result->status === 'error' ? '#ef4444' : '#f59e0b';
$icon = $result->status === 'error'
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="' . $color . '" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>'
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="' . $color . '" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
$html .= '<div style="background:#fff;border:1px solid ' . $color . '30;border-radius:10px;padding:14px 16px;display:flex;align-items:flex-start;gap:12px;">';
$html .= '<div style="flex-shrink:0;margin-top:2px;">' . $icon . '</div>';
$html .= '<div style="flex:1;">';
$html .= '<div style="font-weight:600;color:#1e293b;font-size:14px;">' . e($result->name) . '</div>';
$html .= '<div style="color:#64748b;font-size:13px;margin-top:2px;">' . e($result->message) . '</div>';
if ($result->fix) {
$html .= '<div style="background:#f8fafc;border-radius:6px;padding:8px 12px;margin-top:8px;font-size:12px;color:#475569;font-family:monospace;">💡 ' . e($result->fix) . '</div>';
}
$html .= '</div></div>';
}
$html .= '</div>';
}
$html .= '</div>';
return new HtmlString($html);
}
private function getSummaryCardHtml(string $label, int $count, string $color, string $icon): string
{
$iconSvg = match ($icon) {
'heroicon-o-check-circle' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />',
'heroicon-o-exclamation-triangle' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />',
'heroicon-o-x-circle' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" />',
default => '',
};
return <<<HTML
<div style="background:#fff;border-radius:12px;padding:16px;border:1px solid #e2e8f0;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
<div style="background:{$color}15;padding:8px;border-radius:10px;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="{$color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
{$iconSvg}
</svg>
</div>
<span style="font-size:12px;font-weight:600;color:#64748b;">{$label}</span>
</div>
<div style="font-size:28px;font-weight:800;color:{$color};line-height:1;">{$count}</div>
</div>
HTML;
}
private function runCommand(string $command, int $timeout = 10): ?string
{
$result = Process::timeout($timeout)->run($command);
return $result->successful() ? trim($result->output()) : null;
}
private function fileExists(string $path): bool
{
return $this->runCommand('test -e ' . escapeshellarg($path) . ' && echo yes') === 'yes';
}
private function dirExists(string $path): bool
{
return $this->runCommand('test -d ' . escapeshellarg($path) . ' && echo yes') === 'yes';
}
private function readFile(string $path): ?string
{
return $this->runCommand('cat ' . escapeshellarg($path) . ' 2>/dev/null');
}
}