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
+124
View File
@@ -0,0 +1,124 @@
<?php
namespace App\Services\Diagnostics;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class DatabaseDiagnostic
{
/**
* @return array<DiagnosticResult>
*/
public function runAll(): array
{
return [
$this->checkConnection(),
$this->checkMigrations(),
$this->checkRequiredTables(),
$this->checkRadioTables(),
];
}
public function checkConnection(): DiagnosticResult
{
try {
DB::connection()->getPdo();
return DiagnosticResult::ok('Database Connection', 'Connected to ' . DB::connection()->getDatabaseName());
} catch (\Exception $e) {
return DiagnosticResult::error('Database Connection', $e->getMessage(), 'Check DB credentials in .env');
}
}
public function checkMigrations(): DiagnosticResult
{
try {
$pending = DB::table('migrations')->exists()
? count($this->getPendingMigrations())
: 'unknown';
if ($pending === 'unknown') {
return DiagnosticResult::warning('Migrations', 'Migrations table not found');
}
if ($pending > 0) {
return DiagnosticResult::warning('Migrations', "{$pending} pending migrations", 'Run: php artisan migrate');
}
return DiagnosticResult::ok('Migrations', 'All migrations up to date');
} catch (\Exception $e) {
return DiagnosticResult::error('Migrations', $e->getMessage());
}
}
public function checkRequiredTables(): DiagnosticResult
{
$requiredTables = [
'users', 'permissions', 'website_settings', 'website_articles',
'website_shop_articles', 'website_shop_categories',
];
$missing = [];
foreach ($requiredTables as $table) {
if (! Schema::hasTable($table)) {
$missing[] = $table;
}
}
if ($missing !== []) {
return DiagnosticResult::error(
'Required Tables',
'Missing: ' . implode(', ', $missing),
'Run: php artisan migrate'
);
}
return DiagnosticResult::ok('Required Tables', 'All required tables exist');
}
public function checkRadioTables(): DiagnosticResult
{
$radioTables = [
'radio_ranks', 'radio_banners', 'radio_schedules',
'radio_shouts', 'radio_history',
];
$missing = [];
foreach ($radioTables as $table) {
if (! Schema::hasTable($table)) {
$missing[] = $table;
}
}
if ($missing !== []) {
return DiagnosticResult::warning(
'Radio Tables',
'Missing: ' . implode(', ', $missing),
'Run radio migration seeder'
);
}
return DiagnosticResult::ok('Radio Tables', 'All radio tables exist');
}
/**
* @return array<string>
*/
private function getPendingMigrations(): array
{
$migrated = DB::table('migrations')->pluck('migration')->toArray();
$allMigrations = [];
$migrationPath = database_path('migrations');
if (is_dir($migrationPath)) {
foreach (scandir($migrationPath) as $file) {
if (str_ends_with($file, '.php')) {
$allMigrations[] = pathinfo($file, PATHINFO_FILENAME);
}
}
}
return array_diff($allMigrations, $migrated);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Services\Diagnostics;
interface DiagnosticCheck
{
public function name(): string;
public function run(): DiagnosticResult;
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Services\Diagnostics;
class DiagnosticResult
{
public function __construct(
public readonly string $name,
public readonly string $status = 'ok',
public readonly string $message = '',
public readonly ?string $fix = null,
) {}
public static function ok(string $name, string $message = ''): self
{
return new self($name, 'ok', $message);
}
public static function warning(string $name, string $message, ?string $fix = null): self
{
return new self($name, 'warning', $message, $fix);
}
public static function error(string $name, string $message, ?string $fix = null): self
{
return new self($name, 'error', $message, $fix);
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace App\Services\Diagnostics;
class DiagnosticRunner
{
/**
* @var array<DiagnosticResult>
*/
private array $results = [];
/**
* @return array<DiagnosticResult>
*/
public function runAll(): array
{
$this->results = [];
$diagnostics = [
new DatabaseDiagnostic(),
new SecurityDiagnostic(),
new SystemDiagnostic(),
new HttpDiagnostic(),
];
foreach ($diagnostics as $diagnostic) {
$this->results = array_merge($this->results, $diagnostic->runAll());
}
return $this->results;
}
public function hasErrors(): bool
{
return array_any($this->results, fn ($r) => $r->status === 'error');
}
public function hasWarnings(): bool
{
return array_any($this->results, fn ($r) => $r->status === 'warning');
}
public function getErrors(): array
{
return array_filter($this->results, fn ($r) => $r->status === 'error');
}
public function getWarnings(): array
{
return array_filter($this->results, fn ($r) => $r->status === 'warning');
}
public function getOk(): array
{
return array_filter($this->results, fn ($r) => $r->status === 'ok');
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
namespace App\Services\Diagnostics;
use Illuminate\Support\Facades\Http;
class HttpDiagnostic
{
/**
* @return array<DiagnosticResult>
*/
public function runAll(): array
{
return [
$this->checkAppUrl(),
$this->checkSslCertificate(),
$this->checkHttpRedirect(),
];
}
public function checkAppUrl(): DiagnosticResult
{
$appUrl = config('app.url');
if (empty($appUrl) || $appUrl === 'http://localhost') {
return DiagnosticResult::warning(
'App URL',
'APP_URL not configured properly',
'Set APP_URL in .env to your domain'
);
}
if (! str_starts_with($appUrl, 'https://') && app()->environment('production')) {
return DiagnosticResult::warning(
'App URL',
'Not using HTTPS in production',
'Configure SSL and update APP_URL'
);
}
return DiagnosticResult::ok('App URL', $appUrl);
}
public function checkSslCertificate(): DiagnosticResult
{
$appUrl = config('app.url');
if (! str_starts_with($appUrl, 'https://')) {
return DiagnosticResult::warning('SSL', 'Not using HTTPS');
}
$host = parse_url($appUrl, PHP_URL_HOST);
if (! $host) {
return DiagnosticResult::warning('SSL', 'Could not parse host from APP_URL');
}
try {
$response = Http::timeout(5)->get($appUrl);
if ($response->successful()) {
return DiagnosticResult::ok('SSL', 'Certificate valid');
}
return DiagnosticResult::warning('SSL', 'HTTPS endpoint returned ' . $response->status());
} catch (\Exception $e) {
return DiagnosticResult::error('SSL', $e->getMessage(), 'Check SSL certificate configuration');
}
}
public function checkHttpRedirect(): DiagnosticResult
{
$appUrl = config('app.url');
if (! str_starts_with($appUrl, 'https://')) {
return DiagnosticResult::ok('HTTP Redirect', 'Not applicable (no HTTPS)');
}
$httpUrl = str_replace('https://', 'http://', $appUrl);
try {
$response = Http::timeout(5)->withoutRedirecting()->get($httpUrl);
if (in_array($response->status(), [301, 302])) {
return DiagnosticResult::ok('HTTP Redirect', 'HTTP redirects to HTTPS');
}
return DiagnosticResult::warning(
'HTTP Redirect',
"HTTP returns {$response->status()} instead of redirect",
'Configure web server to redirect HTTP to HTTPS'
);
} catch (\Exception $e) {
return DiagnosticResult::warning('HTTP Redirect', 'Could not test: ' . $e->getMessage());
}
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace App\Services\Diagnostics;
use Illuminate\Support\Facades\File;
class SecurityDiagnostic
{
/**
* @return array<DiagnosticResult>
*/
public function runAll(): array
{
return [
$this->checkAppKey(),
$this->checkDebugMode(),
$this->checkEnvFile(),
$this->checkFilePermissions(),
];
}
public function checkAppKey(): DiagnosticResult
{
$key = config('app.key');
if (empty($key)) {
return DiagnosticResult::error('App Key', 'No application key set', 'Run: php artisan key:generate');
}
return DiagnosticResult::ok('App Key', 'Application key is set');
}
public function checkDebugMode(): DiagnosticResult
{
if (config('app.debug') && app()->environment('production')) {
return DiagnosticResult::error(
'Debug Mode',
'Debug mode is enabled in production',
'Set APP_DEBUG=false in .env'
);
}
return DiagnosticResult::ok('Debug Mode', 'Debug mode is ' . (config('app.debug') ? 'enabled (dev)' : 'disabled'));
}
public function checkEnvFile(): DiagnosticResult
{
$envPath = base_path('.env');
if (! File::exists($envPath)) {
return DiagnosticResult::error('.env File', '.env file not found', 'Copy .env.example to .env');
}
$content = File::get($envPath);
if (! str_contains($content, "\n") && strlen($content) > 500) {
return DiagnosticResult::warning(
'.env File',
'File appears to be on a single line',
'Ensure .env has proper line breaks'
);
}
return DiagnosticResult::ok('.env File', 'File exists and is properly formatted');
}
public function checkFilePermissions(): DiagnosticResult
{
$directories = [
storage_path(),
bootstrap_path('cache'),
];
$issues = [];
foreach ($directories as $dir) {
if (! is_writable($dir)) {
$issues[] = $dir;
}
}
if ($issues !== []) {
return DiagnosticResult::error(
'File Permissions',
'Not writable: ' . implode(', ', $issues),
'Run: chmod -R 775 storage bootstrap/cache'
);
}
return DiagnosticResult::ok('File Permissions', 'All directories are writable');
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
namespace App\Services\Diagnostics;
use Illuminate\Support\Facades\Redis;
class SystemDiagnostic
{
/**
* @return array<DiagnosticResult>
*/
public function runAll(): array
{
return [
$this->checkPhpExtensions(),
$this->checkPhpVersion(),
$this->checkCache(),
$this->checkSession(),
$this->checkRedis(),
];
}
public function checkPhpExtensions(): DiagnosticResult
{
$required = ['pdo_mysql', 'curl', 'json', 'mbstring', 'xml', 'zip', 'bcmath'];
$missing = array_filter($required, fn ($ext) => ! extension_loaded($ext));
if ($missing !== []) {
return DiagnosticResult::error(
'PHP Extensions',
'Missing: ' . implode(', ', $missing),
'Install: sudo apt install php-' . implode(' php-', $missing)
);
}
return DiagnosticResult::ok('PHP Extensions', 'All required extensions loaded');
}
public function checkPhpVersion(): DiagnosticResult
{
$current = PHP_VERSION_ID;
$min = 80100; // PHP 8.1
if ($current < $min) {
return DiagnosticResult::error(
'PHP Version',
'Current: ' . PHP_VERSION . ' (minimum: 8.1)',
'Upgrade PHP to 8.1 or higher'
);
}
return DiagnosticResult::ok('PHP Version', PHP_VERSION);
}
public function checkCache(): DiagnosticResult
{
try {
Cache::put('diagnostic_test', 'ok', 10);
$value = Cache::get('diagnostic_test');
if ($value === 'ok') {
return DiagnosticResult::ok('Cache', 'Working (' . config('cache.default') . ')');
}
return DiagnosticResult::warning('Cache', 'Cache returned unexpected value');
} catch (\Exception $e) {
return DiagnosticResult::error('Cache', $e->getMessage());
}
}
public function checkSession(): DiagnosticResult
{
$driver = config('session.driver');
if ($driver === 'file' && app()->environment('production')) {
return DiagnosticResult::warning(
'Session',
'Using file sessions in production',
'Consider using redis or database sessions'
);
}
return DiagnosticResult::ok('Session', "Driver: {$driver}");
}
public function checkRedis(): DiagnosticResult
{
if (config('redis.default.host') === '127.0.0.1') {
try {
Redis::ping();
return DiagnosticResult::ok('Redis', 'Connected');
} catch (\Exception $e) {
return DiagnosticResult::warning('Redis', 'Not reachable: ' . $e->getMessage());
}
}
return DiagnosticResult::ok('Redis', 'Not configured (optional)');
}
}