refactor: extract action classes, add Blade components, reduce Commandocentrum

- Create EmulatorControlAction and NitroControlAction classes
- Extract business logic from Commandocentrum controller methods
- Add Blade components for status cards, diagnostics, and summary cards
- Replace shell_exec with file_get_contents in config reading
- Remove duplicate methods and unused code
- Commandocentrum reduced from 2033 to 1780 lines
This commit is contained in:
root
2026-05-19 20:57:31 +02:00
parent 976b990a8a
commit cbe189fd96
6 changed files with 364 additions and 294 deletions
+87
View File
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Actions\Commandocentrum;
use App\Services\EmulatorUpdateService;
use App\Services\RconService;
use App\Services\SettingsService;
use Illuminate\Support\Facades\Process;
class EmulatorControlAction
{
public function __construct(
private readonly SettingsService $settings,
private readonly EmulatorUpdateService $updateService,
) {}
public function start(): array
{
$serviceName = $this->settings->getOrDefault('emulator_service_name', 'arcturus');
$result = Process::timeout(30)->run("systemctl start {$serviceName} 2>&1");
return [
'success' => $result->successful(),
'message' => $result->successful() ? 'Emulator gestart!' : ($result->output() ?: 'Kon emulator niet starten'),
];
}
public function stop(): array
{
$serviceName = $this->settings->getOrDefault('emulator_service_name', 'arcturus');
$result = Process::timeout(30)->run("systemctl stop {$serviceName} 2>&1");
return [
'success' => $result->successful(),
'message' => $result->successful() ? 'Emulator gestopt!' : ($result->output() ?: 'Kon emulator niet stoppen'),
];
}
public function restart(): array
{
$serviceName = $this->settings->getOrDefault('emulator_service_name', 'arcturus');
$result = Process::timeout(60)->run("systemctl restart {$serviceName} 2>&1");
return [
'success' => $result->successful(),
'message' => $result->successful() ? 'Emulator herstart!' : ($result->output() ?: 'Kon emulator niet herstarten'),
];
}
public function sendAlert(string $message): array
{
if (empty($message)) {
return ['success' => false, 'message' => 'Bericht mag niet leeg zijn'];
}
app(RconService::class)->sendCommand('alert', ['message' => $message]);
return ['success' => true, 'message' => 'Alert verstuurd naar alle gebruikers!'];
}
public function build(): array
{
return $this->updateService->buildFromSource();
}
public function update(): array
{
return $this->updateService->updateEmulator();
}
public function runSqlUpdates(): array
{
return $this->updateService->runSqlUpdates();
}
public function getBackups(): array
{
return $this->updateService->getBackupList();
}
public function restoreBackup(string $backupName): array
{
return $this->updateService->restoreBackup($backupName);
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace App\Actions\Commandocentrum;
use App\Services\SettingsService;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Process;
class NitroControlAction
{
public function __construct(
private readonly SettingsService $settings,
) {}
public function pullUpdates(string $clientPath, string $rendererPath, string $branch): array
{
$this->runGitPull($clientPath, $branch);
$this->runGitPull($rendererPath, $branch);
return ['success' => true, 'message' => 'Nitro bijgewerkt van GitHub'];
}
public function build(string $clientPath, string $rendererPath, string $branch): array
{
$this->runGitPull($clientPath, $branch);
$this->runGitPull($rendererPath, $branch);
Process::timeout(120)->run('cd ' . escapeshellarg($clientPath) . ' && sudo -u www-data npm install 2>&1');
Process::timeout(120)->run('cd ' . escapeshellarg($rendererPath) . ' && sudo -u www-data npm install 2>&1');
$exitCode = Artisan::call('build:theme');
return [
'success' => $exitCode === 0,
'message' => $exitCode === 0 ? 'Nitro build succesvol!' : 'Build gestart - controleer handmatig',
];
}
public function generateConfigs(string $siteUrl, string $webroot, string $gamedataPath): array
{
if (! filter_var($siteUrl, FILTER_VALIDATE_URL)) {
return ['success' => false, 'message' => 'Voer een geldige URL in'];
}
$existingConfigs = $this->readExistingConfigs($webroot, $gamedataPath);
$exitCode = Artisan::call('app:generate-nitro-configs', ['--site-url' => $siteUrl]);
if ($existingConfigs !== [] && $exitCode === 0) {
$this->mergeExistingConfigs($webroot, $existingConfigs);
}
$this->settings->set('nitro_last_checked', now()->toIso8601String());
return [
'success' => $exitCode === 0,
'message' => $exitCode === 0 ? 'Configs gegenereerd & bestaande instellingen behouden!' : 'Config gegenereerd (controleer handmatig)',
];
}
private function runGitPull(string $path, string $branch): void
{
Process::timeout(60)->run('cd ' . escapeshellarg($path) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1');
}
private function readExistingConfigs(string $webroot, string $gamedataPath): array
{
$configs = [];
$files = ['renderer-config.json', 'ui-config.json', 'UITexts.json'];
foreach ($files as $file) {
$path = $webroot . '/' . $file;
$content = @file_get_contents($path);
if ($content) {
$decoded = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE) {
$configs[$file] = $decoded;
}
}
}
if ($gamedataPath !== '') {
$gamedataConfigs = [
'ExternalTexts.json' => $gamedataPath . '/config/ExternalTexts.json',
'FurnitureData.json' => $gamedataPath . '/config/FurnitureData.json',
'ProductData.json' => $gamedataPath . '/config/ProductData.json',
'FigureData.json' => $gamedataPath . '/config/FigureData.json',
];
foreach ($gamedataConfigs as $key => $path) {
$content = @file_get_contents($path);
if ($content) {
$decoded = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE) {
$configs['gamedata.' . $key] = $decoded;
}
}
}
}
return $configs;
}
private function mergeExistingConfigs(string $webroot, array $existingConfigs): void
{
if (isset($existingConfigs['renderer-config.json'])) {
$newPath = $webroot . '/renderer-config.json';
$newContent = @file_get_contents($newPath);
$newConfig = json_decode($newContent, true);
if ($newConfig && json_last_error() === JSON_ERROR_NONE) {
$merged = array_merge($existingConfigs['renderer-config.json'], $newConfig);
@file_put_contents($newPath, json_encode($merged, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
}
}
}
+35 -285
View File
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Filament\Pages\Monitoring; namespace App\Filament\Pages\Monitoring;
use App\Actions\Commandocentrum\EmulatorControlAction;
use App\Actions\Commandocentrum\NitroControlAction;
use App\Enums\AlertSeverity; use App\Enums\AlertSeverity;
use App\Models\Miscellaneous\WebsitePermission; use App\Models\Miscellaneous\WebsitePermission;
use App\Models\StaffActivity; use App\Models\StaffActivity;
@@ -1267,64 +1269,29 @@ final class Commandocentrum extends Page implements HasForms
public function sendHotelAlert(): void public function sendHotelAlert(): void
{ {
try { $result = app(EmulatorControlAction::class)->sendAlert($this->data['hotel_alert_message'] ?? '');
$message = $this->data['hotel_alert_message'] ?? ''; $this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger');
if (empty($message)) { if ($result['success']) {
$this->notify('Error', 'Bericht mag niet leeg zijn', 'danger');
return;
}
app(RconService::class)->sendCommand('alert', ['message' => $message]);
$this->notify('Success', 'Alert verstuurd naar alle gebruikers!', 'success');
$this->data['hotel_alert_message'] = ''; $this->data['hotel_alert_message'] = '';
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
} }
} }
public function startEmulator(): void public function startEmulator(): void
{ {
try { $result = app(EmulatorControlAction::class)->start();
$serviceName = $this->getSetting('emulator_service_name', 'arcturus'); $this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger');
$result = Process::timeout(30)->run("sudo systemctl start {$serviceName} 2>&1");
if ($result->successful()) {
$this->notify('Success', 'Emulator gestart!', 'success');
} else {
$this->notify('Error', $result->output() ?: 'Kon emulator niet starten', 'danger');
}
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
} }
public function stopEmulator(): void public function stopEmulator(): void
{ {
try { $result = app(EmulatorControlAction::class)->stop();
$serviceName = $this->getSetting('emulator_service_name', 'arcturus'); $this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger');
$result = Process::timeout(30)->run("sudo systemctl stop {$serviceName} 2>&1");
if ($result->successful()) {
$this->notify('Success', 'Emulator gestopt!', 'success');
} else {
$this->notify('Error', $result->output() ?: 'Kon emulator niet stoppen', 'danger');
}
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
} }
public function restartEmulator(): void public function restartEmulator(): void
{ {
try { $result = app(EmulatorControlAction::class)->restart();
$serviceName = $this->getSetting('emulator_service_name', 'arcturus'); $this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger');
$result = Process::timeout(60)->run("sudo systemctl restart {$serviceName} 2>&1");
if ($result->successful()) {
$this->notify('Success', 'Emulator herstart!', 'success');
} else {
$this->notify('Error', $result->output() ?: 'Kon emulator niet herstarten', 'danger');
}
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
} }
public function checkEmulator(): void public function checkEmulator(): void
@@ -1345,95 +1312,22 @@ final class Commandocentrum extends Page implements HasForms
public function checkEmulatorUpdates(): void public function checkEmulatorUpdates(): void
{ {
try { $result = app(EmulatorControlAction::class)->update();
$this->notify('Info', '🔨 Emulator wordt gebouwd vanaf source...', 'info'); $this->notify($result['success'] ?? false ? 'Success' : 'Error', $result['message'] ?? $result['error'] ?? 'Onbekende fout', ($result['success'] ?? false) ? 'success' : 'danger');
$updateService = app(EmulatorUpdateService::class);
$result = $updateService->buildFromSource();
if ($result['success'] ?? false) {
$this->notify('Success', $result['message'] ?? '✅ Emulator gebouwd!', 'success');
} else {
$this->notify('Error', $result['error'] ?? 'Build mislukt', 'danger');
}
Cache::forget('all_updates_check'); Cache::forget('all_updates_check');
} catch (Exception $e) { $this->fillForm();
$this->notify('Error', $e->getMessage(), 'danger');
}
} }
public function buildEmulator(): void public function buildEmulator(): void
{ {
try { $result = app(EmulatorControlAction::class)->build();
$sourcePath = $this->getSetting('emulator_source_path', '/var/www/emulator-source'); $this->notify($result['success'] ?? false ? 'Success' : 'Error', $result['message'] ?? $result['error'] ?? 'Onbekende fout', ($result['success'] ?? false) ? 'success' : 'danger');
$githubUrl = $this->getSetting('emulator_github_url', '');
$branch = $this->getSetting('emulator_github_branch', 'main');
if ($githubUrl === '' || $githubUrl === '0') {
$this->notify('Error', 'Configureer eerst de Emulator GitHub URL', 'danger');
return;
}
// Pull latest from GitHub
$this->notify('Info', '🔄 Pulling latest changes from GitHub...', 'info');
$pullResult = $this->runCommand('cd ' . escapeshellarg($sourcePath) . ' && git pull origin ' . escapeshellarg($branch) . ' 2>&1', 60);
$mavenCheck = $this->runCommand('which mvn 2>/dev/null');
if (! $mavenCheck || ! trim($mavenCheck)) {
$this->notify('Warning', 'Maven (mvn) is niet geïnstalleerd - kan niet bouwen', 'warning');
return;
}
// Find pom.xml and build
$pomDirs = [$sourcePath, $sourcePath . '/Emulator', $sourcePath . '/Emulator/Emulator'];
foreach ($pomDirs as $pomDir) {
$pomCheck = $this->runCommand('test -f ' . escapeshellarg($pomDir . '/pom.xml') . ' && echo yes');
if ($pomCheck === 'yes') {
$this->notify('Info', '🔨 Building emulator with Maven...', 'info');
$buildResult = $this->runCommand('cd ' . escapeshellarg($pomDir) . ' && mvn clean package -DskipTests 2>&1', 600);
if ($buildResult && str_contains($buildResult, 'BUILD SUCCESS')) {
$jarPath = $this->getSetting('emulator_jar_path', '/var/www/Emulator');
$jarFind = $this->runCommand('find ' . escapeshellarg($pomDir . '/target') . ' -name "*jar-with-dependencies.jar" -type f 2>/dev/null | head -1', 30);
if ($jarFind) {
$sourceJar = trim($jarFind);
$jarName = basename($sourceJar);
$destJar = $jarPath . '/' . $jarName;
$this->runCommand('cp ' . escapeshellarg($sourceJar) . ' ' . escapeshellarg($destJar) . ' 2>&1', 30);
$latestPath = $sourcePath . '/Emulator/Latest_Compiled_Version';
if (is_dir($latestPath)) {
$this->runCommand('cp ' . escapeshellarg($sourceJar) . ' ' . escapeshellarg($latestPath . '/' . $jarName) . ' 2>&1', 30);
}
$this->notify('Success', '✅ Build succesvol! JAR verplaatst naar ' . $jarName . '. Herstart de emulator.', 'success');
} else {
$this->notify('Success', '✅ Build succesvol! Herstart de emulator.', 'success');
}
} else {
$this->notify('Error', 'Build mislukt - controleer logs', 'danger');
}
return;
}
}
$this->notify('Warning', 'Geen pom.xml gevonden - kan niet bouwen vanaf source', 'warning');
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
} }
public function renderBackupsList(): HtmlString public function renderBackupsList(): HtmlString
{ {
try { try {
$service = new EmulatorUpdateService; $backups = app(EmulatorControlAction::class)->getBackups();
$backups = $service->getBackupList();
if ($backups === []) { if ($backups === []) {
return new HtmlString(<<<'HTML' return new HtmlString(<<<'HTML'
@@ -1480,34 +1374,17 @@ final class Commandocentrum extends Page implements HasForms
} }
} }
public function restoreBackup(string $backupName): void
{
try {
$service = new EmulatorUpdateService;
$result = $service->restoreBackup($backupName);
if ($result['success']) {
$this->notify('Success', $result['message'], 'success');
} else {
$this->notify('Error', $result['error'], 'danger');
}
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
}
public function runSqlUpdates(): void public function runSqlUpdates(): void
{ {
try { $result = app(EmulatorControlAction::class)->runSqlUpdates();
$exitCode = Artisan::call('update:auto', ['--force' => true]); $this->notify($result['success'] ?? false ? 'Success' : 'Error', $result['message'] ?? 'Onbekende fout', ($result['success'] ?? false) ? 'success' : 'danger');
if ($exitCode === 0) {
$this->notify('Success', 'SQL updates toegepast!', 'success');
} else {
$this->notify('Warning', 'Update controle voltooid', 'warning');
}
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), '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 public function saveEmulator(): void
@@ -1531,109 +1408,34 @@ final class Commandocentrum extends Page implements HasForms
public function checkNitroUpdates(): void public function checkNitroUpdates(): void
{ {
try {
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client'); $clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer'); $rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
$branch = $this->getSetting('nitro_github_branch', 'main'); $branch = $this->getSetting('nitro_github_branch', 'main');
// Get local commits before pull $result = app(NitroControlAction::class)->pullUpdates($clientPath, $rendererPath, $branch);
$clientCommitBefore = $this->getGitCommit($clientPath); $this->notify('Success', $result['message'], 'success');
$rendererCommitBefore = $this->getGitCommit($rendererPath); $this->fillForm();
// Pull latest from GitHub using sudo
$this->notify('Info', '🔄 Pulling Nitro Client van GitHub...', 'info');
$this->runCommand('cd ' . escapeshellarg($clientPath) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1', 60);
$this->runCommand('cd ' . escapeshellarg($rendererPath) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1', 60);
// Get local commits after pull
$clientCommitAfter = $this->getGitCommit($clientPath);
$rendererCommitAfter = $this->getGitCommit($rendererPath);
// Check if anything was updated
$clientUpdated = $clientCommitBefore !== $clientCommitAfter;
$rendererUpdated = $rendererCommitBefore !== $rendererCommitAfter;
if ($clientUpdated || $rendererUpdated) {
$this->notify('Success', '✅ Nitro bijgewerkt! Build opnieuw met "Build" knop.', 'success');
} else {
$this->notify('Success', '✓ Nitro is al up-to-date!', 'success');
}
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
} }
public function buildNitro(): void public function buildNitro(): void
{ {
try {
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client'); $clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer'); $rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
$branch = $this->getSetting('nitro_github_branch', 'main'); $branch = $this->getSetting('nitro_github_branch', 'main');
// Pull latest from GitHub $result = app(NitroControlAction::class)->build($clientPath, $rendererPath, $branch);
$this->notify('Info', '🔄 Pulling Nitro Client...', 'info'); $this->notify($result['success'] ? 'Success' : 'Warning', $result['message'], $result['success'] ? 'success' : 'warning');
$this->runCommand('cd ' . escapeshellarg($clientPath) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1', 60);
$this->runCommand('cd ' . escapeshellarg($rendererPath) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1', 60);
$this->runCommand('cd ' . escapeshellarg($clientPath) . ' && sudo -u www-data npm install 2>&1', 120);
$this->runCommand('cd ' . escapeshellarg($rendererPath) . ' && sudo -u www-data npm install 2>&1', 120);
// Build
$this->notify('Info', '🔨 Building Nitro...', 'info');
$exitCode = Artisan::call('build:theme');
if ($exitCode === 0) {
$this->notify('Success', '✅ Nitro build succesvol!', 'success');
} else {
$this->notify('Warning', 'Build gestart - controleer handmatig', 'warning');
}
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
} }
public function generateNitroConfigs(): void public function generateNitroConfigs(): void
{ {
try {
$siteUrl = $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl()); $siteUrl = $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl());
$webroot = $this->getSetting('nitro_webroot', '/var/www/Client');
$gamedataPath = $this->getSetting('gamedata_path', '/var/www/Gamedata');
if ($siteUrl === '' || $siteUrl === '0' || ! filter_var($siteUrl, FILTER_VALIDATE_URL)) { $result = app(NitroControlAction::class)->generateConfigs($siteUrl, $webroot, $gamedataPath);
$this->notify('Error', 'Voer een geldige URL in (bijv. https://epicnabbo.nl)', 'danger'); $this->notify($result['success'] ? 'Success' : 'Error', $result['message'], $result['success'] ? 'success' : 'danger');
$this->fillForm();
return;
}
$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']);
$webroot = $paths['nitro_webroot'];
$existingConfigs = $this->readExistingConfigs($webroot, $paths['gamedata_path']);
$exitCode = Artisan::call('app:generate-nitro-configs', [
'--site-url' => $siteUrl,
]);
if ($existingConfigs !== [] && $exitCode === 0) {
$this->mergeExistingConfigs($webroot, $existingConfigs);
}
$settings->set('nitro_last_checked', now()->toIso8601String());
if ($exitCode === 0) {
$this->notify('Success', 'Configs gegenereerd & bestaande instellingen behouden!', 'success');
} else {
$this->notify('Warning', 'Config gegenereerd (controleer handmatig)', 'warning');
}
} catch (Exception $e) {
$this->notify('Error', $e->getMessage(), 'danger');
}
} }
private function renderClothingStatus(): HtmlString private function renderClothingStatus(): HtmlString
@@ -1669,58 +1471,6 @@ final class Commandocentrum extends Page implements HasForms
} }
} }
private function readExistingConfigs(string $webroot, string $gamedataPath = ''): array
{
$configs = [];
$files = ['renderer-config.json', 'ui-config.json', 'UITexts.json'];
foreach ($files as $file) {
$path = $webroot . '/' . $file;
$content = $this->readFile($path);
if ($content) {
$decoded = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE) {
$configs[$file] = $decoded;
}
}
}
if ($gamedataPath !== '' && $gamedataPath !== '0') {
$gamedataConfigs = [
'ExternalTexts.json' => $gamedataPath . '/config/ExternalTexts.json',
'FurnitureData.json' => $gamedataPath . '/config/FurnitureData.json',
'ProductData.json' => $gamedataPath . '/config/ProductData.json',
'FigureData.json' => $gamedataPath . '/config/FigureData.json',
];
foreach ($gamedataConfigs as $key => $path) {
$result = $this->readFile($path);
if ($result) {
$decoded = json_decode($result, true);
if (json_last_error() === JSON_ERROR_NONE) {
$configs['gamedata.' . $key] = $decoded;
}
}
}
}
return $configs;
}
private function mergeExistingConfigs(string $webroot, array $existingConfigs): void
{
if (isset($existingConfigs['renderer-config.json'])) {
$newPath = $webroot . '/renderer-config.json';
$newContent = $this->readFile($newPath);
$newConfig = json_decode($newContent, true);
if ($newConfig && json_last_error() === JSON_ERROR_NONE) {
$merged = array_merge($existingConfigs['renderer-config.json'], $newConfig);
@file_put_contents($newPath, json_encode($merged, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
}
}
public function detectAndSavePaths(): void public function detectAndSavePaths(): void
{ {
try { try {
@@ -0,0 +1,65 @@
@props(['diagnostics'])
@php
$errors = array_filter($diagnostics, fn ($r) => $r->status === 'error');
$warnings = array_filter($diagnostics, fn ($r) => $r->status === 'warning');
$ok = array_filter($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',
};
@endphp
<div style="display:flex;flex-direction:column;gap:16px;">
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">
<x-filament-components::commandocentrum.summary-card label="Gezond" :count="$okCount" color="#22c55e" icon="check" />
<x-filament-components::commandocentrum.summary-card label="Waarschuwingen" :count="$warningCount" color="#f59e0b" icon="warning" />
<x-filament-components::commandocentrum.summary-card label="Fouten" :count="$errorCount" color="#ef4444" icon="error" />
</div>
<div style="background:{{ $overallColor }}15;border:1px solid {{ $overallColor }}30;border-radius:12px;padding:16px;display:flex;align-items:center;gap:12px;">
<div style="width:12px;height:12px;border-radius:50%;background:{{ $overallColor }};"></div>
<span style="font-weight:700;color:{{ $overallColor }};font-size:16px;">Systeem Status: {{ $overallLabel }}</span>
</div>
@if ($errorCount > 0 || $warningCount > 0)
<div style="display:flex;flex-direction:column;gap:8px;">
@foreach ($diagnostics as $result)
@if ($result->status === 'ok')
@continue
@endif
@php
$color = $result->status === 'error' ? '#ef4444' : '#f59e0b';
@endphp
<div style="background:#fff;border:1px solid {{ $color }}30;border-radius:10px;padding:14px 16px;display:flex;align-items:flex-start;gap:12px;">
<div style="flex-shrink:0;margin-top:2px;">
@if ($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>
@else
<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>
@endif
</div>
<div style="flex:1;">
<div style="font-weight:600;color:#1e293b;font-size:14px;">{{ $result->name }}</div>
<div style="color:#64748b;font-size:13px;margin-top:2px;">{{ $result->message }}</div>
@if ($result->fix)
<div style="background:#f8fafc;border-radius:6px;padding:8px 12px;margin-top:8px;font-size:12px;color:#475569;font-family:monospace;">💡 {{ $result->fix }}</div>
@endif
</div>
</div>
@endforeach
</div>
@endif
</div>
@@ -0,0 +1,30 @@
@props(['label', 'value', 'color', 'icon'])
<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">
@if ($icon === '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" />
@elseif ($icon === '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" />
@elseif ($icon === 'database')
<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" />
@elseif ($icon === 'cpu')
<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" />
@endif
</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>
@@ -0,0 +1,19 @@
@props(['label', 'count', 'color', 'icon'])
<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">
@if ($icon === 'check')
<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" />
@elseif ($icon === 'warning')
<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" />
@elseif ($icon === 'error')
<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" />
@endif
</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>