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

889 lines
42 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Pages\Monitoring;
use App\Actions\Commandocentrum\EmulatorControlAction;
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\DiagnosticResult;
use App\Services\Diagnostics\DiagnosticRunner;
use App\Services\GitHubService;
use App\Services\RconService;
use App\Services\SettingsService;
use BackedEnum;
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\Contracts\View\View;
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;
use function __;
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<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'),
'nitro_emulator_path' => $this->getSetting('nitro_emulator_path', '/var/www/emulator'),
'nitro_emulator_service' => $this->getSetting('nitro_emulator_service', 'emulator'),
'nitro_db_name' => $this->getSetting('nitro_db_name', 'habbo'),
'nitro_sql_dir' => $this->getSetting('nitro_sql_dir', '/var/www/emulator/Database Updates'),
'nitro_backup_dir' => $this->getSetting('nitro_backup_dir', '/var/www/emulator/Database Updates/backups'),
'nitro_gamedata_dir' => $this->getSetting('nitro_gamedata_dir', '/var/www/Gamedata/config'),
'nitro_client_dir' => $this->getSetting('nitro_client_dir', '/var/www/Nitro-V3/public/configuration'),
'nitro_client_src' => $this->getSetting('nitro_client_src', '/var/www/Nitro-V3'),
'nitro_renderer_src' => $this->getSetting('nitro_renderer_src', '/var/www/Nitro_Render_V3'),
'hotel_alert_message' => '',
];
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Section::make(__('commandocentrum.live_status'))
->description(__('commandocentrum.live_status_desc'))
->icon('heroicon-o-heart')
->columns(4)
->schema([
Placeholder::make('online_users')
->label(__('commandocentrum.online'))
->content(fn (): HtmlString => $this->getCardHtml(__('commandocentrum.online'), $this->getOnlineUsersCount(), '#22c55e', 'heroicon-o-users')),
Placeholder::make('emulator_status')
->label(__('commandocentrum.emulator'))
->content(fn (): HtmlString => $this->getCardHtml(__('commandocentrum.emulator'), $this->getEmulatorStatusText(), $this->getEmulatorStatusColor(), 'heroicon-o-server')),
Placeholder::make('database_status')
->label(__('commandocentrum.database'))
->content(fn (): HtmlString => $this->getCardHtml(__('commandocentrum.database'), $this->isDatabaseOnline() ? __('commandocentrum.online') : __('commandocentrum.offline'), $this->isDatabaseOnline() ? '#22c55e' : '#ef4444', 'heroicon-o-circle-stack')),
Placeholder::make('server_load')
->label(__('commandocentrum.load'))
->content(fn (): HtmlString => $this->getCardHtml(__('commandocentrum.load'), $this->getServerLoad(), '#3b82f6', 'heroicon-o-cpu-chip')),
]),
Section::make(__('commandocentrum.server_info'))
->description(__('commandocentrum.server_info_desc'))
->icon('heroicon-o-information-circle')
->schema([
Placeholder::make('server_info')
->label('')
->content(fn () => $this->renderServerInfoView()),
]),
Section::make(__('commandocentrum.system_health'))
->description(__('commandocentrum.system_health_desc'))
->icon('heroicon-o-heart')
->afterHeader([
Action::make('refresh_diagnostics')
->label(__('commandocentrum.refresh'))
->icon('heroicon-o-arrow-path')
->color('info')
->action('refreshDiagnostics'),
])
->schema([
Placeholder::make('diagnostics')
->label('')
->content(fn (): HtmlString => $this->renderDiagnostics()),
]),
Section::make(__('commandocentrum.hotel_status'))
->description(__('commandocentrum.hotel_status_desc'))
->icon('heroicon-o-building-office')
->schema([
Placeholder::make('hotel_status')
->label('')
->content(fn () => $this->renderHotelStatusView()),
]),
Section::make(__('commandocentrum.hotel_alert'))
->description(__('commandocentrum.hotel_alert_desc'))
->icon('heroicon-o-megaphone')
->schema([
Placeholder::make('alert_form')
->label('')
->content(fn () => view('filament.components.commandocentrum.alert-form')),
]),
Section::make(__('commandocentrum.emulator_logs'))
->description(__('commandocentrum.emulator_logs_desc'))
->icon('heroicon-o-document-text')
->columnSpanFull()
->schema([
Placeholder::make('emulator_logs')
->label('')
->content(fn () => view('filament.components.emulator-log-viewer')),
]),
Section::make(__('commandocentrum.emulator_control'))
->description(__('commandocentrum.emulator_control_desc'))
->icon('heroicon-o-server')
->columns(3)
->afterHeader([
Action::make('start_emulator')
->label(__('commandocentrum.start'))
->icon('heroicon-o-play')
->color('success')
->action('startEmulator'),
Action::make('stop_emulator')
->label(__('commandocentrum.stop'))
->icon('heroicon-o-stop')
->color('danger')
->action('stopEmulator'),
Action::make('restart_emulator')
->label(__('commandocentrum.restart'))
->icon('heroicon-o-arrow-path')
->color('warning')
->action('restartEmulator'),
Action::make('check_emulator')
->label(__('commandocentrum.check'))
->icon('heroicon-o-check-circle')
->color('info')
->action('checkEmulator'),
])
->schema([
Placeholder::make('emulator_info')
->label('')
->content(fn () => $this->renderEmulatorInfoView()),
]),
Section::make(__('commandocentrum.nitro_update'))
->description(__('commandocentrum.nitro_update_desc'))
->icon('heroicon-o-arrow-path')
->schema([
Placeholder::make('nitro_cli_only')
->label('')
->content(__('commandocentrum.nitro_cli_only')),
]),
Section::make(__('commandocentrum.clothing_sync'))
->description(__('commandocentrum.clothing_sync_desc'))
->icon('heroicon-o-user')
->afterHeader([
Action::make('sync_clothing')
->label('🔄 ' . __('commandocentrum.sync'))
->color('success')
->action('syncClothing'),
])
->schema([
Placeholder::make('clothing_status')
->label('')
->content(fn () => $this->renderClothingStatusView()),
]),
Section::make(__('commandocentrum.notifications'))
->description(__('commandocentrum.notifications_desc'))
->icon('heroicon-o-bell')
->columns(2)
->afterHeader([
Action::make('save_alerts')
->label(__('commandocentrum.save'))
->color('primary')
->action('saveAlerts'),
Action::make('test_discord')
->label(__('commandocentrum.test_discord'))
->color('info')
->action('testDiscord'),
])
->schema([
Toggle::make('alert_email_enabled')
->label(__('commandocentrum.email_notifications')),
TextInput::make('alert_email_address')
->label(__('commandocentrum.email_address'))
->email()
->columnSpanFull(),
Toggle::make('alert_discord_enabled')
->label(__('commandocentrum.discord_notifications')),
TextInput::make('alert_discord_webhook_url')
->label(__('commandocentrum.webhook_url'))
->columnSpanFull(),
Select::make('discord_webhook_ranks')
->label(__('commandocentrum.discord_ranks'))
->multiple()
->options(fn () => WebsitePermission::query()->pluck('permission', 'min_rank')->mapWithKeys(fn ($perm, $rank) => [$rank => "Rank {$rank} ({$perm})"])->toArray())
->helperText(__('commandocentrum.discord_ranks_helper')),
]),
Section::make(__('commandocentrum.social_login'))
->description(__('commandocentrum.social_login_desc'))
->icon('heroicon-o-user-circle')
->schema([
Toggle::make('social_login_google_enabled')
->label(__('commandocentrum.google_login'))
->helperText(__('commandocentrum.google_login_helper')),
TextInput::make('social_login_google_client_id')
->label(__('commandocentrum.google_client_id'))
->helperText(__('commandocentrum.google_client_id_helper')),
TextInput::make('social_login_google_client_secret')
->label(__('commandocentrum.google_client_secret'))
->type('password'),
Toggle::make('social_login_discord_enabled')
->label(__('commandocentrum.discord_login'))
->helperText(__('commandocentrum.discord_login_helper')),
TextInput::make('social_login_discord_client_id')
->label(__('commandocentrum.discord_client_id'))
->helperText(__('commandocentrum.discord_client_id_helper')),
TextInput::make('social_login_discord_client_secret')
->label(__('commandocentrum.discord_client_secret'))
->type('password'),
Toggle::make('social_login_github_enabled')
->label(__('commandocentrum.github_login'))
->helperText(__('commandocentrum.github_login_helper')),
TextInput::make('social_login_github_client_id')
->label(__('commandocentrum.github_client_id'))
->helperText(__('commandocentrum.github_client_id_helper')),
TextInput::make('social_login_github_client_secret')
->label(__('commandocentrum.github_client_secret'))
->type('password'),
])
->columns(2),
Section::make(__('commandocentrum.staff_activity'))
->description(__('commandocentrum.staff_activity_desc'))
->icon('heroicon-o-user-group')
->schema([
Placeholder::make('staff_activity')
->label('')
->content(fn () => $this->renderStaffActivityView()),
]),
]);
}
private function renderServerInfoView(): View
{
$load = sys_getloadavg();
return view('filament.components.commandocentrum.server-info', [
'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',
'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',
]);
}
private function renderHotelStatusView(): View
{
$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);
$clientColor = $clientExists ? '#22c55e' : '#ef4444';
$rendererColor = $rendererExists ? '#22c55e' : '#ef4444';
$clientText = $clientExists ? '✓ ' . substr($clientCommit, 0, 7) : '✗ ' . __('commandocentrum.not_found');
$rendererText = $rendererExists ? '✓ ' . substr($rendererCommit, 0, 7) : '✗ ' . __('commandocentrum.not_found');
$webrootText = $rendererExists ? '✓ ' . basename($nitroWebroot) : '✗ ' . __('commandocentrum.not_found');
$webrootColor = $rendererExists ? '#22c55e' : '#ef4444';
return view('filament.components.commandocentrum.hotel-status', [
'serviceStatus' => $serviceStatus,
'serviceColor' => $serviceColor,
'onlineUsers' => $this->getOnlineUsersCount(),
'emulatorStatus' => $this->getEmulatorStatusText(),
'dbStatus' => $this->isDatabaseOnline() ? __('commandocentrum.online') : __('commandocentrum.offline'),
'dbColor' => $this->isDatabaseOnline() ? '#22c55e' : '#ef4444',
'clientExists' => $clientExists,
'clientColor' => $clientColor,
'clientText' => $clientText,
'rendererExists' => $rendererExists,
'rendererColor' => $rendererColor,
'rendererText' => $rendererText,
'webrootText' => $webrootText,
'webrootColor' => $webrootColor,
]);
}
private function renderEmulatorInfoView(): View
{
$status = $this->getEmulatorStatusText();
return view('filament.components.commandocentrum.emulator-info', [
'version' => $this->getSetting('emulator_version', 'Onbekend'),
'serviceName' => $this->getSetting('emulator_service_name', 'arcturus'),
'status' => $status,
'color' => $status === 'Online' ? '#22c55e' : '#ef4444',
]);
}
private function renderClothingStatusView(): View
{
try {
$count = DB::table('catalog_clothing')->count();
} catch (Exception) {
$count = 0;
}
return view('filament.components.commandocentrum.clothing-status', [
'clothingCount' => $count,
]);
}
private function renderStaffActivityView(): View
{
try {
$activities = StaffActivity::with('user:id,username,look')
->orderByDesc('created_at')
->limit(20)
->get();
} catch (Exception) {
$activities = collect();
}
return view('filament.components.commandocentrum.staff-activity', [
'activities' => $activities,
]);
}
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 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 getRemoteCommit(string $githubUrl, string $branch = 'main'): string
{
$commit = app(GitHubService::class)->getLatestCommit($githubUrl, $branch);
return $commit ?? 'N/A';
}
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 sendHotelAlert(): void
{
$result = app(EmulatorControlAction::class)->sendAlert($this->data['hotel_alert_message'] ?? '');
$this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.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'] ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'], $result['success'] ? 'success' : 'danger');
}
public function stopEmulator(): void
{
$result = app(EmulatorControlAction::class)->stop();
$this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'], $result['success'] ? 'success' : 'danger');
}
public function restartEmulator(): void
{
$result = app(EmulatorControlAction::class)->restart();
$this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.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(__('commandocentrum.success'), __('commandocentrum.emulator_online'), 'success');
} else {
$this->notify(__('commandocentrum.warning'), __('commandocentrum.emulator_unreachable'), 'warning');
}
$this->fillForm();
} catch (Exception $e) {
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
}
}
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(__('commandocentrum.success'), __('commandocentrum.emulator_settings_saved'), 'success');
} catch (Exception $e) {
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
}
}
public function syncClothing(): void
{
try {
$catalogService = app(CatalogService::class);
$result = $catalogService->syncCatalogClothing();
$message = '👔 ' . __('commandocentrum.clothing_items') . ':' . PHP_EOL;
$message .= '• Toegevoegd: ' . $result['inserted'] . PHP_EOL;
$message .= '• Totaal: ' . $result['total'];
$this->notify(__('commandocentrum.success'), $message, 'success');
} catch (Exception $e) {
$this->notify(__('commandocentrum.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(__('commandocentrum.success'), __('commandocentrum.alerts_saved'), 'success');
} catch (Exception $e) {
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
}
}
public function testDiscord(): void
{
try {
$webhookUrl = $this->data['alert_discord_webhook_url'] ?? '';
if (empty($webhookUrl)) {
$this->notify(__('commandocentrum.error'), __('commandocentrum.webhook_empty'), 'danger');
return;
}
Http::post($webhookUrl, ['content' => '✅ Test van Atom CMS Commandocentrum']);
$this->notify(__('commandocentrum.success'), __('commandocentrum.test_sent'), 'success');
} catch (Exception $e) {
$this->notify(__('commandocentrum.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(__('commandocentrum.success'), __('commandocentrum.diagnostics_refreshed'), 'success');
}
private function runDiagnostics(): void
{
$runner = app(DiagnosticRunner::class);
$this->diagnostics = array_map(fn (DiagnosticResult $r) => [
'name' => $r->name,
'status' => $r->status,
'message' => $r->message,
'fix' => $r->fix,
], $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' => __('commandocentrum.critical_issues'),
'warning' => __('commandocentrum.warnings'),
default => __('commandocentrum.healthy'),
};
$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(__('commandocentrum.healthy'), $okCount, '#22c55e', 'heroicon-o-check-circle');
$html .= $this->getSummaryCardHtml(__('commandocentrum.warnings'), $warningCount, '#f59e0b', 'heroicon-o-exclamation-triangle');
$html .= $this->getSummaryCardHtml(__('commandocentrum.errors'), $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;">' . __('commandocentrum.system_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');
}
}