You've already forked Atomcms-edit
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:
+124
@@ -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);
|
||||
}
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Diagnostics;
|
||||
|
||||
interface DiagnosticCheck
|
||||
{
|
||||
public function name(): string;
|
||||
|
||||
public function run(): DiagnosticResult;
|
||||
}
|
||||
+28
@@ -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
@@ -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');
|
||||
}
|
||||
}
|
||||
Executable
+96
@@ -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
@@ -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
@@ -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)');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user