refactor: integrate diagnostics into Commandocentrum and split EmulatorUpdateService

- Add DiagnosticRunner integration to Commandocentrum for system health display
- Refactor EmulatorUpdateService from 2524 lines to 395 lines (facade pattern)
- Extract EmulatorStatusService, EmulatorJarService, EmulatorSourceService
- Extract EmulatorBuildService, EmulatorSqlService, EmulatorBackupService
- Add shared EmulatorConfiguration trait for dependency injection
- Preserve backward compatibility on all public methods
This commit is contained in:
root
2026-05-19 20:20:43 +02:00
parent b1739cabbf
commit f5666c104d
17 changed files with 2743 additions and 7197 deletions
@@ -9,6 +9,7 @@ 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\RconService;
use App\Services\SettingsService;
@@ -61,9 +62,13 @@ final class Commandocentrum extends Page implements HasForms
public string $catalogSyncUrl = '';
/** @var array<\App\Services\Diagnostics\DiagnosticResult> */
public array $diagnostics = [];
public function mount(): void
{
$this->fillForm();
$this->runDiagnostics();
}
protected function fillForm(): void
@@ -147,6 +152,22 @@ final class Commandocentrum extends Page implements HasForms
->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')
@@ -1941,4 +1962,111 @@ final class Commandocentrum extends Page implements HasForms
->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;
}
}