You've already forked Atomcms-edit
Initial commit
This commit is contained in:
Executable
+156
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class AtomSetupCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'atom:setup {--auto=false}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Takes you through a basic setup, allowing you to define general settings';
|
||||
|
||||
private function progressInfo(int $step): void
|
||||
{
|
||||
$this->info(sprintf('Step %s/13', $step));
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
Artisan::call('db:seed --class=WebsiteSettingsSeeder');
|
||||
|
||||
if ($this->option('auto') === 'false') {
|
||||
$step = 1;
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$hotelName = $this->ask('Enter your hotel name');
|
||||
WebsiteSetting::where('key', '=', 'hotel_name')->update([
|
||||
'value' => empty($hotelName) ? 'Hotel' : $hotelName,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$colorMode = $this->choice('Enter your preferred CMS color mode', ['light', 'dark'], 0);
|
||||
WebsiteSetting::where('key', '=', 'cms_color_mode')->update([
|
||||
'value' => $colorMode,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$startCredits = $this->ask('Enter the amount of credits new users should start with: (default is 5000)');
|
||||
WebsiteSetting::where('key', '=', 'start_credits')->update([
|
||||
'value' => empty($startCredits) ? '5000' : $startCredits,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$startDuckets = $this->ask('Enter the amount of credits new users should start with: (default is 5000)');
|
||||
WebsiteSetting::where('key', '=', 'start_duckets')->update([
|
||||
'value' => empty($startDuckets) ? '5000' : $startDuckets,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$startDiamonds = $this->ask('Enter the amount of diamonds new users should start with: (default is 100)');
|
||||
WebsiteSetting::where('key', '=', 'start_diamonds')->update([
|
||||
'value' => empty($startDiamonds) ? '100' : $startDiamonds,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$startPoints = $this->ask('Enter the amount of points new users should start with (default is 0)');
|
||||
WebsiteSetting::where('key', '=', 'start_points')->update([
|
||||
'value' => empty($startPoints) ? '0' : $startPoints,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$maxAccountsPerIP = $this->ask('Enter the amount of accounts a user can register per IP address (default is 2)');
|
||||
WebsiteSetting::where('key', '=', 'max_accounts_per_ip')->update([
|
||||
'value' => empty($maxAccountsPerIP) ? '2' : $maxAccountsPerIP,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$recaptchaEnabled = $this->choice('Google ReCaptcha enabled: (Do not forget to add your keys to your .env file in-case you set this to 1)', ['0', '1'], 0);
|
||||
WebsiteSetting::where('key', '=', 'google_recaptcha_enabled')->update([
|
||||
'value' => $recaptchaEnabled,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$wordfilterEnabled = $this->choice('CMS wordfilter enabled', ['0', '1'], 1);
|
||||
WebsiteSetting::where('key', '=', 'website_wordfilter_enabled')->update([
|
||||
'value' => $wordfilterEnabled,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$requiredBetaCode = $this->choice('Requires beta code to register', ['0', '1'], 0);
|
||||
WebsiteSetting::where('key', '=', 'requires_beta_code')->update([
|
||||
'value' => $requiredBetaCode,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$registrationDisabled = $this->choice('Disable registration (Can be re-enabled later inside website_settings table if set to 1)', ['0', '1'], 0);
|
||||
WebsiteSetting::where('key', '=', 'disable_registration')->update([
|
||||
'value' => $registrationDisabled,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
$step++;
|
||||
|
||||
$giveHC = $this->choice('Give all new users HC automatically', ['0', '1'], 0);
|
||||
WebsiteSetting::where('key', '=', 'give_hc_on_register')->update([
|
||||
'value' => $giveHC,
|
||||
]);
|
||||
|
||||
$this->progressInfo($step);
|
||||
|
||||
$maxCommentArticles = $this->ask('Enter the amount of comments each user can post per article (default is 2)');
|
||||
WebsiteSetting::where('key', '=', 'max_comment_per_article')->update([
|
||||
'value' => empty($maxCommentArticles) ? '2' : $maxCommentArticles,
|
||||
]);
|
||||
}
|
||||
|
||||
$seeders = [
|
||||
'WebsiteLanguageSeeder',
|
||||
'WebsiteArticleSeeder',
|
||||
'WebsitePermissionSeeder',
|
||||
'WebsiteWordfilterSeeder',
|
||||
'WebsiteTeamSeeder',
|
||||
'WebsiteRuleCategorySeeder',
|
||||
'WebsiteRuleSeeder',
|
||||
];
|
||||
|
||||
foreach ($seeders as $seeder) {
|
||||
Artisan::call(sprintf('db:seed --class=%s', $seeder));
|
||||
}
|
||||
|
||||
$this->info('The setup was successful!');
|
||||
$this->newLine();
|
||||
|
||||
// Run system check to verify everything is working
|
||||
$this->info('🔍 Running system check to verify installation...');
|
||||
$this->newLine();
|
||||
$this->call('atom:check');
|
||||
}
|
||||
}
|
||||
Executable
+150
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\AlertChannel;
|
||||
use App\Enums\AlertType;
|
||||
use App\Services\AlertService;
|
||||
use App\Services\EmulatorUpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AutoUpdateCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'update:auto
|
||||
{--force : Forceer update ook al is het niet de geplande tijd}
|
||||
{--sql-only : Alleen SQL updates draaien}
|
||||
{--emu-only : Alleen emulator updates draaien}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Automatische emulator en SQL updates uitvoeren';
|
||||
|
||||
private const string CACHE_KEY_LAST_EMU_UPDATE = 'auto_update_last_emu';
|
||||
|
||||
private const string CACHE_KEY_LAST_SQL_UPDATE = 'auto_update_last_sql';
|
||||
|
||||
public function handle(AlertService $alertService, EmulatorUpdateService $updateService): int
|
||||
{
|
||||
$this->info('Automatische update check...');
|
||||
|
||||
$isForced = $this->option('force');
|
||||
$sqlOnly = $this->option('sql-only');
|
||||
$emuOnly = $this->option('emu-only');
|
||||
|
||||
if (! $isForced && ! $this->isScheduledTime()) {
|
||||
$this->line('Niet de geplande tijd, overslaan.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $sqlOnly) {
|
||||
$this->runEmulatorUpdate($alertService, $updateService);
|
||||
}
|
||||
|
||||
if (! $emuOnly) {
|
||||
$this->runSqlUpdates($alertService, $updateService);
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function isScheduledTime(): bool
|
||||
{
|
||||
$enabled = setting('auto_update_enabled', true);
|
||||
|
||||
if (! $enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$scheduleTime = setting('auto_update_schedule', '03:00');
|
||||
$scheduleDays = setting('auto_update_days', '0,1,2,3,4,5,6');
|
||||
|
||||
$now = now();
|
||||
$currentTime = $now->format('H:i');
|
||||
$currentDay = (int) $now->dayOfWeek;
|
||||
|
||||
$allowedDays = array_map(intval(...), explode(',', $scheduleDays));
|
||||
|
||||
if (! in_array($currentDay, $allowedDays)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($currentTime !== $scheduleTime) {
|
||||
$minuteDiff = abs(strtotime($currentTime) - strtotime((string) $scheduleTime)) / 60;
|
||||
if ($minuteDiff > 5) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function runEmulatorUpdate(AlertService $alertService, EmulatorUpdateService $updateService): void
|
||||
{
|
||||
$check = $updateService->checkForUpdates();
|
||||
|
||||
if (! ($check['update_available'] ?? false)) {
|
||||
$this->line('Emulator is al up-to-date.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->warn('Nieuwe emulator versie beschikbaar: ' . ($check['latest_version'] ?? 'onbekend'));
|
||||
$this->info('Emulator updaten...');
|
||||
|
||||
$result = $updateService->updateEmulator();
|
||||
|
||||
if ($result['success']) {
|
||||
$this->info('Emulator succesvol geüpdatet!');
|
||||
$alertService->sendEmulatorUpdate($result['version'] ?? 'onbekend', $result['message'] ?? '');
|
||||
Log::info('[AutoUpdate] Emulator updated to v' . ($result['version'] ?? 'unknown'));
|
||||
} else {
|
||||
$this->error('Emulator update mislukt: ' . ($result['error'] ?? 'onbekende fout'));
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_ERROR,
|
||||
'Emulator Auto-Update Mislukt: ' . ($result['error'] ?? 'Onbekende fout'),
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY_LAST_EMU_UPDATE, now()->toIso8601String());
|
||||
}
|
||||
|
||||
private function runSqlUpdates(AlertService $alertService, EmulatorUpdateService $updateService): void
|
||||
{
|
||||
$check = $updateService->checkForSqlUpdates();
|
||||
|
||||
if (! ($check['has_updates'] ?? false)) {
|
||||
$this->line('Geen SQL updates beschikbaar.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->warn($check['message']);
|
||||
$this->info('SQL updates uitvoeren...');
|
||||
|
||||
$result = $updateService->runSqlUpdates();
|
||||
|
||||
if ($result['success'] && ! empty($result['files_run'])) {
|
||||
$count = count($result['files_run']);
|
||||
$this->info("{$count} SQL updates succesvol uitgevoerd!");
|
||||
$alertService->sendSqlUpdate($count, $result['message'] ?? '');
|
||||
Log::info('[AutoUpdate] SQL updates completed', ['files' => $result['files_run']]);
|
||||
} elseif (! empty($result['errors'])) {
|
||||
$this->warn('SQL updates met fouten: ' . implode(', ', $result['errors']));
|
||||
$alertService->send(
|
||||
AlertType::CRITICAL_ERROR,
|
||||
'SQL Updates Met Fouten: ' . implode(', ', $result['errors']),
|
||||
['files' => $result['errors']],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY_LAST_SQL_UPDATE, now()->toIso8601String());
|
||||
}
|
||||
}
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class BuildTheme extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'build:theme';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Build a selected theme assets';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$themes = $this->getAvailableThemes();
|
||||
|
||||
if ($themes->isEmpty()) {
|
||||
$this->error('No themes found in resources/themes/');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$selectedTheme = $this->choice(
|
||||
'Which theme would you like to build?',
|
||||
$themes->all(),
|
||||
0,
|
||||
);
|
||||
|
||||
$themeName = is_array($selectedTheme) ? ($selectedTheme[0] ?? '') : $selectedTheme;
|
||||
$this->info('Building ' . $themeName . ' theme...');
|
||||
|
||||
$this->runBuildCommand($themeName);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, string>
|
||||
*/
|
||||
private function getAvailableThemes(): Collection
|
||||
{
|
||||
$themesPath = resource_path('themes');
|
||||
|
||||
if (! File::exists($themesPath)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return collect(File::directories($themesPath))
|
||||
->map(fn ($path) => basename((string) $path))
|
||||
->sort();
|
||||
}
|
||||
|
||||
private function runBuildCommand(string $theme): void
|
||||
{
|
||||
$command = escapeshellcmd("npm run build:{$theme}");
|
||||
$output = [];
|
||||
$returnCode = 0;
|
||||
|
||||
exec($command, $output, $returnCode);
|
||||
|
||||
foreach ($output as $line) {
|
||||
$this->line($line);
|
||||
}
|
||||
|
||||
if ($returnCode === 0) {
|
||||
$this->info("Theme {$theme} built successfully!");
|
||||
} else {
|
||||
$this->error("Failed to build theme {$theme}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+161
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
#[AsCommand(name: 'radio:check-dj')]
|
||||
final class CheckDjConnection extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'radio:check-dj {--force : Force check even if auto-detection is disabled}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Check if a DJ is connected via Sambroadcaster or Virtual DJ';
|
||||
|
||||
private const string CACHE_KEY = 'website_settings';
|
||||
|
||||
private const string DJ_CHECK_RATE_LIMIT = 'dj-check';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$autoDetection = $this->getSetting('radio_auto_dj_detection', '0');
|
||||
|
||||
if ($autoDetection !== '1' && ! $this->option('force')) {
|
||||
$this->info('Auto DJ detectie is uitgeschakeld. Gebruik --force om toch te controleren.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if (RateLimiter::tooManyAttempts(self::DJ_CHECK_RATE_LIMIT, 10)) {
|
||||
$this->info('Te veel pogingen. Wacht even.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
RateLimiter::hit(self::DJ_CHECK_RATE_LIMIT, 60);
|
||||
|
||||
$sambroadcasterUrl = $this->getSetting('radio_sambroadcaster_api_url', '');
|
||||
$virtualDjUrl = $this->getSetting('radio_virtual_dj_url', '');
|
||||
|
||||
$detectedDj = $this->checkSambroadcaster($sambroadcasterUrl);
|
||||
|
||||
if ($detectedDj === null) {
|
||||
$detectedDj = $this->checkVirtualDj($virtualDjUrl);
|
||||
}
|
||||
|
||||
if ($detectedDj === null) {
|
||||
$this->info('Geen DJ verbonden.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->processDetectedDj($detectedDj);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function getSetting(string $key, string $default = ''): string
|
||||
{
|
||||
/** @var WebsiteSetting|null $setting */
|
||||
$setting = WebsiteSetting::where('key', $key)->first();
|
||||
|
||||
return $setting !== null && isset($setting->value) ? (string) $setting->value : $default;
|
||||
}
|
||||
|
||||
private function checkSambroadcaster(string $url): ?string
|
||||
{
|
||||
if ($url === '' || $url === '0') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(5)
|
||||
->withQueryParameters(['format' => 'json'])
|
||||
->get($url . '/dj');
|
||||
|
||||
if ($response->successful()) {
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = $response->json();
|
||||
|
||||
$djName = $data['dj_name'] ?? $data['dj'] ?? $data['current_dj'] ?? $data['name'] ?? null;
|
||||
|
||||
return is_string($djName) ? $djName : null;
|
||||
}
|
||||
} catch (ConnectionException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function checkVirtualDj(string $url): ?string
|
||||
{
|
||||
if ($url === '' || $url === '0') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$endpoints = ['/info', '/status', '/api/dj', '/api/info'];
|
||||
|
||||
foreach ($endpoints as $endpoint) {
|
||||
try {
|
||||
$response = Http::timeout(5)
|
||||
->withQueryParameters(['format' => 'json'])
|
||||
->get($url . $endpoint);
|
||||
|
||||
if ($response->successful()) {
|
||||
/** @var array<string, mixed> $data */
|
||||
$data = $response->json();
|
||||
$djName = $data['djname'] ?? $data['DJName'] ?? $data['current_dj'] ?? $data['dj'] ?? $data['name'] ?? null;
|
||||
|
||||
if ($djName !== null && is_string($djName) && ! in_array(strtolower($djName), ['guest', ''])) {
|
||||
return $djName;
|
||||
}
|
||||
}
|
||||
} catch (ConnectionException) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function processDetectedDj(string $detectedDj): void
|
||||
{
|
||||
/** @var User|null $djUser */
|
||||
$djUser = User::where('username', $detectedDj)->first();
|
||||
|
||||
if ($djUser === null) {
|
||||
$this->warn("DJ {$detectedDj} gedetecteerd maar heeft geen account.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$currentDjId = $this->getSetting('radio_current_dj_id', '');
|
||||
|
||||
if ($currentDjId === (string) $djUser->id) {
|
||||
$this->info("DJ is al actief: {$detectedDj}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
WebsiteSetting::updateOrCreate(
|
||||
['key' => 'radio_current_dj_id'],
|
||||
['value' => (string) $djUser->id],
|
||||
);
|
||||
|
||||
Cache::forget(self::CACHE_KEY);
|
||||
|
||||
$this->info("DJ gedetecteerd: {$detectedDj}");
|
||||
$this->info('DJ ingesteld als actieve presentator.');
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Miscellaneous\WebsiteSetting;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CheckScheduledMaintenance extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'maintenance:check-scheduled';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Check and activate scheduled maintenance mode';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$scheduledAt = setting('maintenance_scheduled_at');
|
||||
$durationSetting = setting('maintenance_duration_minutes');
|
||||
$duration = (int) (is_string($durationSetting) ? $durationSetting : '30');
|
||||
$isEnabledSetting = setting('maintenance_enabled');
|
||||
$isEnabled = $isEnabledSetting === '1';
|
||||
|
||||
if (! $scheduledAt) {
|
||||
$this->info('No maintenance scheduled');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$scheduledAtString = is_scalar($scheduledAt) ? (string) $scheduledAt : '';
|
||||
$startTime = Carbon::parse($scheduledAtString);
|
||||
$endTime = $startTime->copy()->addMinutes($duration);
|
||||
$now = Carbon::now();
|
||||
|
||||
if ($now->between($startTime, $endTime) && ! $isEnabled) {
|
||||
WebsiteSetting::updateOrCreate(
|
||||
['key' => 'maintenance_enabled'],
|
||||
['value' => '1'],
|
||||
);
|
||||
|
||||
$this->info("Maintenance mode enabled (scheduled for {$startTime->format('Y-m-d H:i')})");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if ($now->greaterThan($endTime) && $isEnabled) {
|
||||
WebsiteSetting::where('key', 'maintenance_enabled')->delete();
|
||||
|
||||
$this->info("Maintenance mode disabled (schedule ended at {$endTime->format('Y-m-d H:i')})");
|
||||
|
||||
WebsiteSetting::where('key', 'maintenance_scheduled_at')->delete();
|
||||
WebsiteSetting::where('key', 'maintenance_duration_minutes')->delete();
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info('No action needed');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\AlertService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DDoSDetectionCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'monitor:ddos
|
||||
{--threshold=100 : Minimum requests per IP om als verdacht te markeren}
|
||||
{--time-window=60 : Tijd window in seconden}
|
||||
{--block : Automatisch IPs blokkeren na detectie}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Detecteer mogelijke DDoS aanvallen en stuur alerts';
|
||||
|
||||
private const string CACHE_KEY_DDOS_TRACKING = 'ddos_tracking';
|
||||
|
||||
private const string CACHE_KEY_BLOCKED_IPS = 'ddos_blocked_ips';
|
||||
|
||||
private const int TRACKING_DURATION_SECONDS = 300;
|
||||
|
||||
public function handle(AlertService $alertService): int
|
||||
{
|
||||
$threshold = (int) $this->option('threshold');
|
||||
$timeWindow = (int) $this->option('time-window');
|
||||
$autoBlock = (bool) $this->option('block');
|
||||
|
||||
$this->info("DDoS detectie gestart (threshold: {$threshold} req/IP in {$timeWindow}s)");
|
||||
|
||||
$ddosData = $this->getDDoSData();
|
||||
$suspiciousIps = $this->analyzeTraffic($ddosData, $threshold, $timeWindow);
|
||||
|
||||
if ($suspiciousIps === []) {
|
||||
$this->line('Geen verdachte activiteit gedetecteerd.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->handleSuspiciousIps($suspiciousIps, $alertService, $autoBlock);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function getDDoSData(): array
|
||||
{
|
||||
$data = Cache::get(self::CACHE_KEY_DDOS_TRACKING, []);
|
||||
|
||||
if (empty($data)) {
|
||||
$data = $this->fetchApacheTrafficData();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function fetchApacheTrafficData(): array
|
||||
{
|
||||
$this->warn('Gebruik handmatige IP tracking (geen server logs toegang).');
|
||||
|
||||
return $this->getManualTrackingData();
|
||||
}
|
||||
|
||||
private function tailFile(string $path, int $lines = 100): array
|
||||
{
|
||||
if (! file_exists($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$file = new \SplFileObject($path, 'r');
|
||||
$file->seek(PHP_INT_MAX);
|
||||
$totalLines = $file->key() + 1;
|
||||
|
||||
$startLine = max(0, $totalLines - $lines);
|
||||
$result = [];
|
||||
|
||||
$file->seek($startLine);
|
||||
while (! $file->eof()) {
|
||||
$result[] = rtrim($file->current(), "\r\n");
|
||||
$file->next();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getManualTrackingData(): array
|
||||
{
|
||||
$tracking = Cache::get('manual_ip_tracking', []);
|
||||
|
||||
foreach ($tracking as $ip => $data) {
|
||||
$tracking[$ip]['requests'] = array_filter(
|
||||
$data['requests'],
|
||||
fn ($timestamp) => $timestamp > time() - self::TRACKING_DURATION_SECONDS,
|
||||
);
|
||||
$tracking[$ip]['count'] = count($tracking[$ip]['requests']);
|
||||
|
||||
if ($tracking[$ip]['count'] === 0) {
|
||||
unset($tracking[$ip]);
|
||||
}
|
||||
}
|
||||
|
||||
Cache::put('manual_ip_tracking', $tracking, self::TRACKING_DURATION_SECONDS);
|
||||
|
||||
return $tracking;
|
||||
}
|
||||
|
||||
private function analyzeTraffic(array $data, int $threshold, int $timeWindow): array
|
||||
{
|
||||
$suspicious = [];
|
||||
$cutoffTime = time() - $timeWindow;
|
||||
|
||||
foreach ($data as $ip => $info) {
|
||||
$recentRequests = array_filter(
|
||||
$info['requests'],
|
||||
fn ($timestamp) => $timestamp > $cutoffTime,
|
||||
);
|
||||
|
||||
$requestCount = count($recentRequests);
|
||||
|
||||
if ($requestCount >= $threshold) {
|
||||
$suspicious[$ip] = [
|
||||
'count' => $requestCount,
|
||||
'time_window' => $timeWindow,
|
||||
'first_seen' => $recentRequests === [] ? time() : min($recentRequests),
|
||||
'last_seen' => $recentRequests === [] ? time() : max($recentRequests),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $suspicious;
|
||||
}
|
||||
|
||||
private function handleSuspiciousIps(array $suspiciousIps, AlertService $alertService, bool $autoBlock): void
|
||||
{
|
||||
$blockedIps = Cache::get(self::CACHE_KEY_BLOCKED_IPS, []);
|
||||
$newBlocks = [];
|
||||
|
||||
foreach ($suspiciousIps as $ip => $details) {
|
||||
$this->error("VERDACHTE IP GEDETECTEERD: {$ip}");
|
||||
$this->table(
|
||||
['Metric', 'Value'],
|
||||
[
|
||||
['Requests', $details['count']],
|
||||
['Time Window', $details['time_window'] . 's'],
|
||||
['First Seen', date('H:i:s', $details['first_seen'])],
|
||||
['Last Seen', date('H:i:s', $details['last_seen'])],
|
||||
],
|
||||
);
|
||||
|
||||
if ($autoBlock && ! in_array($ip, $blockedIps)) {
|
||||
$this->blockIp($ip);
|
||||
$blockedIps[] = $ip;
|
||||
$newBlocks[] = $ip;
|
||||
}
|
||||
}
|
||||
|
||||
$totalRequests = array_sum(array_column($suspiciousIps, 'count'));
|
||||
$uniqueIps = count($suspiciousIps);
|
||||
|
||||
$alertService->sendDDoSDetected([
|
||||
'total_requests' => $totalRequests,
|
||||
'unique_ips' => $uniqueIps,
|
||||
'time_window' => array_first($suspiciousIps)['time_window'],
|
||||
'suspicious_ips' => array_keys($suspiciousIps),
|
||||
'auto_blocked' => $newBlocks,
|
||||
]);
|
||||
|
||||
if ($newBlocks !== []) {
|
||||
$this->info('Automatisch geblokkeerd: ' . implode(', ', $newBlocks));
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY_BLOCKED_IPS, $blockedIps, 3600);
|
||||
}
|
||||
|
||||
private function blockIp(string $ip): void
|
||||
{
|
||||
try {
|
||||
$escapedIp = escapeshellarg($ip);
|
||||
exec("iptables -A INPUT -s {$escapedIp} -j DROP 2>/dev/null");
|
||||
Log::warning("IP blocked due to DDoS detection: {$ip}");
|
||||
$this->warn("IP {$ip} geblokkeerd via iptables.");
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to block IP {$ip}: " . $e->getMessage());
|
||||
$this->error("Kon IP {$ip} niet blokkeren: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function trackRequest(string $ip): void
|
||||
{
|
||||
$tracking = Cache::get('manual_ip_tracking', []);
|
||||
|
||||
if (! isset($tracking[$ip])) {
|
||||
$tracking[$ip] = ['requests' => [], 'count' => 0];
|
||||
}
|
||||
|
||||
$tracking[$ip]['requests'][] = time();
|
||||
$tracking[$ip]['count']++;
|
||||
|
||||
Cache::put('manual_ip_tracking', $tracking, self::TRACKING_DURATION_SECONDS);
|
||||
}
|
||||
|
||||
public static function getBlockedIps(): array
|
||||
{
|
||||
return Cache::get(self::CACHE_KEY_BLOCKED_IPS, []);
|
||||
}
|
||||
|
||||
public static function clearBlockedIp(string $ip): void
|
||||
{
|
||||
$blocked = Cache::get(self::CACHE_KEY_BLOCKED_IPS, []);
|
||||
$blocked = array_filter($blocked, fn ($blockedIp) => $blockedIp !== $ip);
|
||||
Cache::put(self::CACHE_KEY_BLOCKED_IPS, array_values($blocked), 3600);
|
||||
|
||||
$escapedIp = escapeshellarg($ip);
|
||||
exec("iptables -D INPUT -s {$escapedIp} -j DROP 2>/dev/null");
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\AlertService;
|
||||
use App\Services\RconService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class EmulatorMonitorCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'monitor:emulator {--notify-online : Stuur een melding wanneer emulator online komt}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Monitor de emulator status en stuur alerts bij problemen';
|
||||
|
||||
private const string CACHE_KEY_EMULATOR_STATUS = 'emulator_monitor_status';
|
||||
|
||||
private const string CACHE_KEY_OFFLINE_SINCE = 'emulator_offline_since';
|
||||
|
||||
public function handle(AlertService $alertService): int
|
||||
{
|
||||
$this->info('Emulator status controleren...');
|
||||
|
||||
$rconService = new RconService;
|
||||
$isConnected = $rconService->isConnected();
|
||||
|
||||
Cache::get(self::CACHE_KEY_EMULATOR_STATUS);
|
||||
$wasOffline = Cache::has(self::CACHE_KEY_OFFLINE_SINCE);
|
||||
|
||||
if ($isConnected) {
|
||||
$this->handleOnlineStatus($alertService, $wasOffline);
|
||||
} else {
|
||||
$this->handleOfflineStatus($alertService);
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY_EMULATOR_STATUS, [
|
||||
'connected' => $isConnected,
|
||||
'checked_at' => now()->toIso8601String(),
|
||||
]);
|
||||
|
||||
$this->info('Emulator status: ' . ($isConnected ? 'ONLINE' : 'OFFLINE'));
|
||||
|
||||
return $isConnected ? Command::SUCCESS : Command::FAILURE;
|
||||
}
|
||||
|
||||
private function handleOnlineStatus(AlertService $alertService, bool $wasOffline): void
|
||||
{
|
||||
if ($wasOffline && $this->option('notify-online')) {
|
||||
$offlineSince = Cache::get(self::CACHE_KEY_OFFLINE_SINCE);
|
||||
|
||||
if ($offlineSince) {
|
||||
$duration = now()->diffInMinutes(Carbon::parse($offlineSince));
|
||||
$this->warn("Emulator was offline voor {$duration} minuten. Nu weer online!");
|
||||
}
|
||||
|
||||
$alertService->sendEmulatorOnline();
|
||||
}
|
||||
|
||||
Cache::forget(self::CACHE_KEY_OFFLINE_SINCE);
|
||||
$this->line('Emulator is online en reageert.');
|
||||
}
|
||||
|
||||
private function handleOfflineStatus(AlertService $alertService): void
|
||||
{
|
||||
if (! Cache::has(self::CACHE_KEY_OFFLINE_SINCE)) {
|
||||
Cache::put(self::CACHE_KEY_OFFLINE_SINCE, now()->toIso8601String());
|
||||
$this->warn('Emulator is OFFLINE! Eerste detectie - alert wordt verzonden.');
|
||||
$alertService->sendEmulatorOffline('RCON verbinding mislukt bij monitoring check');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$offlineSince = Cache::get(self::CACHE_KEY_OFFLINE_SINCE);
|
||||
$offlineDuration = now()->diffInMinutes(Carbon::parse($offlineSince));
|
||||
|
||||
$escalationMinutes = $this->getEscalationMinutes($offlineDuration);
|
||||
|
||||
if ($escalationMinutes > 0 && $offlineDuration % $escalationMinutes === 0) {
|
||||
$this->warn("Emulator is al {$offlineDuration} minuten offline. Escalating alert.");
|
||||
$alertService->sendEmulatorOffline("Emulator offline voor {$offlineDuration} minuten");
|
||||
}
|
||||
|
||||
$this->error("Emulator is OFFLINE! Offline sinds: {$offlineSince} ({$offlineDuration} minuten geleden)");
|
||||
}
|
||||
|
||||
private function getEscalationMinutes(float|int $offlineMinutes): int
|
||||
{
|
||||
return match (true) {
|
||||
$offlineMinutes >= 60 => 30,
|
||||
$offlineMinutes >= 30 => 15,
|
||||
$offlineMinutes >= 15 => 5,
|
||||
default => 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\AlertChannel;
|
||||
use App\Enums\AlertType;
|
||||
use App\Services\AlertService;
|
||||
use App\Services\EmulatorUpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EmulatorUpdateCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'emulator:update
|
||||
{--check : Alleen controleren op updates}
|
||||
{--force : Forceer update ook al is er geen nieuwe versie}
|
||||
{--repair : Probeer emulator te repareren}
|
||||
{--rebuild : Forceer build vanaf source}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Update de emulator vanaf GitHub';
|
||||
|
||||
public function handle(EmulatorUpdateService $updateService, AlertService $alertService): int
|
||||
{
|
||||
if ($this->option('repair')) {
|
||||
return $this->repairEmulator($updateService, $alertService);
|
||||
}
|
||||
|
||||
if (! $updateService->isConfigured()) {
|
||||
$this->error('Geen GitHub URL geconfigureerd voor emulator updates.');
|
||||
$this->info('Configureer dit in Filament > Settings > Emulator');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Emulator update service gestart...');
|
||||
|
||||
$checkResult = $updateService->checkForUpdates();
|
||||
|
||||
if (isset($checkResult['error'])) {
|
||||
$this->error($checkResult['error']);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['Property', 'Value'],
|
||||
[
|
||||
['Huidige Versie', $checkResult['current_version']],
|
||||
['Nieuwste Versie', $checkResult['latest_version']],
|
||||
['Update Beschikbaar', $checkResult['update_available'] ? 'JA' : 'NEE'],
|
||||
['Type', $checkResult['update_type'] ?? 'N/A'],
|
||||
],
|
||||
);
|
||||
|
||||
if ($this->option('check')) {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$force = $this->option('force');
|
||||
$rebuild = $this->option('rebuild');
|
||||
|
||||
if (! $checkResult['update_available'] && ! $force && ! $rebuild) {
|
||||
$this->info('Emulator is al up-to-date!');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $checkResult['update_available'] && ($force || $rebuild)) {
|
||||
$this->warn('Geen nieuwe versie beschikbaar, maar force/rebuild aangevraagd.');
|
||||
}
|
||||
|
||||
if (! $force && ! $rebuild && ! $this->confirm('Wil je de emulator updaten naar v' . $checkResult['latest_version'] . '?')) {
|
||||
$this->info('Update geannuleerd.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info('Emulator wordt geüpdatet...');
|
||||
|
||||
try {
|
||||
if ($rebuild || $checkResult['type'] === 'source_build') {
|
||||
$this->info('Build vanaf source...');
|
||||
$result = $updateService->buildFromSource($force);
|
||||
} else {
|
||||
$result = $updateService->updateEmulator();
|
||||
}
|
||||
|
||||
if ($result['success']) {
|
||||
$this->info($result['message'] ?? 'Emulator succesvol geüpdatet!');
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_ONLINE,
|
||||
'Emulator succesvol geüpdatet naar v' . ($result['version'] ?? 'onbekend'),
|
||||
[
|
||||
'version' => $result['version'] ?? 'onbekend',
|
||||
'jar' => $result['jar'] ?? 'N/A',
|
||||
],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
Log::info('[EmulatorUpdateCommand] Update successful', $result);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->error('Update mislukt: ' . ($result['error'] ?? 'Onbekende fout'));
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_ERROR,
|
||||
'Emulator update mislukt: ' . ($result['error'] ?? 'Onbekende fout'),
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
Log::error('[EmulatorUpdateCommand] Update failed', $result);
|
||||
|
||||
return Command::FAILURE;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Update exception: ' . $e->getMessage());
|
||||
$alertService->send(
|
||||
AlertType::CRITICAL_ERROR,
|
||||
'Emulator update exception: ' . $e->getMessage(),
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
Log::error('[EmulatorUpdateCommand] Exception', ['error' => $e->getMessage()]);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function repairEmulator(EmulatorUpdateService $updateService, AlertService $alertService): int
|
||||
{
|
||||
$this->warn('🔧 Emulator repair modus gestart...');
|
||||
$this->info('Dit zal de emulator status controleren en proberen te repareren.');
|
||||
|
||||
try {
|
||||
$repairResult = $updateService->repairEmulator();
|
||||
|
||||
if ($repairResult['success']) {
|
||||
$this->info('✅ Repair succesvol!');
|
||||
foreach ($repairResult['actions'] ?? [] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_ONLINE,
|
||||
'Emulator gerepareerd',
|
||||
['actions' => $repairResult['actions'] ?? []],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->error('❌ Repair mislukt: ' . ($repairResult['error'] ?? 'Onbekende fout'));
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_ERROR,
|
||||
'Emulator repair mislukt: ' . ($repairResult['error'] ?? 'Onbekende fout'),
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::FAILURE;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Repair exception: ' . $e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
class FixCodeCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'atom:fix-code {--dirty : Only fix changed files}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Automatically fix code style and syntax errors in the project';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('🛠️ Starting Atom Code Fixer...');
|
||||
|
||||
// 1. Check if Laravel Pint is installed (Standard in Laravel 9+)
|
||||
if (! file_exists(base_path('vendor/bin/pint'))) {
|
||||
$this->warn('⚠️ Laravel Pint not found. Installing...');
|
||||
$this->runProcess(['composer', 'require', 'laravel/pint', '--dev']);
|
||||
}
|
||||
|
||||
// 2. Run Pint (Fixes styling, unused imports, spacing)
|
||||
$this->info('🎨 Running Code Style Fixer (Pint)...');
|
||||
|
||||
$params = ['vendor/bin/pint'];
|
||||
if ($this->option('dirty')) {
|
||||
$params[] = '--dirty';
|
||||
}
|
||||
|
||||
$process = new Process($params, base_path());
|
||||
$process->setTimeout(300);
|
||||
$process->run(function ($type, $buffer) {
|
||||
$this->output->write($buffer);
|
||||
});
|
||||
|
||||
if (! $process->isSuccessful()) {
|
||||
$this->error('❌ Pint encountered some issues.');
|
||||
} else {
|
||||
$this->info('✅ Code style fixed!');
|
||||
}
|
||||
|
||||
// 3. Optional: Run PHPStan/Larastan if available (For deep logic checks)
|
||||
if (file_exists(base_path('vendor/bin/phpstan'))) {
|
||||
$this->info("\n🧠 Running Static Analysis (PHPStan)...");
|
||||
$analyze = new Process(['vendor/bin/phpstan', 'analyse', 'app'], base_path());
|
||||
$analyze->setTimeout(300);
|
||||
$analyze->run(function ($type, $buffer) {
|
||||
$this->output->write($buffer);
|
||||
});
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info('🚀 Code cleanup finished.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $command
|
||||
*/
|
||||
private function runProcess(array $command): void
|
||||
{
|
||||
$process = new Process($command, base_path());
|
||||
$process->run(function ($type, $buffer) {
|
||||
$this->output->write($buffer);
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class FixGamedataSymlinks extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'atom:fix-gamedata-symlinks {--dry-run : Show what would be done without making changes}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Create symlinks in gamedata directory for bundled assets';
|
||||
|
||||
private array $symlinks = [
|
||||
'effect' => 'bundled/effect',
|
||||
'furniture' => 'bundled/furniture',
|
||||
'generic' => 'bundled/generic',
|
||||
'pet' => 'bundled/pet',
|
||||
'figure' => 'bundled/figure',
|
||||
'generic_custom' => 'bundled/generic',
|
||||
];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$gamedataPath = base_path('../Gamedata');
|
||||
|
||||
if (! File::exists($gamedataPath)) {
|
||||
$this->error('Gamedata directory not found at: ' . $gamedataPath);
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Checking gamedata symlinks...');
|
||||
$this->newLine();
|
||||
|
||||
$created = 0;
|
||||
$skipped = 0;
|
||||
$fixed = 0;
|
||||
|
||||
foreach ($this->symlinks as $link => $target) {
|
||||
$linkPath = $gamedataPath . '/' . $link;
|
||||
$targetPath = $gamedataPath . '/' . $target;
|
||||
|
||||
if (! File::exists($targetPath)) {
|
||||
$this->warn(" ⚠️ Target does not exist: $target");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (File::exists($linkPath)) {
|
||||
if (is_link($linkPath)) {
|
||||
$currentTarget = readlink($linkPath);
|
||||
if ($currentTarget === $target) {
|
||||
$this->line(" ✓ $link → $target (already correct)");
|
||||
$skipped++;
|
||||
} elseif ($this->option('dry-run')) {
|
||||
$this->warn(" → Would fix: $link (currently points to: $currentTarget)");
|
||||
} else {
|
||||
unlink($linkPath);
|
||||
symlink($target, $linkPath);
|
||||
$this->info(" ✨ Fixed: $link → $target");
|
||||
$fixed++;
|
||||
}
|
||||
} elseif ($this->option('dry-run')) {
|
||||
$this->warn(" → Would replace: $link (is a regular file/directory)");
|
||||
} else {
|
||||
$this->warn(" ⚠️ Cannot create symlink, $link exists as file/directory");
|
||||
}
|
||||
} elseif ($this->option('dry-run')) {
|
||||
$this->warn(" → Would create: $link → $target");
|
||||
} else {
|
||||
symlink($target, $linkPath);
|
||||
$this->info(" ✓ Created: $link → $target");
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info('Summary:');
|
||||
$this->line(" Created: $created");
|
||||
$this->line(" Fixed: $fixed");
|
||||
$this->line(" Already correct: $skipped");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
+796
@@ -0,0 +1,796 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\NitroUpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class GenerateNitroConfigs extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'app:generate-nitro-configs
|
||||
{--site-url= : The site URL to use}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Generate Nitro configuration files (renderer-config.json, ui-config.json, UITexts.json)';
|
||||
|
||||
private array $checks = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$siteUrl = $this->option('site-url') ?? setting('nitro_site_url', config('app.url', 'https://epicnabbo.nl'));
|
||||
|
||||
$this->info('🔧 Nitro Config Generator');
|
||||
$this->line('═══════════════════════════════════════════════');
|
||||
|
||||
// Check 1: Validate URL
|
||||
$this->addCheck(function () use ($siteUrl) {
|
||||
if (empty($siteUrl) || ! filter_var($siteUrl, FILTER_VALIDATE_URL)) {
|
||||
throw new \Exception("Invalid URL: {$siteUrl}");
|
||||
}
|
||||
$this->line(" ✓ URL: {$siteUrl}");
|
||||
});
|
||||
|
||||
$nitroService = new NitroUpdateService;
|
||||
$status = $nitroService->getStatus();
|
||||
$webroot = $status['webroot'] ?? '/var/www/Client';
|
||||
$buildPath = $status['build_path'] ?? '/var/www/atomcms/nitro-client/dist';
|
||||
|
||||
// Check 2: Webroot exists
|
||||
$this->addCheck(function () use ($webroot) {
|
||||
$result = Process::timeout(5)->run('test -d ' . escapeshellarg((string) $webroot));
|
||||
if ($result->exitCode() !== 0) {
|
||||
throw new \Exception("Webroot does not exist: {$webroot}");
|
||||
}
|
||||
$this->line(" ✓ Webroot exists: {$webroot}");
|
||||
});
|
||||
|
||||
// Check 3: Webroot is writable
|
||||
$this->addCheck(function () use ($webroot) {
|
||||
$result = Process::timeout(5)->run('test -w ' . escapeshellarg((string) $webroot));
|
||||
if ($result->exitCode() !== 0) {
|
||||
throw new \Exception("Webroot is not writable: {$webroot}");
|
||||
}
|
||||
$this->line(' ✓ Webroot is writable');
|
||||
});
|
||||
|
||||
// Check 4: Build path exists
|
||||
$this->addCheck(function () use ($buildPath) {
|
||||
$result = Process::timeout(5)->run('test -d ' . escapeshellarg((string) $buildPath));
|
||||
if ($result->exitCode() !== 0) {
|
||||
throw new \Exception("Build path does not exist: {$buildPath}");
|
||||
}
|
||||
$this->line(" ✓ Build path exists: {$buildPath}");
|
||||
});
|
||||
|
||||
// Check both buildPath and webroot for example files
|
||||
$this->addCheck(function () use ($buildPath, $webroot) {
|
||||
// Map .example to .json fallback
|
||||
$fileMap = [
|
||||
'renderer-config.example' => 'renderer-config.json',
|
||||
'ui-config.example' => 'ui-config.json',
|
||||
'UITexts.example' => 'UITexts.example',
|
||||
];
|
||||
|
||||
$allFound = true;
|
||||
|
||||
foreach ($fileMap as $exampleFile => $jsonFile) {
|
||||
// Check .example file first
|
||||
$buildPathResult = Process::timeout(5)->run('test -f ' . escapeshellarg($buildPath . '/' . $exampleFile));
|
||||
$webrootResult = Process::timeout(5)->run('test -f ' . escapeshellarg($webroot . '/' . $exampleFile));
|
||||
|
||||
if ($buildPathResult->exitCode() === 0) {
|
||||
$this->line(" ✓ Found in build path: {$exampleFile}");
|
||||
} elseif ($webrootResult->exitCode() === 0) {
|
||||
$this->line(" ✓ Found in webroot: {$exampleFile}");
|
||||
} elseif ($jsonFile !== $exampleFile) {
|
||||
// Check json fallback for .example files
|
||||
$jsonBuildResult = Process::timeout(5)->run('test -f ' . escapeshellarg($buildPath . '/' . $jsonFile));
|
||||
$jsonWebrootResult = Process::timeout(5)->run('test -f ' . escapeshellarg($webroot . '/' . $jsonFile));
|
||||
if ($jsonBuildResult->exitCode() === 0) {
|
||||
$this->line(" ✓ Found in build path: {$jsonFile}");
|
||||
} elseif ($jsonWebrootResult->exitCode() === 0) {
|
||||
$this->line(" ✓ Found in webroot: {$jsonFile}");
|
||||
} else {
|
||||
$this->warn(" ⚠ Missing: {$exampleFile} or {$jsonFile}");
|
||||
$allFound = false;
|
||||
}
|
||||
} else {
|
||||
$this->warn(" ⚠ Missing: {$exampleFile}");
|
||||
$allFound = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $allFound) {
|
||||
throw new \Exception('Some example files are missing');
|
||||
}
|
||||
});
|
||||
|
||||
// Check 6: Check disk space
|
||||
$this->addCheck(function () use ($webroot) {
|
||||
$result = Process::timeout(10)->run("df -h {$webroot} | tail -1 | awk '{print $4}'");
|
||||
if ($result->successful()) {
|
||||
$free = trim($result->output());
|
||||
$this->line(" ✓ Free space: {$free}");
|
||||
}
|
||||
});
|
||||
|
||||
// Check 7: Validate bundled/config gamedata files
|
||||
$this->addCheck(function () {
|
||||
$bundledConfigDir = '/var/www/Gamedata/bundled/config';
|
||||
$configDir = '/var/www/Gamedata/config';
|
||||
$requiredFiles = ['HabboAvatarActions.json', 'FurnitureData.json', 'ExternalTexts.json', 'ProductData.json', 'FigureData.json', 'FigureMap.json', 'EffectMap.json'];
|
||||
|
||||
// Create bundled/config if it doesn't exist
|
||||
if (! is_dir($bundledConfigDir)) {
|
||||
mkdir($bundledConfigDir, 0755, true);
|
||||
$this->line(" 📁 Created directory: {$bundledConfigDir}");
|
||||
}
|
||||
|
||||
// Copy missing files from config to bundled/config
|
||||
foreach ($requiredFiles as $file) {
|
||||
$targetPath = $bundledConfigDir . '/' . $file;
|
||||
if (! file_exists($targetPath)) {
|
||||
$sourcePath = $configDir . '/' . $file;
|
||||
if (file_exists($sourcePath)) {
|
||||
copy($sourcePath, $targetPath);
|
||||
$this->line(" 📋 Copied: {$file} to bundled/config");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->line(' ✓ Gamedata bundled/config files ready');
|
||||
});
|
||||
|
||||
// Check 8: Validate existing config files
|
||||
$this->addCheck(function () use ($webroot) {
|
||||
$configFiles = ['renderer-config.json', 'ui-config.json', 'UITexts.json'];
|
||||
foreach ($configFiles as $file) {
|
||||
$path = $webroot . '/' . $file;
|
||||
$result = Process::timeout(5)->run('test -f ' . escapeshellarg($path));
|
||||
if ($result->exitCode() === 0) {
|
||||
// Validate JSON
|
||||
$jsonCheck = Process::timeout(5)->run('python3 -c "import json; json.load(open(\'' . $path . '\'))" 2>&1 || echo "INVALID"');
|
||||
if (str_contains($jsonCheck->output(), 'INVALID')) {
|
||||
$this->warn(" ⚠ Invalid JSON: {$file}");
|
||||
} else {
|
||||
$this->line(" ✓ Valid JSON: {$file}");
|
||||
}
|
||||
} else {
|
||||
$this->line(" - New file: {$file}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Run all checks
|
||||
$this->line('');
|
||||
$this->info('Running pre-flight checks...');
|
||||
$this->line('───────────────────────────────────────────────');
|
||||
|
||||
try {
|
||||
foreach ($this->checks as $check) {
|
||||
$check();
|
||||
}
|
||||
$this->line('───────────────────────────────────────────────');
|
||||
$this->info('✅ All checks passed!');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Check failed: ' . $e->getMessage());
|
||||
$this->newLine();
|
||||
$this->error('Please fix the issue before generating configs.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Generate configs
|
||||
$this->newLine();
|
||||
$this->info('Generating configuration files...');
|
||||
$this->line('───────────────────────────────────────────────');
|
||||
|
||||
$protocol = str_starts_with((string) $siteUrl, 'https') ? 'https' : 'http';
|
||||
$host = preg_replace('/^https?:\/\//', '', (string) $siteUrl);
|
||||
|
||||
try {
|
||||
// Step 0: Sync latest example files from GitHub
|
||||
$this->syncExampleFromGithub($buildPath, $webroot);
|
||||
|
||||
$rendererConfig = $this->generateRendererConfig($protocol, $host, $buildPath, $webroot);
|
||||
$uiConfig = $this->generateUiConfig($protocol, $host, $buildPath, $webroot);
|
||||
|
||||
// Compare with existing deployed configs
|
||||
$this->compareConfigs($rendererConfig, 'renderer-config', $webroot);
|
||||
$this->compareConfigs($uiConfig, 'ui-config', $webroot);
|
||||
|
||||
// Validate generated JSON
|
||||
$this->validateJson($rendererConfig, 'renderer-config');
|
||||
$this->validateJson($uiConfig, 'ui-config');
|
||||
|
||||
$tempFile = '/tmp/nitro_config_' . uniqid();
|
||||
file_put_contents($tempFile . '_renderer', json_encode($rendererConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
file_put_contents($tempFile . '_ui', json_encode($uiConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
// Copy generated configs
|
||||
$commands = [
|
||||
'cp "' . $tempFile . '_renderer" "' . $webroot . '/renderer-config.json"',
|
||||
'cp "' . $tempFile . '_ui" "' . $webroot . '/ui-config.json"',
|
||||
];
|
||||
|
||||
// Copy UITexts.json if it exists (check both buildPath and webroot)
|
||||
$uitextsInBuild = Process::timeout(5)->run('test -f "' . $buildPath . '/UITexts.example"')->exitCode() === 0;
|
||||
$uitextsInWebroot = Process::timeout(5)->run('test -f "' . $webroot . '/UITexts.example"')->exitCode() === 0;
|
||||
if ($uitextsInBuild) {
|
||||
$commands[] = 'cp "' . $buildPath . '/UITexts.example" "' . $webroot . '/UITexts.json"';
|
||||
$this->line(' ✓ Generated: UITexts.json (from build path)');
|
||||
} elseif ($uitextsInWebroot) {
|
||||
$commands[] = 'cp "' . $webroot . '/UITexts.example" "' . $webroot . '/UITexts.json"';
|
||||
$this->line(' ✓ Generated: UITexts.json (from webroot)');
|
||||
}
|
||||
|
||||
$commands[] = 'rm "' . $tempFile . '_renderer" "' . $tempFile . '_ui"';
|
||||
|
||||
Process::timeout(10)->run(implode(' && ', $commands));
|
||||
|
||||
// Set proper ownership
|
||||
Process::timeout(10)->run("chown www-data:www-data {$webroot}/renderer-config.json {$webroot}/ui-config.json {$webroot}/UITexts.json 2>/dev/null");
|
||||
|
||||
setting('nitro_config_generated_at', now()->toIso8601String());
|
||||
|
||||
$this->line(' ✓ Generated: renderer-config.json');
|
||||
$this->line(' ✓ Generated: ui-config.json');
|
||||
|
||||
// Verify generated files
|
||||
$this->verifyGeneratedFiles($webroot);
|
||||
|
||||
$this->line('───────────────────────────────────────────────');
|
||||
$this->info('✅ Configs generated successfully!');
|
||||
|
||||
Log::info('[NitroConfig] Generated successfully', [
|
||||
'webroot' => $webroot,
|
||||
'host' => $host,
|
||||
]);
|
||||
|
||||
return 0;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Generation failed: ' . $e->getMessage());
|
||||
Log::error('[NitroConfig] Generation failed', ['error' => $e->getMessage()]);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private function addCheck(callable $check): void
|
||||
{
|
||||
$this->checks[] = function () use ($check) {
|
||||
$check();
|
||||
};
|
||||
}
|
||||
|
||||
private function validateJson(array $data, string $name): void
|
||||
{
|
||||
$encoded = json_encode($data, JSON_THROW_ON_ERROR);
|
||||
$decoded = json_decode($encoded, true);
|
||||
|
||||
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \Exception("Invalid JSON generated for {$name}");
|
||||
}
|
||||
|
||||
$this->line(" ✓ Validated: {$name}");
|
||||
}
|
||||
|
||||
private function verifyGeneratedFiles(string $webroot): void
|
||||
{
|
||||
$files = ['renderer-config.json', 'ui-config.json', 'UITexts.json'];
|
||||
$allValid = true;
|
||||
|
||||
foreach ($files as $file) {
|
||||
$path = $webroot . '/' . $file;
|
||||
$result = Process::timeout(5)->run('test -f ' . escapeshellarg($path));
|
||||
|
||||
if ($result->exitCode() === 0) {
|
||||
$size = Process::timeout(5)->run('stat -c%s ' . escapeshellarg($path));
|
||||
$sizeStr = trim($size->output()) . ' bytes';
|
||||
$this->line(" ✓ Verified: {$file} ({$sizeStr})");
|
||||
} else {
|
||||
$this->warn(" ⚠ Missing: {$file}");
|
||||
$allValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $allValid) {
|
||||
throw new \Exception('Some files were not generated');
|
||||
}
|
||||
}
|
||||
|
||||
private function generateRendererConfig(string $protocol, string $host, string $buildPath, string $webroot): array
|
||||
{
|
||||
$httpProtocol = $protocol === 'https' ? 'https' : 'http';
|
||||
$wssProtocol = $protocol === 'https' ? 'wss' : 'ws';
|
||||
$wsHost = 'ws.' . $host;
|
||||
|
||||
$rendererConfig = [];
|
||||
|
||||
// Check build path first, then webroot for .example file
|
||||
$examplePath = $buildPath . '/renderer-config.example';
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() !== 0) {
|
||||
$examplePath = $webroot . '/renderer-config.example';
|
||||
}
|
||||
|
||||
// Fallback to .json version (newer Nitro-V3 format)
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() !== 0) {
|
||||
$jsonPath = $buildPath . '/renderer-config.json';
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($jsonPath))->exitCode() === 0) {
|
||||
$examplePath = $jsonPath;
|
||||
} else {
|
||||
$jsonPath = $webroot . '/renderer-config.json';
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($jsonPath))->exitCode() === 0) {
|
||||
$examplePath = $jsonPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load COMPLETE example file as base
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() === 0) {
|
||||
$content = file_get_contents($examplePath);
|
||||
// Fix invalid escape sequences (literal \n, \r, \t in JSON values)
|
||||
$content = $this->fixInvalidJsonEscapeSequences($content);
|
||||
$rendererConfig = @json_decode($content, true) ?: [];
|
||||
$this->line(' ✓ Loaded renderer config from: ' . basename($examplePath) . ' (' . count($rendererConfig) . ' keys)');
|
||||
} else {
|
||||
$this->warn(' ⚠ renderer-config.example/json not found');
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Recursively replace ALL URLs in the entire config
|
||||
$rendererConfig = $this->replaceUrlsRecursively($rendererConfig, $httpProtocol, $wssProtocol, $host, $wsHost);
|
||||
|
||||
// Auto-detect asset directory names on disk and fix paths
|
||||
$rendererConfig = $this->autoDetectAssetPaths($rendererConfig, $webroot);
|
||||
|
||||
// Special handling for socket.url - use ws subdomain
|
||||
if (isset($rendererConfig['socket.url'])) {
|
||||
$rendererConfig['socket.url'] = $wssProtocol . '://ws.' . $host;
|
||||
}
|
||||
|
||||
// Auto-detect local asset paths from Gamedata directory
|
||||
$gamedataBase = $httpProtocol . '://' . $host;
|
||||
|
||||
// Check what directories exist in Gamedata and set URLs accordingly
|
||||
$gamedataPath = '/var/www/Gamedata';
|
||||
if (is_dir($gamedataPath)) {
|
||||
// Check for bundled directory - serve via /gamedata/bundled (nginx alias)
|
||||
if (is_dir($gamedataPath . '/bundled')) {
|
||||
$rendererConfig['asset.url'] = $gamedataBase . '/gamedata/bundled';
|
||||
}
|
||||
// JSON config files are in /gamedata/config/
|
||||
if (is_dir($gamedataPath . '/config')) {
|
||||
$rendererConfig['gamedata.url'] = $gamedataBase . '/gamedata/config';
|
||||
}
|
||||
// Check for c_images directory - serve via /gamedata/c_images
|
||||
if (is_dir($gamedataPath . '/c_images')) {
|
||||
$rendererConfig['image.library.url'] = $gamedataBase . '/gamedata/c_images/';
|
||||
// Use icons folder for furni icons
|
||||
if (is_dir($gamedataPath . '/icons')) {
|
||||
$rendererConfig['hof.furni.url'] = $gamedataBase . '/gamedata/icons';
|
||||
} else {
|
||||
$rendererConfig['hof.furni.url'] = $gamedataBase . '/gamedata/c_images/dcr/hof_furni';
|
||||
}
|
||||
}
|
||||
// Fix furni icon path - icons are directly in hof.furni folder, not in icons subfolder
|
||||
if (isset($rendererConfig['furni.asset.icon.url'])) {
|
||||
$rendererConfig['furni.asset.icon.url'] = '${hof.furni.url}/%libname%%param%_icon.png';
|
||||
}
|
||||
// Fix sound machine samples path - sounds are in /gamedata/sounds/
|
||||
if (isset($rendererConfig['external.samples.url'])) {
|
||||
$rendererConfig['external.samples.url'] = $gamedataBase . '/gamedata/sounds/sound_machine_sample_%sample%.mp3';
|
||||
}
|
||||
// Check for images directory
|
||||
if (is_dir($gamedataPath . '/images')) {
|
||||
$rendererConfig['images.url'] = $gamedataBase . '/gamedata/images';
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing keys that might not be in the example
|
||||
if (! isset($rendererConfig['external.plugins'])) {
|
||||
$rendererConfig['external.plugins'] = [];
|
||||
}
|
||||
|
||||
// Add YouTube API key from settings
|
||||
$youtubeApiKey = setting('youtube_api_key', '');
|
||||
if (! empty($youtubeApiKey)) {
|
||||
$rendererConfig['youtube.api.key'] = $youtubeApiKey;
|
||||
$this->line(' ✓ Added YouTube API key to renderer config');
|
||||
}
|
||||
|
||||
// Ensure pet.types matches the exact required list in order
|
||||
if (isset($rendererConfig['pet.types'])) {
|
||||
$requiredPetTypes = [
|
||||
'dog',
|
||||
'cat',
|
||||
'croco',
|
||||
'terrier',
|
||||
'bear',
|
||||
'pig',
|
||||
'lion',
|
||||
'rhino',
|
||||
'spider',
|
||||
'turtle',
|
||||
'chicken',
|
||||
'frog',
|
||||
'dragon',
|
||||
'monster',
|
||||
'monkey',
|
||||
'horse',
|
||||
'monsterplant',
|
||||
'bunnyeaster',
|
||||
'bunnyevil',
|
||||
'bunnydepressed',
|
||||
'bunnylove',
|
||||
'pigeongood',
|
||||
'pigeonevil',
|
||||
'demonmonkey',
|
||||
'bearbaby',
|
||||
'terrierbaby',
|
||||
'gnome',
|
||||
'leprechaun',
|
||||
'kittenbaby',
|
||||
'puppybaby',
|
||||
'pigletbaby',
|
||||
'haloompa',
|
||||
'fools',
|
||||
'pterosaur',
|
||||
'velociraptor',
|
||||
'cow',
|
||||
'dragondog',
|
||||
'pkmshaymin2',
|
||||
'LeetEendjes',
|
||||
'pkmnentei',
|
||||
'squirtle',
|
||||
'LeetBH',
|
||||
'LeetCaviaaa',
|
||||
'LeetFantj',
|
||||
'LeetHotelMario',
|
||||
'LeetUil',
|
||||
'LeetWolf',
|
||||
'pokemon_mewblu',
|
||||
'LeetBB',
|
||||
'LeetHotelMari1',
|
||||
'LeetMewtw',
|
||||
'LeetPikachu',
|
||||
'LeetYos',
|
||||
'LeetE',
|
||||
'LeetMewt1',
|
||||
'LeetPen',
|
||||
'slendermn',
|
||||
'pkmnPAPI0',
|
||||
'pkmnPAPI1',
|
||||
'pkmnPAPI2',
|
||||
'pkmnPAPI3',
|
||||
'pkmnPAPI4',
|
||||
'pokmn_mew',
|
||||
'pkmashhhpet',
|
||||
'pkmbeautfly',
|
||||
'pkmcelebipe',
|
||||
'pkmdarkraip',
|
||||
'pkmeeveepet',
|
||||
'pkmjirachip',
|
||||
'pkmpichupet',
|
||||
'pkmriolupet',
|
||||
'pkmshayminp',
|
||||
'pkmtogepipe',
|
||||
'pkmvictinip',
|
||||
'slenderm1',
|
||||
'LeetEendj16',
|
||||
'pkmnente1',
|
||||
'LeetBa',
|
||||
'babymeisje',
|
||||
'babyBH',
|
||||
'bb_hbx',
|
||||
'LeetUi1',
|
||||
];
|
||||
|
||||
$rendererConfig['pet.types'] = $requiredPetTypes;
|
||||
$this->line(' ✓ Set pet.types to exact required list');
|
||||
}
|
||||
|
||||
return $rendererConfig;
|
||||
}
|
||||
|
||||
private function generateUiConfig(string $protocol, string $host, string $buildPath, string $webroot): array
|
||||
{
|
||||
$httpProtocol = $protocol === 'https' ? 'https' : 'http';
|
||||
$wssProtocol = $protocol === 'https' ? 'wss' : 'ws';
|
||||
$wsHost = 'ws.' . $host;
|
||||
|
||||
$uiConfig = [];
|
||||
|
||||
// Check build path first, then webroot for .example file
|
||||
$examplePath = $buildPath . '/ui-config.example';
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() !== 0) {
|
||||
$examplePath = $webroot . '/ui-config.example';
|
||||
}
|
||||
|
||||
// Fallback to .json version (newer Nitro-V3 format)
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() !== 0) {
|
||||
$jsonPath = $buildPath . '/ui-config.json';
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($jsonPath))->exitCode() === 0) {
|
||||
$examplePath = $jsonPath;
|
||||
} else {
|
||||
$jsonPath = $webroot . '/ui-config.json';
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($jsonPath))->exitCode() === 0) {
|
||||
$examplePath = $jsonPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load COMPLETE example file as base
|
||||
if (Process::timeout(5)->run('test -f ' . escapeshellarg($examplePath))->exitCode() === 0) {
|
||||
$content = file_get_contents($examplePath);
|
||||
// Fix invalid escape sequences
|
||||
$content = $this->fixInvalidJsonEscapeSequences($content);
|
||||
$uiConfig = @json_decode($content, true) ?: [];
|
||||
$this->line(' ✓ Loaded ui config from: ' . basename($examplePath) . ' (' . count($uiConfig) . ' keys)');
|
||||
} else {
|
||||
$this->warn(' ⚠ ui-config.example/json not found');
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Recursively replace ALL URLs in the entire config
|
||||
$uiConfig = $this->replaceUrlsRecursively($uiConfig, $httpProtocol, $wssProtocol, $host, $wsHost);
|
||||
|
||||
return $uiConfig;
|
||||
}
|
||||
|
||||
private function fixInvalidJsonEscapeSequences(string $content): string
|
||||
{
|
||||
// The example file has literal backslash + any letter in JSON values
|
||||
// which breaks JSON. We need to escape all of these.
|
||||
|
||||
$backslash = chr(92);
|
||||
$letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
|
||||
|
||||
foreach ($letters as $letter) {
|
||||
$content = str_replace($backslash . $letter, $backslash . $backslash . $letter, $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function replaceUrlsRecursively(array $config, string $httpProtocol, string $wsProtocol, string $host, string $wsHost): array
|
||||
{
|
||||
foreach ($config as &$value) {
|
||||
if (is_array($value)) {
|
||||
// Handle nested arrays (like navigator.room.models)
|
||||
if ($this->isAssociativeArray($value)) {
|
||||
$value = $this->replaceUrlsRecursively($value, $httpProtocol, $wsProtocol, $host, $wsHost);
|
||||
} else {
|
||||
// Handle arrays of URLs
|
||||
$value = array_map(function ($item) use ($httpProtocol, $wsProtocol, $host, $wsHost) {
|
||||
if (is_string($item)) {
|
||||
return $this->replaceUrl($item, $httpProtocol, $wsProtocol, $host, $wsHost);
|
||||
}
|
||||
|
||||
return $item;
|
||||
}, $value);
|
||||
}
|
||||
} elseif (is_string($value)) {
|
||||
$value = $this->replaceUrl($value, $httpProtocol, $wsProtocol, $host, $wsHost);
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
private function replaceUrl(string $url, string $httpProtocol, string $wsProtocol, string $host, string $wsHost): string
|
||||
{
|
||||
// Replace ws/wss URLs with ws subdomain
|
||||
if (str_starts_with($url, 'ws://') || str_starts_with($url, 'wss://')) {
|
||||
$path = parse_url($url, PHP_URL_PATH) ?? '/';
|
||||
|
||||
return $wsProtocol . '://' . $wsHost . ($path !== '/' ? $path : '');
|
||||
}
|
||||
|
||||
// Replace localhost in all URLs
|
||||
$url = preg_replace('#https?://localhost(?::\d+)?#', $httpProtocol . '://' . $host, $url);
|
||||
$url = preg_replace('#wss?://localhost(?::\d+)?#', $wsProtocol . '://' . $wsHost, (string) $url);
|
||||
$url = preg_replace('#localhost(?::\d+)?#', $host, (string) $url);
|
||||
|
||||
// Fix broken escape sequences in URL paths (from invalid JSON in example files)
|
||||
// Pattern: /public\nitro-assets\gamedata or any variation
|
||||
$url = preg_replace('#/public\\n[a-z_-]+\\\\gamedata#i', '/gamedata', (string) $url);
|
||||
$url = preg_replace('#/public\\\\[a-z_-]+\\\\gamedata#i', '/gamedata', (string) $url);
|
||||
$url = preg_replace('#/nitro-assets\\\\gamedata#i', '/gamedata', (string) $url);
|
||||
$url = preg_replace('#/nitro\\\\[a-z_-]+#i', '/gamedata', (string) $url);
|
||||
|
||||
// Fix known asset path patterns
|
||||
$url = str_replace('/public/nitro-assets/gamedata', '/gamedata', $url);
|
||||
$url = str_replace('/swf/gamedata', '/gamedata', $url);
|
||||
|
||||
// Clean up any remaining backslashes in URLs
|
||||
$url = str_replace('\\', '/', $url);
|
||||
|
||||
// Clean up any remaining double slashes (but keep protocol slashes)
|
||||
$url = preg_replace('#([^:])//+#', '$1/', $url);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
private function isAssociativeArray(array $arr): bool
|
||||
{
|
||||
if ($arr === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_keys($arr) !== range(0, count($arr) - 1);
|
||||
}
|
||||
|
||||
private function autoDetectAssetPaths(array $config, string $webroot): array
|
||||
{
|
||||
// Check multiple possible gamedata locations
|
||||
$possiblePaths = [
|
||||
$webroot . '/gamedata',
|
||||
'/var/www/Gamedata',
|
||||
'/var/www/gamedata',
|
||||
];
|
||||
$gamedataPath = array_find($possiblePaths, fn ($path) => is_dir($path));
|
||||
|
||||
if (! $gamedataPath) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
$assetDir = opendir($gamedataPath);
|
||||
$actualDirs = [];
|
||||
while (($entry = readdir($assetDir)) !== false) {
|
||||
if ($entry !== '.' && $entry !== '..' && is_dir($gamedataPath . '/' . $entry)) {
|
||||
$actualDirs[strtolower($entry)] = $entry;
|
||||
}
|
||||
}
|
||||
closedir($assetDir);
|
||||
|
||||
$pathChecks = [
|
||||
'pet.asset.url' => 'pets',
|
||||
'furni.asset.url' => 'furniture',
|
||||
'avatar.asset.url' => 'clothes',
|
||||
'avatar.asset.effect.url' => 'effect',
|
||||
'generic.asset.url' => 'generic_custom',
|
||||
];
|
||||
|
||||
foreach ($config as $key => &$value) {
|
||||
if (! is_string($value) || ! isset($pathChecks[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$expectedDir = $pathChecks[$key];
|
||||
$lowerExpected = strtolower($expectedDir);
|
||||
$actualName = null;
|
||||
|
||||
// Special case: "figure" is often used instead of "clothes" for avatars
|
||||
if ($lowerExpected === 'clothes' && isset($actualDirs['figure'])) {
|
||||
$actualName = $actualDirs['figure'];
|
||||
} elseif (isset($actualDirs[$lowerExpected])) {
|
||||
$actualName = $actualDirs[$lowerExpected];
|
||||
} else {
|
||||
foreach ($actualDirs as $actualLower => $actual) {
|
||||
if (str_starts_with($actualLower, rtrim($lowerExpected, 's')) ||
|
||||
str_starts_with(rtrim($actualLower, 's'), rtrim($lowerExpected, 's'))) {
|
||||
$actualName = $actual;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($actualName && $actualName !== $expectedDir) {
|
||||
$value = str_replace("/{$expectedDir}/", "/{$actualName}/", $value);
|
||||
$this->line(" 🔍 Auto-detected: {$key} -> /{$actualName}/ (was /{$expectedDir}/)");
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
private function syncExampleFromGithub(string $buildPath, string $webroot): void
|
||||
{
|
||||
$this->info('Syncing latest examples from GitHub...');
|
||||
|
||||
$rendererRepo = setting('nitro_github_url', '');
|
||||
$repo = $this->parseRepoFromUrl($rendererRepo);
|
||||
if (! $repo) {
|
||||
$this->warn(' ⚠ No GitHub repo configured, skipping sync');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$branch = setting('nitro_github_branch', 'main');
|
||||
|
||||
$examples = [
|
||||
'renderer-config.example' => 'renderer-config.example',
|
||||
'ui-config.example' => 'ui-config.example',
|
||||
'UITexts.example' => 'UITexts.json',
|
||||
];
|
||||
|
||||
foreach (array_keys($examples) as $remoteFile) {
|
||||
$tempFile = '/tmp/' . $remoteFile . '_' . uniqid();
|
||||
$fetched = false;
|
||||
|
||||
// Try multiple paths: root, public/, nitro-client/dist/, Nitro-V3 paths
|
||||
$paths = ['', 'public/', 'nitro-client/dist/', 'dist/', 'src/', 'assets/'];
|
||||
foreach ($paths as $path) {
|
||||
$url = "https://raw.githubusercontent.com/{$repo}/{$branch}/{$path}{$remoteFile}";
|
||||
$result = Process::timeout(15)->run("curl -sL -o {$tempFile} '{$url}'");
|
||||
|
||||
if ($result->successful() && file_exists($tempFile) && filesize($tempFile) > 10) {
|
||||
$content = file_get_contents($tempFile);
|
||||
$data = @json_decode($content, true);
|
||||
|
||||
if (is_array($data) && $data !== []) {
|
||||
// Save to both buildPath and webroot
|
||||
file_put_contents($buildPath . '/' . $remoteFile, $content);
|
||||
file_put_contents($webroot . '/' . $remoteFile, $content);
|
||||
$this->line(" ✓ Synced: {$remoteFile} (" . count($data) . " keys from {$path}{$remoteFile})");
|
||||
$fetched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@unlink($tempFile);
|
||||
}
|
||||
|
||||
if (! $fetched) {
|
||||
$this->line(" - Skipped: {$remoteFile} (not found in any path)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parseRepoFromUrl(string $url): ?string
|
||||
{
|
||||
if (preg_match('/github\.com\/([^\/]+\/[^\/\?#]+)/', $url, $matches)) {
|
||||
return rtrim($matches[1], '/');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function compareConfigs(array $generated, string $name, string $webroot): void
|
||||
{
|
||||
$currentPath = $webroot . '/' . $name . '.json';
|
||||
if (! file_exists($currentPath)) {
|
||||
$this->line(" ℹ {$name}: Geen bestaande config — nieuwe generatie");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$current = @json_decode(file_get_contents($currentPath), true);
|
||||
if (! is_array($current)) {
|
||||
$this->warn(" ⚠ {$name}: Bestaande config is ongeldig JSON");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$newKeys = array_diff(array_keys($generated), array_keys($current));
|
||||
$removedKeys = array_diff(array_keys($current), array_keys($generated));
|
||||
$changedKeys = [];
|
||||
|
||||
foreach (array_intersect(array_keys($generated), array_keys($current)) as $key) {
|
||||
if ($generated[$key] !== $current[$key]) {
|
||||
$changedKeys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
if ($newKeys !== []) {
|
||||
$this->line(" 🆕 {$name}: " . count($newKeys) . ' nieuwe key(s): ' . implode(', ', array_slice($newKeys, 0, 10)));
|
||||
if (count($newKeys) > 10) {
|
||||
$this->line(' ... en ' . (count($newKeys) - 10) . ' meer');
|
||||
}
|
||||
}
|
||||
if ($removedKeys !== []) {
|
||||
$this->line(" 🗑 {$name}: " . count($removedKeys) . ' verwijderde key(s): ' . implode(', ', array_slice($removedKeys, 0, 5)));
|
||||
}
|
||||
if ($changedKeys !== []) {
|
||||
$this->line(" 🔄 {$name}: " . count($changedKeys) . ' gewijzigde key(s): ' . implode(', ', array_slice($changedKeys, 0, 5)));
|
||||
}
|
||||
if ($newKeys === [] && $removedKeys === [] && $changedKeys === []) {
|
||||
$this->line(" ✓ {$name}: Geen wijzigingen");
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\WebsiteAd;
|
||||
use App\Services\SettingsService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ImportAdsData extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'import:ads-data';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Import ads data from the filesystem';
|
||||
|
||||
private const int CHUNK_SIZE = 100;
|
||||
|
||||
private const array ALLOWED_EXTENSIONS = ['jpeg', 'jpg', 'png', 'gif'];
|
||||
|
||||
public function handle(SettingsService $settingsService): void
|
||||
{
|
||||
$adsPathSetting = $settingsService->getOrDefault('ads_path_filesystem');
|
||||
$adsPath = is_string($adsPathSetting) ? $adsPathSetting : '';
|
||||
|
||||
if (! $this->validatePath($adsPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = $this->getImageFiles($adsPath);
|
||||
|
||||
if ($files === []) {
|
||||
$this->warn('No valid image files found in the ads directory.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processFiles($files);
|
||||
|
||||
$this->info('Ads data import completed successfully.');
|
||||
}
|
||||
|
||||
private function validatePath(?string $adsPath): bool
|
||||
{
|
||||
if (in_array($adsPath, [null, '', '0'], true)) {
|
||||
$this->error('Ads path is not configured in website_settings.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! is_dir($adsPath)) {
|
||||
$this->error("The ads path '{$adsPath}' does not exist in the filesystem.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function getImageFiles(string $adsPath): array
|
||||
{
|
||||
return array_filter(scandir($adsPath), function ($file) use ($adsPath) {
|
||||
$filePath = $adsPath . DIRECTORY_SEPARATOR . $file;
|
||||
|
||||
return is_file($filePath) &&
|
||||
in_array(strtolower(pathinfo($file, PATHINFO_EXTENSION)), self::ALLOWED_EXTENSIONS);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $files
|
||||
*/
|
||||
private function processFiles(array $files): void
|
||||
{
|
||||
// Get existing images to avoid duplicates
|
||||
$existingImages = WebsiteAd::query()->pluck('image')->toArray();
|
||||
|
||||
$newFiles = Collection::make($files)
|
||||
->filter(fn ($file) => ! in_array($file, $existingImages))
|
||||
->map(fn ($file) => ['image' => $file])
|
||||
->values();
|
||||
|
||||
$skippedCount = count($files) - $newFiles->count();
|
||||
if ($skippedCount > 0) {
|
||||
$this->warn("Skipped {$skippedCount} existing files.");
|
||||
}
|
||||
|
||||
$newFiles->chunk(self::CHUNK_SIZE)->each(function ($chunk) {
|
||||
WebsiteAd::insert($chunk->all());
|
||||
$this->info('Processed ' . $chunk->count() . ' files.');
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\WebsiteBadge;
|
||||
use App\Services\SettingsService;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ImportBadgeData extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'import:badge-data';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Import badge data from JSON file';
|
||||
|
||||
private const int CHUNK_SIZE = 100;
|
||||
|
||||
private const string BADGE_PREFIX = 'badge_desc_';
|
||||
|
||||
public function __construct(
|
||||
private readonly SettingsService $settingsService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$jsonPathSetting = $this->settingsService->getOrDefault('nitro_external_texts_file');
|
||||
$jsonPath = is_string($jsonPathSetting) ? $jsonPathSetting : '';
|
||||
|
||||
if (! $this->validateJsonFile($jsonPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->processBadgeData($jsonPath);
|
||||
$this->info('Badge data imported successfully.');
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to import badge data: ' . $e->getMessage());
|
||||
$this->error('Failed to import badge data. Check the logs for details.');
|
||||
}
|
||||
}
|
||||
|
||||
private function validateJsonFile(?string $jsonPath): bool
|
||||
{
|
||||
if (in_array($jsonPath, [null, '', '0'], true)) {
|
||||
$this->error('The JSON file path is not configured in the website settings.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! file_exists($jsonPath)) {
|
||||
$this->error('The JSON file does not exist at the specified path: ' . $jsonPath);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function processBadgeData(string $jsonPath): void
|
||||
{
|
||||
$jsonData = File::json($jsonPath);
|
||||
|
||||
// Extract badge names and descriptions
|
||||
$badgeNames = Collection::make($jsonData)
|
||||
->filter(fn ($value, $key) => str_starts_with((string) $key, 'badge_name_'))
|
||||
->mapWithKeys(fn ($value, $key) => [str_replace('badge_name_', '', $key) => $value]);
|
||||
|
||||
$badgeDescriptions = Collection::make($jsonData)
|
||||
->filter(fn ($value, $key) => str_starts_with((string) $key, self::BADGE_PREFIX))
|
||||
->mapWithKeys(fn ($value, $key) => [str_replace(self::BADGE_PREFIX, '', $key) => $value]);
|
||||
|
||||
// Combine badge names and descriptions
|
||||
$badgeData = $badgeNames->map(fn ($name, $key) => [
|
||||
'badge_key' => $key, // Use only the badge name (e.g., 14X12, 14XR1)
|
||||
'badge_name' => $name,
|
||||
'badge_description' => $badgeDescriptions->get($key, 'No description available'),
|
||||
])->values();
|
||||
|
||||
// Upsert the combined data in chunks
|
||||
$badgeData->chunk(self::CHUNK_SIZE)->each(function ($chunk) {
|
||||
WebsiteBadge::upsert(
|
||||
$chunk->toArray(),
|
||||
['badge_key'],
|
||||
['badge_name', 'badge_description'],
|
||||
);
|
||||
|
||||
$this->info('Processed ' . $chunk->count() . ' badges.');
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
+272
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\AlertChannel;
|
||||
use App\Enums\AlertType;
|
||||
use App\Services\AlertService;
|
||||
use App\Services\NitroUpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class NitroUpdateCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'nitro:auto
|
||||
{--force : Forceer update ook al is het niet de geplande tijd}
|
||||
{--build-only : Alleen build en deploy uitvoeren}
|
||||
{--full : Volledige reset en reinstall}
|
||||
{--repair : Probeer Nitro te repareren}
|
||||
{--diagnose : Toon diagnose informatie}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Automatische Nitro client en renderer updates';
|
||||
|
||||
private const string CACHE_KEY_LAST_UPDATE = 'nitro_last_update';
|
||||
|
||||
public function handle(AlertService $alertService, NitroUpdateService $nitroService): int
|
||||
{
|
||||
$this->info('Nitro update check...');
|
||||
|
||||
if ($this->option('diagnose')) {
|
||||
return $this->diagnose($nitroService);
|
||||
}
|
||||
|
||||
if ($this->option('repair')) {
|
||||
return $this->repair($alertService, $nitroService);
|
||||
}
|
||||
|
||||
$isForced = $this->option('force');
|
||||
$buildOnly = $this->option('build-only');
|
||||
$full = $this->option('full');
|
||||
|
||||
if ($buildOnly) {
|
||||
return $this->buildOnly($alertService, $nitroService);
|
||||
}
|
||||
|
||||
if (! $isForced && ! $this->isScheduledTime()) {
|
||||
$this->line('Niet de geplande tijd, overslaan.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$repo = setting('nitro_github_url', 'duckietm/Nitro-V3 (default)');
|
||||
$this->info("GitHub: {$repo}");
|
||||
|
||||
if ($full) {
|
||||
return $this->fullReinstall($alertService, $nitroService);
|
||||
}
|
||||
|
||||
$status = $nitroService->getStatus();
|
||||
|
||||
$this->info('Client: ' . ($status['client_installed'] ? '✅' : '❌'));
|
||||
$this->info('Renderer: ' . ($status['renderer_installed'] ? '✅' : '❌'));
|
||||
$this->info('Build: ' . ($status['build_exists'] ? '✅' : '❌'));
|
||||
$this->info('Deployed: ' . ($status['deployed'] ? '✅' : '❌'));
|
||||
|
||||
if (! $status['client_installed'] || ! $status['renderer_installed']) {
|
||||
$this->warn('Nitro niet volledig geïnstalleerd. Voer --full uit voor volledige installatie.');
|
||||
if ($this->confirm('Wil je nu volledig installeren?')) {
|
||||
return $this->fullReinstall($alertService, $nitroService);
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->line('✅ Nitro client is up-to-date.');
|
||||
|
||||
if ($isForced) {
|
||||
$this->warn('Force update aangevraagd...');
|
||||
|
||||
return $this->fullReinstall($alertService, $nitroService);
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY_LAST_UPDATE, now()->toIso8601String());
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function diagnose(NitroUpdateService $nitroService): int
|
||||
{
|
||||
$this->info('🔍 Nitro diagnose...');
|
||||
$diagnosis = $nitroService->diagnose();
|
||||
|
||||
$this->newLine();
|
||||
$this->info('=== Controle Resultaten ===');
|
||||
|
||||
$checks = $diagnosis['checks'] ?? [];
|
||||
foreach ($checks as $key => $value) {
|
||||
if (is_bool($value)) {
|
||||
$this->line(($value ? '✅' : '❌') . ' ' . $key);
|
||||
} elseif (is_array($value)) {
|
||||
$this->line($key . ': ' . count($value) . ' items');
|
||||
} else {
|
||||
$this->line($key . ': ' . $value);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($diagnosis['issues'])) {
|
||||
$this->newLine();
|
||||
$this->error('=== Problemen ===');
|
||||
foreach ($diagnosis['issues'] as $issue) {
|
||||
$this->line('❌ ' . $issue);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($diagnosis['recommendations'])) {
|
||||
$this->newLine();
|
||||
$this->info('=== Aanbevelingen ===');
|
||||
foreach ($diagnosis['recommendations'] as $rec) {
|
||||
$this->line('💡 ' . $rec);
|
||||
}
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function repair(AlertService $alertService, NitroUpdateService $nitroService): int
|
||||
{
|
||||
$this->warn('🔧 Nitro repair modus gestart...');
|
||||
$this->info('Dit zal de Nitro installatie controleren en repareren.');
|
||||
|
||||
try {
|
||||
$repairResult = $nitroService->repair();
|
||||
|
||||
if ($repairResult['success']) {
|
||||
$this->info('✅ Repair succesvol!');
|
||||
foreach ($repairResult['actions'] ?? [] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_UPDATE,
|
||||
'Nitro client gerepareerd',
|
||||
['actions' => $repairResult['actions'] ?? []],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->error('❌ Repair mislukt: ' . ($repairResult['error'] ?? 'Onbekende fout'));
|
||||
if (! empty($repairResult['actions'])) {
|
||||
$this->line('Uitgevoerde acties:');
|
||||
foreach ($repairResult['actions'] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
}
|
||||
if (! empty($repairResult['errors'])) {
|
||||
$this->line('Fouten:');
|
||||
foreach ($repairResult['errors'] as $error) {
|
||||
$this->line(' ❌ ' . $error);
|
||||
}
|
||||
}
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_ERROR,
|
||||
'Nitro repair mislukt: ' . ($repairResult['error'] ?? 'Onbekende fout'),
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::FAILURE;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Repair exception: ' . $e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function buildOnly(AlertService $alertService, NitroUpdateService $nitroService): int
|
||||
{
|
||||
$this->info('Build en deploy uitvoeren...');
|
||||
|
||||
$buildResult = $nitroService->buildClient();
|
||||
|
||||
if (! $buildResult['success']) {
|
||||
$this->error('Build mislukt: ' . ($buildResult['error'] ?? 'Onbekend'));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$nitroService->deployClient();
|
||||
$nitroService->generateConfigs();
|
||||
|
||||
$this->info('Client succesvol gedeployed!');
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_UPDATE,
|
||||
'Nitro client opnieuw gedeployed',
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function fullReinstall(AlertService $alertService, NitroUpdateService $nitroService): int
|
||||
{
|
||||
$this->warn('Volledige reinstall wordt uitgevoerd...');
|
||||
|
||||
try {
|
||||
$result = $nitroService->updateNitro();
|
||||
|
||||
if ($result['success']) {
|
||||
$this->info('Nitro succesvol opnieuw geïnstalleerd!');
|
||||
$alertService->send(
|
||||
AlertType::EMULATOR_UPDATE,
|
||||
'Nitro client succesvol geüpdatet en gedeployed',
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->error('Reinstall mislukt: ' . ($result['error'] ?? 'Onbekende fout'));
|
||||
$alertService->send(
|
||||
AlertType::CRITICAL_ERROR,
|
||||
'Nitro Update Mislukt: ' . ($result['error'] ?? 'Onbekende fout'),
|
||||
[],
|
||||
AlertChannel::DISCORD,
|
||||
);
|
||||
|
||||
return Command::FAILURE;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Reinstall exception: ' . $e->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function isScheduledTime(): bool
|
||||
{
|
||||
$scheduleTime = setting('nitro_auto_update_schedule', '03:00');
|
||||
$scheduleDays = setting('nitro_auto_update_days', '0,6');
|
||||
$enabled = setting('nitro_auto_update_enabled', false);
|
||||
|
||||
if (! $enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$now = now();
|
||||
$currentTime = $now->format('H:i');
|
||||
$currentDay = (int) $now->dayOfWeek;
|
||||
|
||||
$allowedDays = array_map(intval(...), explode(',', $scheduleDays));
|
||||
|
||||
if (! in_array($currentDay, $allowedDays)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($currentTime !== $scheduleTime) {
|
||||
$minuteDiff = abs(strtotime($currentTime) - strtotime((string) $scheduleTime)) / 60;
|
||||
if ($minuteDiff > 5) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\NitroUpdateService;
|
||||
use App\Services\SettingsService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SwitchNitroBranch extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'app:switch-nitro-branch {--branch=main}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Switch Nitro to a specific branch (runs in background)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$branch = $this->option('branch') ?? 'main';
|
||||
|
||||
$this->info("🔄 Switching Nitro to branch: {$branch}");
|
||||
|
||||
try {
|
||||
$nitroService = new NitroUpdateService;
|
||||
$result = $nitroService->updateNitro(true);
|
||||
|
||||
if ($result['success'] ?? false) {
|
||||
$this->info("✅ Switched to {$branch} successfully!");
|
||||
$this->info($result['message'] ?? '');
|
||||
Log::info('[NitroSwitch] Success', ['branch' => $branch, 'message' => $result['message'] ?? '']);
|
||||
} else {
|
||||
$this->error('❌ Switch failed: ' . ($result['error'] ?? 'Unknown error'));
|
||||
Log::error('[NitroSwitch] Failed', ['branch' => $branch, 'error' => $result['error'] ?? 'Unknown']);
|
||||
}
|
||||
|
||||
Cache::forget('website_settings');
|
||||
SettingsService::clearCache();
|
||||
|
||||
return ($result['success'] ?? false) ? 0 : 1;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('❌ Exception: ' . $e->getMessage());
|
||||
Log::error('[NitroSwitch] Exception', ['branch' => $branch, 'error' => $e->getMessage()]);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+5071
File diff suppressed because it is too large
Load Diff
Executable
+251
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\EmulatorUpdateService;
|
||||
use App\Services\NitroUpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class SystemHealthCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'system:health
|
||||
{--json : Output als JSON}
|
||||
{--details : Toon meer details}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Controleer systeem gezondheid';
|
||||
|
||||
public function handle(EmulatorUpdateService $emuService, NitroUpdateService $nitroService): int
|
||||
{
|
||||
$json = $this->option('json');
|
||||
$this->option('details');
|
||||
|
||||
$health = $this->performHealthCheck($emuService, $nitroService);
|
||||
|
||||
if ($json) {
|
||||
$this->line(json_encode($health, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return $health['status'] === 'healthy' ? 0 : 1;
|
||||
}
|
||||
|
||||
$this->displayHealthCheck($health);
|
||||
|
||||
return $health['status'] === 'healthy' ? 0 : 1;
|
||||
}
|
||||
|
||||
private function performHealthCheck(EmulatorUpdateService $emuService, NitroUpdateService $nitroService): array
|
||||
{
|
||||
$checks = [];
|
||||
$issues = [];
|
||||
$warnings = [];
|
||||
|
||||
$checks['timestamp'] = now()->toIso8601String();
|
||||
|
||||
$checks['php'] = [
|
||||
'version' => PHP_VERSION,
|
||||
'status' => 'ok',
|
||||
];
|
||||
|
||||
$checks['system'] = [
|
||||
'os' => PHP_OS,
|
||||
'user' => posix_getpwuid(posix_geteuid())['name'] ?? 'unknown',
|
||||
];
|
||||
|
||||
$diskFree = @disk_free_space('/');
|
||||
$diskTotal = @disk_total_space('/');
|
||||
$diskPercent = $diskTotal > 0 ? round(($diskFree / $diskTotal) * 100, 1) : 0;
|
||||
$checks['disk'] = [
|
||||
'free' => $this->formatBytes($diskFree),
|
||||
'total' => $this->formatBytes($diskTotal),
|
||||
'percent_free' => $diskPercent,
|
||||
'status' => $diskPercent > 10 ? 'ok' : 'critical',
|
||||
];
|
||||
if ($diskPercent < 10) {
|
||||
$issues[] = 'Disk space critically low: ' . $diskPercent . '% free';
|
||||
} elseif ($diskPercent < 20) {
|
||||
$warnings[] = 'Disk space low: ' . $diskPercent . '% free';
|
||||
}
|
||||
|
||||
try {
|
||||
$emuDiagnosis = $emuService->diagnose();
|
||||
$checks['emulator'] = [
|
||||
'configured' => $emuDiagnosis['checks']['is_configured'] ?? false,
|
||||
'jar_exists' => $emuDiagnosis['checks']['jar_exists'] ?? false,
|
||||
'service_running' => $emuDiagnosis['checks']['service_running'] ?? false,
|
||||
'db_connected' => $emuDiagnosis['checks']['emulator_db_connected'] ?? false,
|
||||
'status' => empty($emuDiagnosis['issues']) ? 'ok' : 'issues',
|
||||
'issues' => $emuDiagnosis['issues'] ?? [],
|
||||
];
|
||||
|
||||
if (! empty($emuDiagnosis['issues'])) {
|
||||
foreach ($emuDiagnosis['issues'] as $issue) {
|
||||
$issues[] = 'Emulator: ' . $issue;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$checks['emulator'] = [
|
||||
'status' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
$issues[] = 'Emulator check failed: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
$nitroDiagnosis = $nitroService->diagnose();
|
||||
$checks['nitro'] = [
|
||||
'client_installed' => $nitroDiagnosis['checks']['client_installed'] ?? false,
|
||||
'renderer_installed' => $nitroDiagnosis['checks']['renderer_installed'] ?? false,
|
||||
'deployed' => $nitroDiagnosis['checks']['deployed'] ?? false,
|
||||
'status' => empty($nitroDiagnosis['issues']) ? 'ok' : 'issues',
|
||||
'issues' => $nitroDiagnosis['issues'] ?? [],
|
||||
];
|
||||
|
||||
if (! empty($nitroDiagnosis['issues'])) {
|
||||
foreach ($nitroDiagnosis['issues'] as $issue) {
|
||||
$issues[] = 'Nitro: ' . $issue;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$checks['nitro'] = [
|
||||
'status' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
$issues[] = 'Nitro check failed: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
$sqlDiagnosis = $emuService->diagnoseSqlUpdates();
|
||||
$checks['sql_updates'] = [
|
||||
'table_exists' => $sqlDiagnosis['table_exists'] ?? false,
|
||||
'applied' => $sqlDiagnosis['applied_count'] ?? 0,
|
||||
'pending' => $sqlDiagnosis['pending_count'] ?? 0,
|
||||
'status' => ($sqlDiagnosis['pending_count'] ?? 0) > 0 ? 'pending' : 'ok',
|
||||
];
|
||||
|
||||
if (($sqlDiagnosis['pending_count'] ?? 0) > 0) {
|
||||
$warnings[] = $sqlDiagnosis['pending_count'] . ' SQL updates pending';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$checks['sql_updates'] = [
|
||||
'status' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
$webserverCheck = Process::timeout(5)->run('which nginx || which apache2 || which httpd');
|
||||
$checks['webserver'] = [
|
||||
'installed' => $webserverCheck->successful(),
|
||||
'status' => $webserverCheck->successful() ? 'ok' : 'unknown',
|
||||
];
|
||||
|
||||
$mysqlCheck = Process::timeout(5)->run('which mysql || which mariadb');
|
||||
$checks['database'] = [
|
||||
'client_installed' => $mysqlCheck->successful(),
|
||||
'status' => $mysqlCheck->successful() ? 'ok' : 'unknown',
|
||||
];
|
||||
|
||||
$nodeCheck = Process::timeout(5)->run('which node && which yarn');
|
||||
$checks['node'] = [
|
||||
'installed' => $nodeCheck->successful(),
|
||||
'status' => $nodeCheck->successful() ? 'ok' : 'missing',
|
||||
];
|
||||
if (! $nodeCheck->successful()) {
|
||||
$warnings[] = 'Node.js/Yarn niet geïnstalleerd';
|
||||
}
|
||||
|
||||
$status = $issues === [] ? 'healthy' : ($warnings === [] ? 'warning' : 'degraded');
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'checks' => $checks,
|
||||
'issues' => $issues,
|
||||
'warnings' => $warnings,
|
||||
];
|
||||
}
|
||||
|
||||
private function displayHealthCheck(array $health): void
|
||||
{
|
||||
$status = $health['status'];
|
||||
|
||||
$statusIcon = match ($status) {
|
||||
'healthy' => '✅',
|
||||
'warning' => '⚠️',
|
||||
'degraded' => '❌',
|
||||
default => '❓',
|
||||
};
|
||||
|
||||
$this->info("{$statusIcon} Systeem Gezondheid: " . strtoupper($status));
|
||||
$this->line('═══════════════════════════════════════════════');
|
||||
|
||||
$checks = $health['checks'] ?? [];
|
||||
|
||||
$this->line('📦 Systeem:');
|
||||
$this->line(' PHP: ' . ($checks['php']['version'] ?? '?'));
|
||||
$this->line(' User: ' . ($checks['system']['user'] ?? '?'));
|
||||
|
||||
$diskStatus = $checks['disk']['status'] ?? '?';
|
||||
$diskIcon = $diskStatus === 'ok' ? '✅' : '❌';
|
||||
$this->line(" {$diskIcon} Disk: " . ($checks['disk']['percent_free'] ?? '?') . '% vrij');
|
||||
|
||||
$emuStatus = $checks['emulator']['status'] ?? '?';
|
||||
$emuIcon = $emuStatus === 'ok' ? '✅' : '⚠️';
|
||||
$this->line(" {$emuIcon} Emulator: " . ucfirst($emuStatus));
|
||||
|
||||
$nitroStatus = $checks['nitro']['status'] ?? '?';
|
||||
$nitroIcon = $nitroStatus === 'ok' ? '✅' : '⚠️';
|
||||
$this->line(" {$nitroIcon} Nitro: " . ucfirst($nitroStatus));
|
||||
|
||||
$sqlStatus = $checks['sql_updates']['status'] ?? '?';
|
||||
$sqlIcon = $sqlStatus === 'ok' ? '✅' : '⚠️';
|
||||
$this->line(" {$sqlIcon} SQL Updates: " . ucfirst($sqlStatus) . ' (' . ($checks['sql_updates']['applied'] ?? 0) . ' toegepast)');
|
||||
|
||||
$nodeStatus = $checks['node']['status'] ?? '?';
|
||||
$nodeIcon = $nodeStatus === 'ok' ? '✅' : '❌';
|
||||
$this->line(" {$nodeIcon} Node.js: " . ucfirst($nodeStatus));
|
||||
|
||||
if (! empty($health['issues'])) {
|
||||
$this->line('');
|
||||
$this->error('❌ Problemen:');
|
||||
foreach ($health['issues'] as $issue) {
|
||||
$this->line(' - ' . $issue);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($health['warnings'])) {
|
||||
$this->line('');
|
||||
$this->warn('⚠️ Waarschuwingen:');
|
||||
foreach ($health['warnings'] as $warning) {
|
||||
$this->line(' - ' . $warning);
|
||||
}
|
||||
}
|
||||
|
||||
$this->line('═══════════════════════════════════════════════');
|
||||
|
||||
if ($status === 'healthy') {
|
||||
$this->info('✅ Alles werkt correct!');
|
||||
} else {
|
||||
$this->warn('Run "php artisan system:repair" om problemen op te lossen');
|
||||
}
|
||||
}
|
||||
|
||||
private function formatBytes(?float $bytes): string
|
||||
{
|
||||
if ($bytes === null) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$i = 0;
|
||||
|
||||
while ($bytes >= 1024 && $i < count($units) - 1) {
|
||||
$bytes /= 1024;
|
||||
$i++;
|
||||
}
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$i];
|
||||
}
|
||||
}
|
||||
Executable
+213
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\AlertService;
|
||||
use App\Services\EmulatorUpdateService;
|
||||
use App\Services\NitroUpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class SystemRepairCommand extends Command
|
||||
{
|
||||
#[\Override]
|
||||
protected $signature = 'system:repair
|
||||
{--emu : Alleen emulator repareren}
|
||||
{--nitro : Alleen Nitro repareren}
|
||||
{--check : Alleen controleren, niet repareren}
|
||||
{--force : Forceer reparatie ook al is alles OK}
|
||||
{--full : Volledige reset en reinstall}
|
||||
{--nuke : Alles verwijderen en opnieuw installeren (gevaarlijk!)}';
|
||||
|
||||
#[\Override]
|
||||
protected $description = 'Automatische systeem reparatie voor emulator en Nitro';
|
||||
|
||||
private const string CACHE_KEY_LAST_REPAIR = 'system_last_repair';
|
||||
|
||||
public function handle(AlertService $alertService, EmulatorUpdateService $emuService, NitroUpdateService $nitroService): int
|
||||
{
|
||||
$this->info('🔧 System Repair Service gestart...');
|
||||
$this->line('═══════════════════════════════════════════════');
|
||||
|
||||
$checkOnly = $this->option('check');
|
||||
$force = $this->option('force');
|
||||
$full = $this->option('full');
|
||||
$emuOnly = $this->option('emu');
|
||||
$nitroOnly = $this->option('nitro');
|
||||
|
||||
$results = [
|
||||
'emulator' => null,
|
||||
'nitro' => null,
|
||||
];
|
||||
|
||||
if (! $nitroOnly) {
|
||||
$results['emulator'] = $this->repairEmulator($emuService, $checkOnly, $force, $full);
|
||||
}
|
||||
|
||||
if (! $emuOnly) {
|
||||
$results['nitro'] = $this->repairNitro($nitroService, $checkOnly, $force, $full);
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY_LAST_REPAIR, now()->toIso8601String());
|
||||
|
||||
$emuOk = $results['emulator'] === null || ($results['emulator']['success'] ?? false);
|
||||
$nitroOk = $results['nitro'] === null || ($results['nitro']['success'] ?? false);
|
||||
|
||||
$this->line('═══════════════════════════════════════════════');
|
||||
if ($emuOk && $nitroOk) {
|
||||
$this->info('✅ Alle systemen OK');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
private function ensureBaseDirectories(): void
|
||||
{
|
||||
$basePaths = [
|
||||
'/var/www',
|
||||
'/var/www/atomcms',
|
||||
storage_path('app'),
|
||||
];
|
||||
|
||||
foreach ($basePaths as $path) {
|
||||
if (! is_dir($path)) {
|
||||
@mkdir($path, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
Process::timeout(5)->run('chown -R www-data:www-data /var/www/atomcms 2>/dev/null || true');
|
||||
Process::timeout(5)->run('chmod -R 755 /var/www/atomcms 2>/dev/null || true');
|
||||
}
|
||||
|
||||
private function repairEmulator(EmulatorUpdateService $service, bool $checkOnly, bool $force, bool $full): array
|
||||
{
|
||||
$this->info('Emulator controleren...');
|
||||
|
||||
$this->ensureBaseDirectories();
|
||||
|
||||
try {
|
||||
$diagnosis = $service->diagnose();
|
||||
|
||||
if (empty($diagnosis['issues']) && ! $force && ! $full) {
|
||||
$this->line(' ✅ Emulator is OK');
|
||||
|
||||
return ['success' => true, 'status' => 'ok'];
|
||||
}
|
||||
|
||||
if ($checkOnly) {
|
||||
$this->warn(' ⚠️ Emulator problemen gevonden:');
|
||||
foreach ($diagnosis['issues'] ?? [] as $issue) {
|
||||
$this->line(' - ' . $issue);
|
||||
}
|
||||
foreach ($diagnosis['recommendations'] ?? [] as $rec) {
|
||||
$this->line(' 💡 ' . $rec);
|
||||
}
|
||||
|
||||
return ['success' => false, 'status' => 'issues_found', 'issues' => $diagnosis['issues']];
|
||||
}
|
||||
|
||||
$this->warn(' 🔧 Emulator wordt gerepareerd...');
|
||||
|
||||
if ($full) {
|
||||
$this->line(' 📦 Volledige reset...');
|
||||
$repairResult = $service->repairEmulator();
|
||||
} else {
|
||||
$repairResult = $service->repairEmulator();
|
||||
}
|
||||
|
||||
if ($repairResult['success']) {
|
||||
$this->info(' ✅ Emulator gerepareerd!');
|
||||
foreach ($repairResult['actions'] ?? [] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
|
||||
return ['success' => true, 'status' => 'repaired', 'actions' => $repairResult['actions']];
|
||||
}
|
||||
|
||||
$this->error(' ❌ Emulator repair mislukt: ' . ($repairResult['error'] ?? 'Onbekend'));
|
||||
if (! empty($repairResult['actions'])) {
|
||||
$this->line(' Uitgevoerde acties:');
|
||||
foreach ($repairResult['actions'] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => false, 'status' => 'failed', 'error' => $repairResult['error']];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ❌ Emulator exception: ' . $e->getMessage());
|
||||
Log::error('[SystemRepair] Emulator exception', ['error' => $e->getMessage()]);
|
||||
|
||||
return ['success' => false, 'status' => 'exception', 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
private function repairNitro(NitroUpdateService $service, bool $checkOnly, bool $force, bool $full): array
|
||||
{
|
||||
$this->info('Nitro controleren...');
|
||||
|
||||
$this->ensureBaseDirectories();
|
||||
|
||||
try {
|
||||
$diagnosis = $service->diagnose();
|
||||
|
||||
if (empty($diagnosis['issues']) && ! $force && ! $full) {
|
||||
$this->line(' ✅ Nitro is OK');
|
||||
|
||||
return ['success' => true, 'status' => 'ok'];
|
||||
}
|
||||
|
||||
if ($checkOnly) {
|
||||
$this->warn(' ⚠️ Nitro problemen gevonden:');
|
||||
foreach ($diagnosis['issues'] ?? [] as $issue) {
|
||||
$this->line(' - ' . $issue);
|
||||
}
|
||||
foreach ($diagnosis['recommendations'] ?? [] as $rec) {
|
||||
$this->line(' 💡 ' . $rec);
|
||||
}
|
||||
|
||||
return ['success' => false, 'status' => 'issues_found', 'issues' => $diagnosis['issues']];
|
||||
}
|
||||
|
||||
$this->warn(' 🔧 Nitro wordt gerepareerd...');
|
||||
|
||||
if ($full) {
|
||||
$this->line(' 📦 Volledige reset...');
|
||||
$repairResult = $service->updateNitro();
|
||||
} else {
|
||||
$repairResult = $service->repair();
|
||||
}
|
||||
|
||||
if ($repairResult['success']) {
|
||||
$this->info(' ✅ Nitro gerepareerd!');
|
||||
foreach ($repairResult['actions'] ?? [] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
|
||||
return ['success' => true, 'status' => 'repaired', 'actions' => $repairResult['actions']];
|
||||
}
|
||||
|
||||
$this->error(' ❌ Nitro repair mislukt: ' . ($repairResult['error'] ?? 'Onbekend'));
|
||||
if (! empty($repairResult['actions'])) {
|
||||
$this->line(' Uitgevoerde acties:');
|
||||
foreach ($repairResult['actions'] as $action) {
|
||||
$this->line(' - ' . $action);
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => false, 'status' => 'failed', 'error' => $repairResult['error']];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error(' ❌ Nitro exception: ' . $e->getMessage());
|
||||
Log::error('[SystemRepair] Nitro exception', ['error' => $e->getMessage()]);
|
||||
|
||||
return ['success' => false, 'status' => 'exception', 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Console\Commands\AutoUpdateCommand;
|
||||
use App\Console\Commands\DDoSDetectionCommand;
|
||||
use App\Console\Commands\EmulatorMonitorCommand;
|
||||
use App\Console\Commands\EmulatorUpdateCommand;
|
||||
use App\Console\Commands\FixCodeCommand;
|
||||
use App\Console\Commands\GenerateNitroConfigs;
|
||||
use App\Console\Commands\NitroUpdateCommand;
|
||||
use App\Console\Commands\SystemCheckCommand;
|
||||
use App\Console\Commands\SystemHealthCommand;
|
||||
use App\Console\Commands\SystemRepairCommand;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
#[\Override]
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
$schedule->command('radio:check-dj')->everyMinute()->withoutOverlapping();
|
||||
$schedule->command('maintenance:check-scheduled')->everyMinute()->withoutOverlapping();
|
||||
$schedule->command('monitor:emulator')->everyMinute()->withoutOverlapping();
|
||||
$schedule->command('monitor:ddos')->everyFiveMinutes()->withoutOverlapping();
|
||||
$schedule->command('update:auto')->everyMinute()->withoutOverlapping();
|
||||
$schedule->command('nitro:auto')->everyMinute()->withoutOverlapping();
|
||||
$schedule->command('system:repair')->everyTenMinutes()->withoutOverlapping();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function commands(): void
|
||||
{
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
|
||||
$this->commands[] = SystemCheckCommand::class;
|
||||
$this->commands[] = FixCodeCommand::class;
|
||||
$this->commands[] = EmulatorMonitorCommand::class;
|
||||
$this->commands[] = DDoSDetectionCommand::class;
|
||||
$this->commands[] = EmulatorUpdateCommand::class;
|
||||
$this->commands[] = AutoUpdateCommand::class;
|
||||
$this->commands[] = NitroUpdateCommand::class;
|
||||
$this->commands[] = SystemRepairCommand::class;
|
||||
$this->commands[] = SystemHealthCommand::class;
|
||||
$this->commands[] = GenerateNitroConfigs::class;
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user