Remove all auto-update functionality (commands, services, widgets, blades, translations)

This commit is contained in:
root
2026-06-03 22:54:39 +02:00
parent 1f9af5279a
commit 1f04979ffe
106 changed files with 29572 additions and 41008 deletions
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Actions\Commandocentrum; namespace App\Actions\Commandocentrum;
use App\Services\EmulatorUpdateService;
use App\Services\RconService; use App\Services\RconService;
use App\Services\SettingsService; use App\Services\SettingsService;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
@@ -13,7 +12,6 @@ class EmulatorControlAction
{ {
public function __construct( public function __construct(
private readonly SettingsService $settings, private readonly SettingsService $settings,
private readonly EmulatorUpdateService $updateService,
) {} ) {}
public function start(): array public function start(): array
@@ -59,29 +57,4 @@ class EmulatorControlAction
return ['success' => true, 'message' => 'Alert verstuurd naar alle gebruikers!']; return ['success' => true, 'message' => 'Alert verstuurd naar alle gebruikers!'];
} }
public function build(): array
{
return $this->updateService->buildFromSource();
}
public function update(): array
{
return $this->updateService->updateEmulator();
}
public function runSqlUpdates(): array
{
return $this->updateService->runSqlUpdates();
}
public function getBackups(): array
{
return $this->updateService->getBackupList();
}
public function restoreBackup(string $backupName): array
{
return $this->updateService->restoreBackup($backupName);
}
} }
@@ -1,119 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Commandocentrum;
use App\Services\SettingsService;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Process;
class NitroControlAction
{
public function __construct(
private readonly SettingsService $settings,
) {}
public function pullUpdates(string $clientPath, string $rendererPath, string $branch): array
{
$this->runGitPull($clientPath, $branch);
$this->runGitPull($rendererPath, $branch);
return ['success' => true, 'message' => 'Nitro bijgewerkt van GitHub'];
}
public function build(string $clientPath, string $rendererPath, string $branch): array
{
$this->runGitPull($clientPath, $branch);
$this->runGitPull($rendererPath, $branch);
Process::timeout(120)->run('cd ' . escapeshellarg($clientPath) . ' && sudo -u www-data npm install 2>&1');
Process::timeout(120)->run('cd ' . escapeshellarg($rendererPath) . ' && sudo -u www-data npm install 2>&1');
$exitCode = Artisan::call('build:theme');
return [
'success' => $exitCode === 0,
'message' => $exitCode === 0 ? 'Nitro build succesvol!' : 'Build gestart - controleer handmatig',
];
}
public function generateConfigs(string $siteUrl, string $webroot, string $gamedataPath): array
{
if (! filter_var($siteUrl, FILTER_VALIDATE_URL)) {
return ['success' => false, 'message' => 'Voer een geldige URL in'];
}
$existingConfigs = $this->readExistingConfigs($webroot, $gamedataPath);
$exitCode = Artisan::call('app:generate-nitro-configs', ['--site-url' => $siteUrl]);
if ($existingConfigs !== [] && $exitCode === 0) {
$this->mergeExistingConfigs($webroot, $existingConfigs);
}
$this->settings->set('nitro_last_checked', now()->toIso8601String());
return [
'success' => $exitCode === 0,
'message' => $exitCode === 0 ? 'Configs gegenereerd & bestaande instellingen behouden!' : 'Config gegenereerd (controleer handmatig)',
];
}
private function runGitPull(string $path, string $branch): void
{
Process::timeout(60)->run('cd ' . escapeshellarg($path) . ' && sudo -u www-data git pull origin ' . escapeshellarg($branch) . ' 2>&1');
}
private function readExistingConfigs(string $webroot, string $gamedataPath): array
{
$configs = [];
$files = ['renderer-config.json', 'ui-config.json', 'UITexts.json'];
foreach ($files as $file) {
$path = $webroot . '/' . $file;
$content = @file_get_contents($path);
if ($content) {
$decoded = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE) {
$configs[$file] = $decoded;
}
}
}
if ($gamedataPath !== '') {
$gamedataConfigs = [
'ExternalTexts.json' => $gamedataPath . '/config/ExternalTexts.json',
'FurnitureData.json' => $gamedataPath . '/config/FurnitureData.json',
'ProductData.json' => $gamedataPath . '/config/ProductData.json',
'FigureData.json' => $gamedataPath . '/config/FigureData.json',
];
foreach ($gamedataConfigs as $key => $path) {
$content = @file_get_contents($path);
if ($content) {
$decoded = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE) {
$configs['gamedata.' . $key] = $decoded;
}
}
}
}
return $configs;
}
private function mergeExistingConfigs(string $webroot, array $existingConfigs): void
{
if (isset($existingConfigs['renderer-config.json'])) {
$newPath = $webroot . '/renderer-config.json';
$newContent = @file_get_contents($newPath);
$newConfig = json_decode($newContent, true);
if ($newConfig && json_last_error() === JSON_ERROR_NONE) {
$merged = array_merge($existingConfigs['renderer-config.json'], $newConfig);
@file_put_contents($newPath, json_encode($merged, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
}
}
}
-150
View File
@@ -1,150 +0,0 @@
<?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());
}
}
@@ -1,172 +0,0 @@
<?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;
}
}
}
@@ -1,796 +0,0 @@
<?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");
}
}
}
-272
View File
@@ -1,272 +0,0 @@
<?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;
}
}
@@ -1,49 +0,0 @@
<?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;
}
}
}
@@ -1,251 +0,0 @@
<?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];
}
}
@@ -1,213 +0,0 @@
<?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()];
}
}
}
-15
View File
@@ -4,16 +4,10 @@ declare(strict_types=1);
namespace App\Console; namespace App\Console;
use App\Console\Commands\AutoUpdateCommand;
use App\Console\Commands\DDoSDetectionCommand; use App\Console\Commands\DDoSDetectionCommand;
use App\Console\Commands\EmulatorMonitorCommand; use App\Console\Commands\EmulatorMonitorCommand;
use App\Console\Commands\EmulatorUpdateCommand;
use App\Console\Commands\FixCodeCommand; use App\Console\Commands\FixCodeCommand;
use App\Console\Commands\GenerateNitroConfigs;
use App\Console\Commands\NitroUpdateCommand;
use App\Console\Commands\SystemCheckCommand; use App\Console\Commands\SystemCheckCommand;
use App\Console\Commands\SystemHealthCommand;
use App\Console\Commands\SystemRepairCommand;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -28,9 +22,6 @@ class Kernel extends ConsoleKernel
$schedule->command('maintenance:check-scheduled')->everyMinute()->withoutOverlapping(); $schedule->command('maintenance:check-scheduled')->everyMinute()->withoutOverlapping();
$schedule->command('monitor:emulator')->everyMinute()->withoutOverlapping(); $schedule->command('monitor:emulator')->everyMinute()->withoutOverlapping();
$schedule->command('monitor:ddos')->everyFiveMinutes()->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] #[\Override]
@@ -42,12 +33,6 @@ class Kernel extends ConsoleKernel
$this->commands[] = FixCodeCommand::class; $this->commands[] = FixCodeCommand::class;
$this->commands[] = EmulatorMonitorCommand::class; $this->commands[] = EmulatorMonitorCommand::class;
$this->commands[] = DDoSDetectionCommand::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'); require base_path('routes/console.php');
} }
File diff suppressed because it is too large Load Diff
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Filament\Pages\Monitoring; namespace App\Filament\Pages\Monitoring;
use App\Actions\Commandocentrum\EmulatorControlAction; use App\Actions\Commandocentrum\EmulatorControlAction;
use App\Actions\Commandocentrum\NitroControlAction;
use App\Enums\AlertSeverity; use App\Enums\AlertSeverity;
use App\Models\Miscellaneous\WebsitePermission; use App\Models\Miscellaneous\WebsitePermission;
use App\Models\StaffActivity; use App\Models\StaffActivity;
@@ -16,7 +15,6 @@ use App\Services\Diagnostics\DiagnosticRunner;
use App\Services\GitHubService; use App\Services\GitHubService;
use App\Services\RconService; use App\Services\RconService;
use App\Services\SettingsService; use App\Services\SettingsService;
use App\Services\UpdateHistoryService;
use BackedEnum; use BackedEnum;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
@@ -106,21 +104,6 @@ final class Commandocentrum extends Page implements HasForms
'emulator_database_username' => $this->getSetting('emulator_database_username', ''), 'emulator_database_username' => $this->getSetting('emulator_database_username', ''),
'emulator_database_password' => $this->getSetting('emulator_database_password', ''), 'emulator_database_password' => $this->getSetting('emulator_database_password', ''),
'emulator_version' => $this->getSetting('emulator_version', 'Onbekend'), 'emulator_version' => $this->getSetting('emulator_version', 'Onbekend'),
'auto_update_enabled' => $this->getSettingBool('auto_update_enabled'),
'auto_update_schedule' => $this->getSetting('auto_update_schedule', '03:00'),
'auto_update_days' => $this->getSetting('auto_update_days', '0,6'),
'nitro_client_path' => $this->getSetting('nitro_client_path', $paths['nitro_client_path']),
'nitro_renderer_path' => $this->getSetting('nitro_renderer_path', $paths['nitro_renderer_path']),
'nitro_build_path' => $this->getSetting('nitro_build_path', $paths['nitro_build_path']),
'nitro_webroot' => $this->getSetting('nitro_webroot', $paths['nitro_webroot']),
'gamedata_path' => $this->getSetting('gamedata_path', $paths['gamedata_path']),
'nitro_github_branch' => $this->getSetting('nitro_github_branch', 'main'),
'nitro_github_url' => $this->getSetting('nitro_github_url', ''),
'nitro_site_url' => $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl()),
'nitro_auto_update_configs' => $this->getSettingBool('nitro_auto_update_configs'),
'nitro_auto_update_enabled' => $this->getSettingBool('nitro_auto_update_enabled'),
'nitro_auto_update_schedule' => $this->getSetting('nitro_auto_update_schedule', '03:00'),
'nitro_auto_update_days' => $this->getSetting('nitro_auto_update_days', '0,6'),
'hotel_alert_message' => '', 'hotel_alert_message' => '',
]; ];
} }
@@ -233,92 +216,6 @@ final class Commandocentrum extends Page implements HasForms
->content(fn () => $this->renderEmulatorInfoView()), ->content(fn () => $this->renderEmulatorInfoView()),
]), ]),
Section::make(__('commandocentrum.emulator_updates'))
->description(__('commandocentrum.emulator_updates_desc'))
->icon('heroicon-o-arrow-down-circle')
->afterHeader([
Action::make('check_updates')
->label(__('commandocentrum.check_updates'))
->color('info')
->action('checkEmulatorUpdates'),
Action::make('build_emulator')
->label('🔨 ' . __('commandocentrum.build'))
->color('success')
->action('buildEmulator'),
Action::make('run_sql')
->label(__('commandocentrum.sql_updates'))
->color('purple')
->action('runSqlUpdates'),
Action::make('save_emulator')
->label(__('commandocentrum.save'))
->color('primary')
->action('saveEmulator'),
])
->schema([
Placeholder::make('emulator_settings')
->label('')
->content(fn () => $this->renderEmulatorSettingsView()),
]),
Section::make(__('commandocentrum.emulator_backups'))
->description(__('commandocentrum.emulator_backups_desc'))
->icon('heroicon-s-archive-box')
->schema([
Placeholder::make('backups_list')
->label('')
->content(fn () => $this->renderBackupsListView()),
]),
Section::make(__('commandocentrum.nitro_client'))
->description(__('commandocentrum.nitro_client_desc'))
->icon('heroicon-o-cloud-arrow-down')
->afterHeader([
Action::make('detect_paths')
->label('🔍 ' . __('commandocentrum.auto_detect'))
->color('success')
->action('detectAndSavePaths'),
Action::make('check_nitro')
->label(__('commandocentrum.check'))
->color('info')
->action('checkNitroUpdates'),
Action::make('build_nitro')
->label(__('commandocentrum.build'))
->color('pink')
->action('buildNitro'),
Action::make('generate_configs')
->label(__('commandocentrum.generate_configs'))
->color('indigo')
->action('generateNitroConfigs'),
Action::make('save_nitro')
->label(__('commandocentrum.save'))
->color('primary')
->action('saveNitro'),
])
->schema([
Placeholder::make('nitro_settings')
->label('')
->content(fn () => $this->renderNitroSettingsView()),
]),
Section::make(__('commandocentrum.auto_updates'))
->description(__('commandocentrum.auto_updates_desc'))
->icon('heroicon-o-clock')
->columns(2)
->afterHeader([
Action::make('save_auto')
->label(__('commandocentrum.save'))
->color('primary')
->action('saveAutoUpdate'),
])
->schema([
Toggle::make('auto_update_enabled')
->label(__('commandocentrum.enable_auto_updates')),
TextInput::make('auto_update_schedule')
->label(__('commandocentrum.schedule')),
TextInput::make('auto_update_days')
->label(__('commandocentrum.days')),
]),
Section::make(__('commandocentrum.clothing_sync')) Section::make(__('commandocentrum.clothing_sync'))
->description(__('commandocentrum.clothing_sync_desc')) ->description(__('commandocentrum.clothing_sync_desc'))
->icon('heroicon-o-user') ->icon('heroicon-o-user')
@@ -367,15 +264,6 @@ final class Commandocentrum extends Page implements HasForms
->helperText(__('commandocentrum.discord_ranks_helper')), ->helperText(__('commandocentrum.discord_ranks_helper')),
]), ]),
Section::make(__('commandocentrum.update_history'))
->description(__('commandocentrum.update_history_desc'))
->icon('heroicon-o-clock')
->schema([
Placeholder::make('history')
->label('')
->content(fn () => $this->renderUpdateHistoryView()),
]),
Section::make(__('commandocentrum.social_login')) Section::make(__('commandocentrum.social_login'))
->description(__('commandocentrum.social_login_desc')) ->description(__('commandocentrum.social_login_desc'))
->icon('heroicon-o-user-circle') ->icon('heroicon-o-user-circle')
@@ -491,100 +379,6 @@ final class Commandocentrum extends Page implements HasForms
]); ]);
} }
private function renderEmulatorSettingsView(): View
{
return view('filament.components.commandocentrum.emulator-settings', [
'emulatorBranchesHtml' => $this->getEmulatorBranchesHtml(),
'emulatorStatusHtml' => $this->renderEmulatorStatusView()->render(),
]);
}
private function renderEmulatorStatusView(): View
{
$serviceName = $this->getSetting('emulator_service_name', 'arcturus');
$jarPath = $this->getSetting('emulator_jar_path', '/var/www/Emulator');
$sourcePath = $this->getSetting('emulator_source_path', '/var/www/emulator-source');
$githubUrl = $this->getSetting('emulator_github_url', '');
$branch = $this->getSetting('emulator_github_branch', 'main');
$jarExists = $this->fileExists($jarPath);
$sourceExists = $this->fileExists($sourcePath);
$sourceCommit = $this->getGitCommit($sourcePath);
$remoteVersion = $githubUrl !== '' && $githubUrl !== '0' ? $this->getRemoteCommit($githubUrl, $branch) : 'N/A';
$canBuild = false;
$checkDirs = [
$sourcePath,
$sourcePath . '/Emulator',
$sourcePath . '/Emulator/Emulator',
$sourcePath . '/emulator',
$sourcePath . '/emulator/emulator',
];
foreach ($checkDirs as $dir) {
$check = $this->runCommand('test -f ' . escapeshellarg($dir . '/pom.xml') . ' && echo yes');
if ($check && trim($check) === 'yes') {
$canBuild = true;
break;
}
}
return view('filament.components.commandocentrum.emulator-status', [
'emulatorOnline' => $this->getEmulatorStatusText() === 'Online',
'jarExists' => $jarExists,
'serviceName' => $serviceName,
'sourceCommit' => $sourceCommit,
'remoteVersion' => $remoteVersion,
'canBuild' => $canBuild,
'jarPath' => $jarPath,
'sourcePath' => $sourcePath,
]);
}
private function renderNitroSettingsView(): View
{
return view('filament.components.commandocentrum.nitro-settings', [
'nitroBranchesHtml' => $this->getNitroBranchesHtml(),
'nitroStatusHtml' => $this->renderNitroStatusView()->render(),
]);
}
private function renderNitroStatusView(): View
{
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
$webroot = $this->getSetting('nitro_webroot', '/var/www/Client');
$clientGithubUrl = $this->getSetting('nitro_github_url', '');
$rendererGithubUrl = $this->getSetting('nitro_renderer_github_url', 'https://github.com/duckietm/Nitro_Render_V3');
$clientCommit = $this->getGitCommit($clientPath);
$rendererCommit = $this->getGitCommit($rendererPath);
$clientRemote = $clientGithubUrl !== '' && $clientGithubUrl !== '0' ? $this->getRemoteCommit($clientGithubUrl, $this->getSetting('nitro_github_branch', 'main')) : 'N/A';
$rendererRemote = $rendererGithubUrl !== '' && $rendererGithubUrl !== '0' ? $this->getRemoteCommit($rendererGithubUrl, $this->getSetting('nitro_renderer_github_branch', 'main')) : 'N/A';
return view('filament.components.commandocentrum.nitro-status', [
'clientExists' => $this->checkPathExists($clientPath),
'rendererExists' => $this->checkPathExists($rendererPath),
'webrootExists' => $this->checkPathExists($webroot),
'clientCommit' => $clientCommit,
'rendererCommit' => $rendererCommit,
'clientRemote' => $clientRemote,
'rendererRemote' => $rendererRemote,
]);
}
private function renderBackupsListView(): View
{
try {
$backups = app(EmulatorControlAction::class)->getBackups();
} catch (Exception) {
$backups = [];
}
return view('filament.components.commandocentrum.backups-list', [
'backups' => $backups,
]);
}
private function renderClothingStatusView(): View private function renderClothingStatusView(): View
{ {
try { try {
@@ -614,19 +408,6 @@ final class Commandocentrum extends Page implements HasForms
]); ]);
} }
private function renderUpdateHistoryView(): View
{
try {
$history = app(UpdateHistoryService::class)->getRecent(10);
} catch (Exception) {
$history = [];
}
return view('filament.components.commandocentrum.update-history', [
'history' => $history,
]);
}
private function getSetting(string $key, string $default = ''): string private function getSetting(string $key, string $default = ''): string
{ {
try { try {
@@ -878,33 +659,6 @@ final class Commandocentrum extends Page implements HasForms
} }
} }
public function checkEmulatorUpdates(): void
{
$result = app(EmulatorControlAction::class)->update();
$this->notify($result['success'] ?? false ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'] ?? $result['error'] ?? __('commandocentrum.unknown'), ($result['success'] ?? false) ? 'success' : 'danger');
Cache::forget('all_updates_check');
$this->fillForm();
}
public function buildEmulator(): void
{
$result = app(EmulatorControlAction::class)->build();
$this->notify($result['success'] ?? false ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'] ?? $result['error'] ?? __('commandocentrum.unknown'), ($result['success'] ?? false) ? 'success' : 'danger');
}
public function runSqlUpdates(): void
{
$result = app(EmulatorControlAction::class)->runSqlUpdates();
$this->notify($result['success'] ?? false ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'] ?? __('commandocentrum.unknown'), ($result['success'] ?? false) ? 'success' : 'danger');
}
public function restoreBackup(string $backupName): void
{
$result = app(EmulatorControlAction::class)->restoreBackup($backupName);
$this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'] ?? $result['error'] ?? __('commandocentrum.unknown'), $result['success'] ? 'success' : 'danger');
$this->fillForm();
}
public function saveEmulator(): void public function saveEmulator(): void
{ {
try { try {
@@ -924,38 +678,6 @@ final class Commandocentrum extends Page implements HasForms
} }
} }
public function checkNitroUpdates(): void
{
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
$branch = $this->getSetting('nitro_github_branch', 'main');
$result = app(NitroControlAction::class)->pullUpdates($clientPath, $rendererPath, $branch);
$this->notify(__('commandocentrum.success'), $result['message'], 'success');
$this->fillForm();
}
public function buildNitro(): void
{
$clientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client');
$rendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer');
$branch = $this->getSetting('nitro_github_branch', 'main');
$result = app(NitroControlAction::class)->build($clientPath, $rendererPath, $branch);
$this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.warning'), $result['message'], $result['success'] ? 'success' : 'warning');
}
public function generateNitroConfigs(): void
{
$siteUrl = $this->getSetting('nitro_site_url', $this->getCurrentSiteUrl());
$webroot = $this->getSetting('nitro_webroot', '/var/www/Client');
$gamedataPath = $this->getSetting('gamedata_path', '/var/www/Gamedata');
$result = app(NitroControlAction::class)->generateConfigs($siteUrl, $webroot, $gamedataPath);
$this->notify($result['success'] ? __('commandocentrum.success') : __('commandocentrum.error'), $result['message'], $result['success'] ? 'success' : 'danger');
$this->fillForm();
}
public function syncClothing(): void public function syncClothing(): void
{ {
try { try {
@@ -972,58 +694,6 @@ final class Commandocentrum extends Page implements HasForms
} }
} }
public function detectAndSavePaths(): void
{
try {
$paths = $this->autoDetectPaths();
$settings = app(SettingsService::class);
$settings->set('nitro_client_path', $paths['nitro_client_path']);
$settings->set('nitro_renderer_path', $paths['nitro_renderer_path']);
$settings->set('nitro_build_path', $paths['nitro_build_path']);
$settings->set('nitro_webroot', $paths['nitro_webroot']);
$settings->set('gamedata_path', $paths['gamedata_path']);
$settings->set('emulator_jar_path', $paths['emulator_jar_path']);
$settings->set('emulator_source_path', $paths['emulator_source_path']);
$this->fillForm();
$this->notify(__('commandocentrum.success'), __('commandocentrum.paths_detected'), 'success');
} catch (Exception $e) {
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
}
}
public function saveNitro(): void
{
try {
$settings = app(SettingsService::class);
$settings->set('nitro_client_path', $this->data['nitro_client_path'] ?? '/var/www/atomcms/nitro-client');
$settings->set('nitro_renderer_path', $this->data['nitro_renderer_path'] ?? '/var/www/atomcms/nitro-renderer');
$settings->set('nitro_build_path', $this->data['nitro_build_path'] ?? '/var/www/atomcms/nitro-client/dist');
$settings->set('nitro_webroot', $this->data['nitro_webroot'] ?? '/var/www/Client');
$settings->set('gamedata_path', $this->data['gamedata_path'] ?? '/var/www/Gamedata');
$settings->set('nitro_github_url', $this->data['nitro_github_url'] ?? '');
$settings->set('nitro_github_branch', $this->data['nitro_github_branch'] ?? 'main');
$settings->set('nitro_site_url', $this->data['nitro_site_url'] ?? '');
$this->notify(__('commandocentrum.success'), __('commandocentrum.nitro_settings_saved'), 'success');
} catch (Exception $e) {
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
}
}
public function saveAutoUpdate(): void
{
try {
$settings = app(SettingsService::class);
$settings->set('auto_update_enabled', ($this->data['auto_update_enabled'] ?? false) ? '1' : '0');
$settings->set('auto_update_schedule', $this->data['auto_update_schedule'] ?? '03:00');
$settings->set('auto_update_days', $this->data['auto_update_days'] ?? '0,6');
$this->notify(__('commandocentrum.success'), __('commandocentrum.auto_update_saved'), 'success');
} catch (Exception $e) {
$this->notify(__('commandocentrum.error'), $e->getMessage(), 'danger');
}
}
public function saveAlerts(): void public function saveAlerts(): void
{ {
try { try {
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace App\Filament\Resources\Miscellaneous\AlertLogResource; namespace App\Filament\Resources\Miscellaneous\AlertLogResource;
use App\Models\Miscellaneous\AlertLog; use App\Models\Miscellaneous\AlertLog;
use App\Services\EmulatorUpdateService;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Resource; use Filament\Resources\Resource;
@@ -136,14 +135,12 @@ class AlertLogResource extends Resource
->icon('heroicon-o-trash') ->icon('heroicon-o-trash')
->color('gray') ->color('gray')
->action(function () { ->action(function () {
$updateService = new EmulatorUpdateService;
$result = $updateService->clearAllLogs();
Cache::flush(); Cache::flush();
AlertLog::truncate(); AlertLog::truncate();
Notification::make() Notification::make()
->success() ->success()
->title('🗑️ Alle Logs Geleegd!') ->title('🗑️ Alle Logs Geleegd!')
->body($result['message']) ->body('Alle logs zijn gewist.')
->send(); ->send();
}) })
->requiresConfirmation(), ->requiresConfirmation(),
@@ -1,343 +0,0 @@
<?php
namespace App\Filament\Widgets;
use App\Services\EmulatorUpdateService;
use App\Services\NitroUpdateService;
use App\Services\RconService;
use Filament\Notifications\Notification;
use Filament\Widgets\Widget;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class UpdateCheckerWidget extends Widget
{
#[\Override]
protected string $view = 'filament.widgets.update-checker';
#[\Override]
protected int|string|array $columnSpan = 'full';
#[\Override]
protected static ?int $sort = 0;
public ?string $emulatorVersion = null;
public ?string $latestEmulatorVersion = null;
public bool $emulatorUpdate = false;
public ?string $nitroVersion = null;
public ?string $latestNitroVersion = null;
public bool $nitroUpdate = false;
public int $onlineUsers = 0;
public string $dbSize = '0 MB';
public bool $hasAnyUpdate = false;
public int $sqlApplied = 0;
public int $sqlPending = 0;
public bool $sqlTableExists = false;
public function mount(): void
{
$this->emulatorVersion = setting('emulator_version', '?');
$this->nitroVersion = setting('nitro_client_version', '?');
try {
$this->onlineUsers = (int) DB::connection('mysql')->table('users')->where('online', '1')->count();
$sizeResult = DB::connection('mysql')->select('SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) as db_size FROM information_schema.tables WHERE table_schema = DATABASE()');
$this->dbSize = ($sizeResult[0]->db_size ?? '0') . ' MB';
} catch (\Exception) {
$this->dbSize = '? MB';
}
$this->loadSqlStatus();
$cached = Cache::get('all_updates_check');
if ($cached !== null) {
$this->emulatorUpdate = $cached['emulator'] ?? false;
$this->latestEmulatorVersion = $cached['emulator_version'] ?? null;
$this->nitroUpdate = $cached['nitro'] ?? false;
$this->latestNitroVersion = $cached['nitro_version'] ?? null;
$this->hasAnyUpdate = $this->emulatorUpdate || $this->nitroUpdate;
return;
}
$this->performCheck();
}
private function loadSqlStatus(): void
{
try {
$updateService = new EmulatorUpdateService;
$sqlDiagnosis = $updateService->diagnoseSqlUpdates();
$this->sqlTableExists = $sqlDiagnosis['table_exists'] ?? false;
$this->sqlApplied = $sqlDiagnosis['applied_count'] ?? 0;
$this->sqlPending = $sqlDiagnosis['pending_count'] ?? 0;
} catch (\Exception $e) {
Log::error('[UpdateChecker] SQL status failed: ' . $e->getMessage());
}
}
public function performCheck(): void
{
try {
$updateService = new EmulatorUpdateService;
$check = $updateService->checkForUpdates();
$this->emulatorUpdate = $check['update_available'] ?? false;
$this->latestEmulatorVersion = $check['latest_version'] ?? null;
} catch (\Exception $e) {
Log::error('[UpdateChecker] Emulator check failed: ' . $e->getMessage());
}
try {
$nitroService = new NitroUpdateService;
$nitroCheck = $nitroService->checkForUpdates();
$this->nitroUpdate = $nitroCheck['has_updates'] ?? false;
if ($this->nitroUpdate) {
$parts = [];
if ($nitroCheck['client_update'] ?? false) {
$parts[] = 'Client';
}
if ($nitroCheck['renderer_update'] ?? false) {
$parts[] = 'Renderer';
}
$this->latestNitroVersion = implode(' + ', $parts);
}
} catch (\Exception $e) {
Log::error('[UpdateChecker] Nitro check failed: ' . $e->getMessage());
}
$this->hasAnyUpdate = $this->emulatorUpdate || $this->nitroUpdate;
Cache::put('all_updates_check', [
'emulator' => $this->emulatorUpdate,
'emulator_version' => $this->latestEmulatorVersion,
'nitro' => $this->nitroUpdate,
'nitro_version' => $this->latestNitroVersion,
], now()->addMinutes(15));
}
public function forceCheck(): void
{
try {
Cache::forget('all_updates_check');
$updateService = new EmulatorUpdateService;
$updateService->resetInstalledDate();
$this->performCheck();
if ($this->hasAnyUpdate) {
Notification::make()->success()->title('Updates Gevonden!')->body('Klik op "Alles Updaten" om te installeren')->send();
} else {
Notification::make()->info()->title('Geen Updates')->body('Alles is al up-to-date')->send();
}
} catch (\Exception $e) {
Notification::make()->danger()->title('Fout')->body($e->getMessage())->send();
}
}
public function repairSystem(): void
{
try {
Cache::forget('all_updates_check');
$messages = [];
$errors = [];
try {
$emuService = new EmulatorUpdateService;
$repairResult = $emuService->repairEmulator();
if ($repairResult['success']) {
$messages[] = '🖥️ Emulator: ' . ($repairResult['message'] ?? 'Gerepareerd');
} else {
$errors[] = 'Emulator: ' . ($repairResult['error'] ?? 'Onbekende fout');
}
} catch (\Exception $e) {
$errors[] = 'Emulator: ' . $e->getMessage();
}
try {
$nitroService = new NitroUpdateService;
$repairResult = $nitroService->repair();
if ($repairResult['success']) {
$messages[] = '🎮 Nitro: ' . ($repairResult['message'] ?? 'Gerepareerd');
} else {
$errors[] = 'Nitro: ' . ($repairResult['error'] ?? 'Onbekende fout');
}
} catch (\Exception $e) {
$errors[] = 'Nitro: ' . $e->getMessage();
}
if ($messages !== []) {
Notification::make()->success()->title('Reparatie Voltooid!')->body(implode(' | ', $messages))->send();
}
if ($errors !== []) {
Notification::make()->danger()->title('Reparatie Fout')->body(implode(' | ', $errors))->send();
}
Cache::forget('all_updates_check');
$this->mount();
} catch (\Exception $e) {
Notification::make()->danger()->title('Fout')->body($e->getMessage())->send();
}
}
public function diagnoseSystem(): void
{
try {
$emuService = new EmulatorUpdateService;
$nitroService = new NitroUpdateService;
$emuDiagnosis = $emuService->diagnose();
$nitroDiagnosis = $nitroService->diagnose();
$issues = array_merge(
$emuDiagnosis['issues'] ?? [],
$nitroDiagnosis['issues'] ?? [],
);
$recommendations = array_merge(
$emuDiagnosis['recommendations'] ?? [],
$nitroDiagnosis['recommendations'] ?? [],
);
if ($issues === []) {
Notification::make()->info()->title('Diagnose')->body('Geen problemen gevonden')->send();
} else {
$body = 'Problemen: ' . implode(', ', $issues);
if ($recommendations !== []) {
$body .= "\n\nAanbevelingen: " . implode(', ', $recommendations);
}
Notification::make()->warning()->title('Diagnose Resultaat')->body($body)->send();
}
} catch (\Exception $e) {
Notification::make()->danger()->title('Fout')->body($e->getMessage())->send();
}
}
public function updateAll(): void
{
try {
Cache::forget('all_updates_check');
Cache::forget('website_settings');
$messages = [];
$errors = [];
$updateService = new EmulatorUpdateService;
try {
$result = $updateService->updateEmulator();
if ($result['success']) {
$messages[] = '🖥️ ' . ($result['message'] ?? 'Emulator geüpdatet');
} else {
$error = $result['error'] ?? '';
if (str_contains($error, 'al up-to-date')) {
$messages[] = '🖥️ Emulator al up-to-date';
} else {
$errors[] = 'Emulator: ' . $error;
}
}
} catch (\Exception $e) {
$errors[] = 'Emulator: ' . $e->getMessage();
}
try {
$nitroService = new NitroUpdateService;
$nitroCheck = $nitroService->checkForUpdates();
if ($nitroCheck['has_updates'] ?? false) {
$result = $nitroService->updateNitro();
if ($result['success']) {
$parts = [];
if ($result['renderer_updated'] ?? false) {
$parts[] = 'Renderer';
}
if ($result['client_updated'] ?? false) {
$parts[] = 'Client';
}
if ($result['built'] ?? false) {
$parts[] = 'Build';
}
if ($result['deployed'] ?? false) {
$parts[] = 'Deploy';
}
if ($parts !== []) {
$messages[] = '🎮 Nitro: ' . implode(', ', $parts);
}
} elseif (! empty($result['errors'] ?? [])) {
$errors = array_merge($errors, $result['errors']);
}
} else {
$messages[] = '🎮 Nitro al up-to-date';
}
} catch (\Exception $e) {
$errors[] = 'Nitro: ' . $e->getMessage();
}
try {
$sqlResult = $updateService->runSqlUpdates();
if ($sqlResult['sql_updated'] ?? false) {
$count = count($sqlResult['files_run'] ?? []);
if ($count > 0) {
$messages[] = "📊 {$count} SQL updates";
}
}
} catch (\Exception $e) {
$errors[] = 'SQL: ' . $e->getMessage();
}
try {
$updateService->restartEmulator();
$messages[] = '🔄 Emulator herstart';
} catch (\Exception $e) {
Log::error('[UpdateChecker] Restart failed: ' . $e->getMessage());
}
try {
$rcon = new RconService;
$rcon->sendCommand('updatecatalog');
$rcon->sendCommand('updatewordfilter');
$messages[] = '📚 Catalogus + filter vernieuwd';
} catch (\Exception $e) {
Log::error('[UpdateChecker] RCON failed: ' . $e->getMessage());
}
if ($messages !== []) {
Notification::make()->success()->title('Updates Voltooid!')->body(implode(' | ', $messages))->send();
}
if ($errors !== []) {
Notification::make()->danger()->title('Sommige Updates Mislukt')->body(implode(' | ', $errors))->send();
}
if ($messages === [] && $errors === []) {
Notification::make()->info()->title('Geen Updates')->body('Alles was al up-to-date')->send();
}
Cache::forget('all_updates_check');
$this->mount();
} catch (\Exception $e) {
Notification::make()->danger()->title('Fout')->body($e->getMessage())->send();
}
}
#[\Override]
public static function canView(): bool
{
return true;
}
}
@@ -1,272 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Services\Emulator;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
class EmulatorBuildService
{
use EmulatorConfiguration;
public function __construct()
{
$this->loadConfiguration();
}
public function buildFromSource(bool $force = false): array
{
$repo = $this->sourceRepo ?: $this->githubRepo;
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
if (! $repo) {
return ['success' => false, 'error' => 'Geen source repo geconfigureerd'];
}
$sourcePath = $this->emulatorSourcePath;
$serviceName = $this->emulatorService;
Log::info('[EmulatorBuild] Starting source build', [
'repo' => $repo,
'branch' => $branch,
'path' => $sourcePath,
]);
$this->ensureJavaInstalled();
Process::timeout(10)->run('mkdir -p ' . escapeshellarg(dirname((string) $sourcePath)));
Process::timeout(10)->run('mkdir -p ' . escapeshellarg((string) $this->jarPath));
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $sourcePath)));
$maxRetries = 2;
$lastError = '';
for ($retry = 0; $retry <= $maxRetries; $retry++) {
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg(dirname((string) $sourcePath)) . ' 2>/dev/null || true');
Process::timeout(10)->run('chown -R www-data:www-data ' . escapeshellarg((string) $sourcePath) . ' 2>/dev/null || true');
if ($retry > 0) {
Log::info('[EmulatorBuild] Retry attempt', ['attempt' => $retry]);
Process::timeout(30)->run('rm -rf ' . escapeshellarg((string) $sourcePath));
}
try {
$existsCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . " ] && echo 'exists'");
$sourceExists = $existsCheck->successful() && trim($existsCheck->output()) === 'exists';
$gitCheck = Process::timeout(5)->run('[ -d ' . escapeshellarg((string) $sourcePath) . "/.git ] && echo 'git'");
$isGitRepo = $gitCheck->successful() && trim($gitCheck->output()) === 'git';
if ($sourceExists && $isGitRepo) {
Log::info('[EmulatorBuild] Pulling latest changes');
$commands = [
'cd ' . escapeshellarg((string) $sourcePath) . ' && git fetch origin',
'cd ' . escapeshellarg((string) $sourcePath) . ' && git checkout ' . escapeshellarg($branch),
'cd ' . escapeshellarg((string) $sourcePath) . ' && git pull origin ' . escapeshellarg($branch),
];
} else {
Log::info('[EmulatorBuild] Cloning repository');
$commands = [
'sudo rm -rf ' . escapeshellarg((string) $sourcePath) . ' 2>/dev/null || rm -rf ' . escapeshellarg((string) $sourcePath),
'mkdir -p ' . escapeshellarg(dirname((string) $sourcePath)),
'git clone --branch ' . escapeshellarg($branch) . ' --depth 1 https://github.com/' . escapeshellarg($repo) . '.git ' . escapeshellarg((string) $sourcePath),
'chown -R www-data:www-data ' . escapeshellarg((string) $sourcePath),
];
}
$command = implode(' && ', $commands);
$result = Process::timeout(300)->run($command);
if ($result->failed()) {
$lastError = 'Git clone/pull failed: ' . substr($result->errorOutput(), 0, 300);
Log::warning('[EmulatorBuild] Git operation failed', ['error' => $lastError, 'attempt' => $retry]);
continue;
}
$buildCommands = $this->getBuildCommands($sourcePath);
Log::info('[EmulatorBuild] Running build', ['command' => $buildCommands]);
$buildResult = Process::timeout(600)->run('cd ' . escapeshellarg((string) $sourcePath) . ' && ' . $buildCommands);
$hasSignalError = str_contains($buildResult->errorOutput(), 'signal') || str_contains($buildResult->output(), 'signal');
if ($hasSignalError) {
Log::warning('[EmulatorBuild] Build process received signal, checking if JAR was built anyway');
}
$jarPath = $this->findBuiltJar($sourcePath);
if ($jarPath) {
Log::info('[EmulatorBuild] JAR found despite build status', ['jar' => $jarPath]);
} elseif ($buildResult->failed() && ! $hasSignalError) {
$lastError = 'Build failed: ' . substr($buildResult->errorOutput(), 0, 500);
Log::warning('[EmulatorBuild] Build failed', ['error' => $lastError, 'attempt' => $retry]);
if ($retry < $maxRetries) {
$cleanCommands = [
'cd ' . escapeshellarg((string) $sourcePath) . ' && mvn clean 2>/dev/null || ./gradlew clean 2>/dev/null || true',
];
Process::timeout(60)->run(implode(' && ', $cleanCommands));
continue;
}
continue;
}
if (! $jarPath) {
$lastError = 'Build succeeded but JAR not found. Check build output.';
Log::warning('[EmulatorBuild] JAR not found', ['attempt' => $retry]);
continue;
}
return $this->deployJar($jarPath, $serviceName);
} catch (\Exception $e) {
$lastError = $e->getMessage();
Log::error('[EmulatorBuild] Source build exception', ['error' => $lastError, 'attempt' => $retry]);
}
}
return [
'success' => false,
'error' => 'Build mislukt na ' . ($maxRetries + 1) . ' pogingen. Laatste fout: ' . $lastError,
];
}
public function getBuildCommands(string $sourcePath): string
{
$pomPath = $sourcePath;
$pomCheck = Process::timeout(5)->run("[ -f {$pomPath}/pom.xml ] && echo 'pom'");
if (! $pomCheck->successful() || trim($pomCheck->output()) !== 'pom') {
$pomPath = $sourcePath . '/Emulator';
$pomCheck = Process::timeout(5)->run("[ -f {$pomPath}/pom.xml ] && echo 'pom'");
}
$gradlewPath = $sourcePath . '/gradlew';
$gradlewCheck = Process::timeout(5)->run("[ -f {$gradlewPath} ] && echo 'gradlew'");
$gradlePath = $sourcePath . '/build.gradle';
$gradleCheck = Process::timeout(5)->run("[ -f {$gradlePath} ] && echo 'gradle'");
if ($pomCheck->successful() && trim($pomCheck->output()) === 'pom') {
return "cd {$pomPath} && mvn clean package -DskipTests 2>&1";
}
if ($gradlewCheck->successful() && trim($gradlewCheck->output()) === 'gradlew') {
return "cd {$sourcePath} && chmod +x gradlew && ./gradlew clean build -x test 2>&1";
}
if ($gradleCheck->successful() && trim($gradleCheck->output()) === 'gradle') {
return "cd {$sourcePath} && gradle clean build -x test 2>&1";
}
return "cd {$sourcePath} && ls -la";
}
public function findBuiltJar(string $sourcePath): ?string
{
$patterns = [
$sourcePath . '/target/*.jar',
$sourcePath . '/build/libs/*.jar',
$sourcePath . '/*/*.jar',
$sourcePath . '/*.jar',
$sourcePath . '/Emulator/target/*.jar',
$sourcePath . '/Emulator/build/libs/*.jar',
$sourcePath . '/Emulator/*/*.jar',
];
foreach ($patterns as $pattern) {
$result = Process::timeout(10)->run('ls -t ' . $pattern . ' 2>/dev/null | head -1');
if ($result->successful()) {
$jarPath = trim($result->output());
if ($jarPath !== '' && $jarPath !== '0' && str_contains($jarPath, '.jar')) {
return $jarPath;
}
}
}
return null;
}
private function deployJar(string $jarPath, string $serviceName): array
{
$jarName = basename($jarPath);
$version = $this->extractVersionFromFilename($jarName);
if ($version === '' || $version === '0') {
$version = date('Y.m.d');
}
$deployCommands = [
'mkdir -p ' . escapeshellarg((string) $this->jarPath),
'chown -R www-data:www-data ' . escapeshellarg((string) $this->jarPath),
'if ls ' . escapeshellarg((string) $this->jarPath) . '/*.jar 1>/dev/null 2>&1; then mkdir -p ' . escapeshellarg((string) $this->jarPath) . '/backup && mv ' . escapeshellarg((string) $this->jarPath) . '/*.jar ' . escapeshellarg((string) $this->jarPath) . '/backup/; fi',
'cp -f ' . escapeshellarg($jarPath) . ' ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName),
'chown www-data:www-data ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName),
'chmod 755 ' . escapeshellarg((string) $this->jarPath) . '/' . escapeshellarg($jarName),
'ls -la ' . escapeshellarg((string) $this->jarPath) . '/',
'systemctl restart ' . escapeshellarg((string) $serviceName) . ' 2>&1 || service ' . escapeshellarg((string) $serviceName) . ' restart 2>&1 || true',
];
$deployCommand = implode(' && ', $deployCommands);
Log::info('[EmulatorBuild] Deploying jar', [
'source' => $jarPath,
'destination' => "{$this->jarPath}/{$jarName}",
]);
$deployResult = Process::timeout(60)->run($deployCommand);
if (! $deployResult->successful()) {
return [
'success' => false,
'error' => 'Deploy failed: ' . substr($deployResult->errorOutput(), 0, 300),
];
}
$sourceService = new EmulatorSourceService;
$sourceInfo = $sourceService->checkForUpdates();
$installedDate = $sourceInfo['latest_timestamp'] ?? time();
$this->settings->set('emulator_version', $version);
$this->settings->set('emulator_jar_installed_date', (string) $installedDate);
$this->settings->set('emulator_source_commit', $sourceInfo['latest_sha'] ?? 'unknown');
$this->settings->set('emulator_source_date', (string) $installedDate);
$currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
$this->settings->set('emulator_installed_branch', $currentBranch);
$sqlService = new EmulatorSqlService;
$sqlService->runUpdates();
Log::info('[EmulatorBuild] Source build successful');
return [
'success' => true,
'version' => $version,
'jar' => $jarName,
'built' => true,
'message' => "✅ Emulator vanaf source gebouwd!\n📦 {$jarName}\n🔄 Service herstart",
];
}
private function ensureJavaInstalled(): void
{
$javaCheck = Process::timeout(5)->run('java -version 2>&1');
if (! $javaCheck->successful()) {
Log::warning('[EmulatorBuild] Java not found, attempting to install');
Process::timeout(180)->run('apt-get update && apt-get install -y default-jdk 2>&1');
}
$mavenCheck = Process::timeout(5)->run('which mvn');
if (! $mavenCheck->successful()) {
Log::warning('[EmulatorBuild] Maven not found, attempting to install');
Process::timeout(180)->run('apt-get install -y maven 2>&1');
}
}
}
@@ -1,509 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Services\Emulator;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
class EmulatorJarService
{
use EmulatorConfiguration;
public function __construct()
{
$this->loadConfiguration();
}
public function isConfigured(): bool
{
return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true);
}
public function checkForUpdates(): array
{
if (! $this->isConfigured()) {
return [
'update_available' => false,
'error' => 'Configureer een GitHub URL of directe .jar URL',
];
}
$sourceService = new EmulatorSourceService;
$sourceInfo = $sourceService->checkForUpdates();
$hasSourceUpdates = $sourceInfo && $sourceInfo['has_update'];
if (! in_array($this->jarDirectUrl, [null, '', '0'], true)) {
return $this->checkDirectUrlUpdates($sourceInfo, $hasSourceUpdates);
}
return $this->checkGitHubFolderUpdates($sourceInfo, $hasSourceUpdates);
}
public function performUpdate(array $check): array
{
$jarUrl = $check['jar_url'];
$jarName = $check['jar_name'];
$version = $check['latest_version'];
$serviceName = $this->emulatorService;
$tempDir = '/tmp/emulator-update-' . Str::random(8);
$tempJar = $tempDir . '/' . $jarName;
$updateScript = $this->buildUpdateScript($jarUrl, $jarName, $tempDir, $tempJar, $serviceName);
$scriptPath = '/tmp/emulator_update_' . uniqid() . '.sh';
file_put_contents($scriptPath, $updateScript);
chmod($scriptPath, 0755);
Log::info('[EmulatorJar] Starting update', [
'version' => $version,
'jar' => $jarName,
'url' => $jarUrl,
]);
try {
$result = Process::timeout(600)->run('bash ' . $scriptPath . ' 2>&1');
@unlink($scriptPath);
if ($result->exitCode() !== 0) {
Log::error('[EmulatorJar] Update failed', [
'output' => $result->output(),
'error' => $result->errorOutput(),
]);
return [
'success' => false,
'error' => 'Update mislukt: ' . substr($result->output(), 0, 300),
];
}
$this->storeUpdateInfo($version, $check);
Log::info('[EmulatorJar] Update successful');
return [
'success' => true,
'version' => $version,
'jar' => $jarName,
'message' => "✅ Emulator geüpdatet naar v{$version}!\n📦 {$jarName}\n🔄 Service herstart",
];
} catch (\Exception $e) {
Log::error('[EmulatorJar] Exception', ['error' => $e->getMessage()]);
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
public function findLatestJar(): ?array
{
if (! $this->githubRepo) {
return null;
}
$branch = $this->githubBranch ?: 'main';
$commonNames = ['arcturus.jar', 'Arcturus.jar', 'emulator.jar', 'habbo.jar', 'hotel.jar'];
foreach ($commonNames as $name) {
try {
$apiUrl = "https://api.github.com/repos/{$this->githubRepo}/contents/Latest_Compiled_Version/{$name}?ref={$branch}";
$response = Http::timeout(10)
->withHeaders([
'Accept' => 'application/vnd.github.v3+json',
'User-Agent' => 'AtomCMS-Emulator-Updater',
])
->get($apiUrl);
if (! $response->successful()) {
continue;
}
$data = json_decode($response->body(), true);
if (! isset($data['sha']) || ! isset($data['download_url'])) {
continue;
}
$commitDate = null;
$commitSha = $data['sha'];
try {
$commitsResponse = Http::timeout(10)
->withHeaders([
'Accept' => 'application/vnd.github.v3+json',
'User-Agent' => 'AtomCMS-Emulator-Updater',
])
->get("https://api.github.com/repos/{$this->githubRepo}/commits", [
'path' => "Latest_Compiled_Version/{$name}",
'sha' => $branch,
'per_page' => 1,
]);
if ($commitsResponse->successful()) {
$commits = $commitsResponse->json();
if (! empty($commits) && isset($commits[0]['commit']['committer']['date'])) {
$commitDate = strtotime($commits[0]['commit']['committer']['date']);
$commitSha = $commits[0]['sha'] ?? $data['sha'];
}
}
} catch (\Exception) {
Log::debug('[EmulatorJar] Could not fetch commit date for ' . $name);
}
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
$installedJarCommit = $this->settings->getOrDefault('emulator_jar_commit', null);
$isUpdate = false;
if ($installedJarCommit !== null) {
$isUpdate = $installedJarCommit !== $commitSha;
} elseif ($installedDate !== null && $commitDate !== null) {
$isUpdate = (int) $installedDate < $commitDate;
} elseif ($installedDate === null && $commitDate !== null) {
$isUpdate = true;
}
$version = $this->extractVersionFromFilename($name);
if ($version === '' || $version === '0') {
$version = $commitDate ? date('Y.m.d', $commitDate) : date('Y.m.d');
}
return [
'name' => $name,
'url' => $data['download_url'],
'version' => $version,
'commit' => $commitSha,
'commit_date' => $commitDate,
'is_update' => $isUpdate,
'installed_date' => $installedDate,
];
} catch (\Exception $e) {
Log::warning('[EmulatorJar] Error checking JAR file ' . $name, ['error' => $e->getMessage()]);
}
}
return null;
}
private function checkDirectUrlUpdates(?array $sourceInfo, bool $hasSourceUpdates): array
{
$jarInfo = $this->validateDirectUrl($this->jarDirectUrl);
if ($jarInfo) {
if (! empty($jarInfo['version'])) {
$currentVersion = $this->settings->getOrDefault('emulator_version', '0.0.0');
if ($jarInfo['version'] !== $currentVersion) {
$this->settings->set('emulator_version', $jarInfo['version']);
}
}
$hasJarUpdates = $jarInfo['is_update'] ?? false;
if ($hasSourceUpdates && ! $hasJarUpdates) {
return [
'update_available' => true,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $jarInfo['version'],
'release_name' => 'Source Update',
'jar_url' => null,
'jar_name' => null,
'jar_size' => 'Onbekend',
'type' => 'source_build',
'source_info' => $sourceInfo,
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
];
}
return [
'update_available' => $hasJarUpdates,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => $jarInfo['version'],
'release_name' => $hasSourceUpdates ? 'Source Update' : 'Direct URL',
'jar_url' => $this->jarDirectUrl,
'jar_name' => $jarInfo['name'],
'jar_size' => 'Onbekend',
'type' => $hasSourceUpdates ? 'source_build' : 'direct_url',
'commit' => $jarInfo['commit_sha'] ?? null,
'source_info' => $sourceInfo,
'has_source_updates' => $hasSourceUpdates,
];
}
if ($hasSourceUpdates) {
return [
'update_available' => true,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : 'Onbekend',
'release_name' => 'Source Update',
'jar_url' => null,
'jar_name' => null,
'jar_size' => 'Onbekend',
'type' => 'source_build',
'source_info' => $sourceInfo,
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
];
}
return [
'update_available' => false,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'error' => 'Directe .jar URL is niet bereikbaar',
];
}
private function checkGitHubFolderUpdates(?array $sourceInfo, bool $hasSourceUpdates): array
{
$jarInfo = $this->findLatestJar();
if (! $jarInfo) {
if ($hasSourceUpdates) {
return [
'update_available' => true,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : 'Onbekend',
'release_name' => 'Source Update',
'jar_url' => null,
'jar_name' => null,
'jar_size' => 'Onbekend',
'type' => 'source_build',
'source_info' => $sourceInfo,
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
];
}
return [
'update_available' => false,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => 'Onbekend',
'error' => 'Kon geen .jar bestand vinden',
'type' => 'not_found',
'source_available' => $sourceInfo !== null,
];
}
$version = $jarInfo['version'];
$hasJarUpdates = $jarInfo['is_update'] ?? false;
if ($hasSourceUpdates && ! $hasJarUpdates) {
return [
'update_available' => true,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => $sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $version,
'release_name' => 'Source Update',
'jar_url' => null,
'jar_name' => null,
'jar_size' => 'Onbekend',
'type' => 'source_build',
'source_info' => $sourceInfo,
'message' => 'Nieuwe commits beschikbaar - build vanaf source nodig',
];
}
return [
'update_available' => $hasJarUpdates,
'current_version' => $this->settings->getOrDefault('emulator_version', '0.0.0'),
'latest_version' => $hasSourceUpdates ? ($sourceInfo['latest_timestamp'] ? date('Y.m.d', $sourceInfo['latest_timestamp']) : $version) : $version,
'release_name' => $hasSourceUpdates ? 'Source Update' : 'Latest from GitHub',
'jar_url' => $jarInfo['url'],
'jar_name' => $jarInfo['name'],
'jar_size' => 'Onbekend',
'type' => $hasSourceUpdates ? 'source_build' : 'github_folder',
'commit' => $jarInfo['commit'] ?? null,
'commit_date' => $jarInfo['commit_date'] ?? null,
'source_info' => $sourceInfo,
'has_source_updates' => $hasSourceUpdates,
];
}
private function validateDirectUrl(string $url): ?array
{
if ($url === '' || $url === '0' || ! str_ends_with(strtolower($url), '.jar')) {
return null;
}
try {
$jarName = basename(parse_url($url, PHP_URL_PATH));
$version = $this->extractVersionFromFilename($jarName);
$storedVersion = $this->settings->getOrDefault('emulator_version', '0.0.0');
$lastModified = null;
$commitSha = null;
$gitHubInfoAvailable = false;
if (preg_match('/github\.com\/([^\/]+)\/([^\/]+)\/raw\/refs\/heads\/([^\/]+)\/(.+)/', $url, $matches)) {
$owner = $matches[1];
$repo = $matches[2];
$branch = $matches[3];
$path = $matches[4];
try {
$apiResponse = Http::timeout(10)
->withHeaders([
'Accept' => 'application/vnd.github.v3+json',
'User-Agent' => 'AtomCMS-Emulator-Updater',
])
->get("https://api.github.com/repos/{$owner}/{$repo}/contents/{$path}", [
'ref' => $branch,
]);
if ($apiResponse->successful()) {
$data = $apiResponse->json();
if (isset($data['sha']) && ! isset($data['message'])) {
$gitHubInfoAvailable = true;
$commitSha = $data['sha'];
if (isset($data['commit']['committer']['date'])) {
$lastModified = strtotime($data['commit']['committer']['date']);
}
}
}
} catch (\Exception) {
Log::debug('[EmulatorJar] Could not fetch GitHub commit info for direct URL');
}
}
if ($lastModified === null) {
$response = Http::timeout(10)->head($url);
if ($response->successful()) {
$modifiedSince = $response->header('Last-Modified');
if ($modifiedSince) {
$lastModified = strtotime($modifiedSince);
if ($lastModified === false) {
$lastModified = null;
}
}
}
}
$isUpdate = false;
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
$storedData = $this->settings->getOrDefault('emulator_direct_url_info_' . md5($url));
if ($storedData !== null && is_string($storedData)) {
$storedDataArray = json_decode($storedData, true);
if (is_array($storedDataArray)) {
if ($gitHubInfoAvailable && $commitSha !== null && isset($storedDataArray['commit_sha'])) {
$isUpdate = $commitSha !== $storedDataArray['commit_sha'];
} elseif ($lastModified !== null && $installedDate !== null) {
$isUpdate = (int) $installedDate < $lastModified;
} elseif ($lastModified !== null && isset($storedDataArray['last_modified'])) {
$isUpdate = $lastModified > $storedDataArray['last_modified'];
} elseif (! in_array($version, ['', '0', $storedVersion], true)) {
$isUpdate = version_compare($version, $storedVersion) > 0;
}
}
} else {
$isUpdate = true;
}
$infoToStore = [
'last_checked' => time(),
'version' => $version,
];
if ($lastModified) {
$infoToStore['last_modified'] = $lastModified;
}
if ($commitSha) {
$infoToStore['commit_sha'] = $commitSha;
}
$this->settings->set('emulator_direct_url_info_' . md5($url), json_encode($infoToStore));
return [
'name' => $jarName,
'version' => $version,
'last_modified' => $lastModified,
'commit_sha' => $commitSha,
'is_update' => $isUpdate,
'gitHub_rate_limited' => ! $gitHubInfoAvailable && preg_match('/github\.com/', $url),
];
} catch (\Exception $e) {
Log::warning('[EmulatorJar] Direct URL not reachable', ['error' => $e->getMessage()]);
}
return null;
}
private function buildUpdateScript(string $jarUrl, string $jarName, string $tempDir, string $tempJar, string $serviceName): string
{
return <<<BASH
#!/bin/bash
set -e
JAR_URL='{$jarUrl}'
JAR_NAME='{$jarName}'
JAR_PATH='{$this->jarPath}'
SERVICE='{$serviceName}'
TEMP_DIR='{$tempDir}'
TEMP_JAR='{$tempJar}'
mkdir -p "\$TEMP_DIR" "\$JAR_PATH"
if ls "\$JAR_PATH"/*.jar 1>/dev/null 2>&1; then
mkdir -p "\$JAR_PATH/backup"
mv "\$JAR_PATH"/*.jar "\$JAR_PATH/backup/" 2>/dev/null || true
fi
download_success=false
for attempt in 1 2 3; do
if curl -L --max-time 300 --retry 3 --retry-delay 5 -o "\$TEMP_JAR" "\$JAR_URL" 2>&1; then
FILE_TYPE=\$(file -b "\$TEMP_JAR" 2>/dev/null)
JAR_SIZE=\$(stat -c%s "\$TEMP_JAR" 2>/dev/null || echo 0)
if echo "\$FILE_TYPE" | grep -qi "zip\|jar\|archive" && [ "\$JAR_SIZE" -gt 1000 ]; then
download_success=true
break
else
rm -f "\$TEMP_JAR" 2>/dev/null || true
sleep 3
fi
else
sleep 5
fi
done
if [ "\$download_success" = false ]; then
if ls "\$JAR_PATH/backup"/*.jar 1>/dev/null 2>&1; then
mv "\$JAR_PATH/backup"/*.jar "\$JAR_PATH/" 2>/dev/null || true
fi
rm -rf "\$TEMP_DIR"
exit 1
fi
mv "\$TEMP_JAR" "\$JAR_PATH/"
chown -R www-data:www-data "\$JAR_PATH"
chmod 755 "\$JAR_PATH/\$JAR_NAME"
systemctl restart "\$SERVICE" 2>&1 || service "\$SERVICE" restart 2>&1 || true
rm -rf "\$TEMP_DIR" 2>/dev/null || true
BASH;
}
private function storeUpdateInfo(string $version, array $check): void
{
setting('emulator_version', $version);
$commitDate = $check['commit_date'] ?? time();
$this->settings->set('emulator_jar_installed_date', (string) $commitDate);
$this->settings->set('emulator_jar_commit', $check['commit'] ?? null);
$sourceSha = $check['source_info']['latest_sha'] ?? $check['commit'] ?? null;
$sourceDate = $check['source_info']['latest_timestamp'] ?? ($check['commit_date'] ?? time());
if ($sourceSha) {
$this->settings->set('emulator_source_commit', $sourceSha);
}
if ($sourceDate) {
$this->settings->set('emulator_source_date', (string) $sourceDate);
}
$currentBranch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
$this->settings->set('emulator_installed_branch', $currentBranch);
}
}
@@ -1,273 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Services\Emulator;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
class EmulatorSourceService
{
use EmulatorConfiguration;
public function __construct()
{
$this->loadConfiguration();
}
public function checkForUpdates(): ?array
{
$repo = $this->sourceRepo ?: $this->githubRepo;
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
if (! $repo) {
return null;
}
$localCheck = $this->checkLocalSourceUpdates();
if ($localCheck !== null) {
return $localCheck;
}
return $this->checkRemoteSourceUpdates($repo, $branch);
}
public function isSourceBuildAvailable(): bool
{
$hasRepo = ! in_array($this->sourceRepo, [null, '', '0'], true) || ! in_array($this->githubRepo, [null, '', '0'], true);
$hasPath = ! in_array($this->emulatorSourcePath, [null, '', '0'], true);
if (! $hasRepo || ! $hasPath) {
return false;
}
try {
$result = Process::timeout(5)->run("[ -d {$this->emulatorSourcePath} ] && echo 'exists' || echo 'not_exists'");
return trim($result->output()) === 'exists';
} catch (\Exception) {
return false;
}
}
private function checkLocalSourceUpdates(): ?array
{
$sourcePath = $this->emulatorSourcePath;
$existsCheck = Process::timeout(5)->run("[ -d {$sourcePath} ] && echo 'exists'");
if (! $existsCheck->successful() || trim($existsCheck->output()) !== 'exists') {
return $this->checkSourceByCloning();
}
try {
$gitCheck = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse --is-inside-work-tree 2>/dev/null");
if (! $gitCheck->successful()) {
return $this->checkSourceByCloning();
}
$currentCommitResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse HEAD");
if (! $currentCommitResult->successful()) {
return null;
}
$currentSha = trim($currentCommitResult->output());
$fetchResult = Process::timeout(30)->run("cd {$sourcePath} && git fetch origin 2>&1");
if ($fetchResult->failed()) {
Log::debug('[EmulatorSource] Git fetch failed, trying local check only');
}
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
$latestResult = Process::timeout(5)->run("cd {$sourcePath} && git rev-parse origin/{$branch} 2>/dev/null || git rev-parse HEAD");
if (! $latestResult->successful()) {
return null;
}
$latestSha = trim($latestResult->output());
$dateResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%ci' {$latestSha}");
$latestDate = null;
$latestTimestamp = time();
if ($dateResult->successful()) {
$latestDate = trim($dateResult->output(), "'");
if ($latestDate !== '' && $latestDate !== '0') {
$latestTimestamp = strtotime($latestDate);
}
}
$this->persistSourceCommitInfo($latestSha, $latestTimestamp);
$msgResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%s' {$latestSha}");
$latestMessage = $msgResult->successful() ? trim($msgResult->output()) : '';
$authorResult = Process::timeout(5)->run("cd {$sourcePath} && git log -1 --format='%an' {$latestSha}");
$latestAuthor = $authorResult->successful() ? trim($authorResult->output()) : '';
$storedSha = $this->settings->getOrDefault('emulator_source_commit', null);
$storedDate = $this->settings->getOrDefault('emulator_source_date', null);
$isUpdate = $storedSha !== null && $storedSha !== $latestSha;
if ($storedSha === null) {
$isUpdate = true;
}
return [
'has_update' => $isUpdate,
'latest_sha' => $latestSha,
'latest_date' => $latestDate,
'latest_timestamp' => $latestTimestamp,
'latest_message' => $latestMessage,
'latest_author' => $latestAuthor,
'stored_sha' => $storedSha,
'stored_date' => $storedDate,
'source' => 'local',
];
} catch (\Exception $e) {
Log::debug('[EmulatorSource] Local source check failed: ' . $e->getMessage());
return null;
}
}
private function checkSourceByCloning(): ?array
{
$repo = $this->sourceRepo ?: $this->githubRepo;
$branch = $this->sourceBranch ?: $this->githubBranch ?: 'main';
if (! $repo) {
return null;
}
$repo = preg_replace('#/tree/[^/]+/.*$#', '', $repo);
$repo = str_replace(['https://github.com/', 'http://github.com/'], '', $repo);
$repo = rtrim($repo, '/');
try {
$tempDir = '/tmp/emulator-source-check-' . Str::random(8);
$cloneResult = Process::timeout(120)->run(
"git clone --branch {$branch} --depth 1 https://github.com/{$repo}.git {$tempDir} 2>&1",
);
if ($cloneResult->failed()) {
return null;
}
$shaResult = Process::timeout(5)->run("cd {$tempDir} && git rev-parse HEAD");
$latestSha = $shaResult->successful() ? trim($shaResult->output()) : '';
$dateResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%ci'");
$latestDate = null;
$latestTimestamp = time();
if ($dateResult->successful()) {
$latestDate = trim($dateResult->output());
if ($latestDate !== '' && $latestDate !== '0') {
$latestTimestamp = strtotime($latestDate);
}
}
$this->persistSourceCommitInfo($latestSha, $latestTimestamp);
$msgResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%s'");
$latestMessage = $msgResult->successful() ? trim($msgResult->output()) : '';
$authorResult = Process::timeout(5)->run("cd {$tempDir} && git log -1 --format='%an'");
$latestAuthor = $authorResult->successful() ? trim($authorResult->output()) : '';
Process::timeout(10)->run("rm -rf {$tempDir}");
$storedSha = $this->settings->getOrDefault('emulator_source_commit', null);
$storedDate = $this->settings->getOrDefault('emulator_source_date', null);
$isUpdate = $storedSha !== null && $storedSha !== $latestSha;
if ($storedSha === null) {
$isUpdate = true;
}
return [
'has_update' => $isUpdate,
'latest_sha' => $latestSha,
'latest_date' => $latestDate,
'latest_timestamp' => $latestTimestamp,
'latest_message' => $latestMessage,
'latest_author' => $latestAuthor,
'stored_sha' => $storedSha,
'stored_date' => $storedDate,
'source' => 'cloned',
];
} catch (\Exception $e) {
Log::debug('[EmulatorSource] Source clone check failed: ' . $e->getMessage());
return null;
}
}
private function checkRemoteSourceUpdates(string $repo, string $branch): ?array
{
try {
$response = Http::timeout(10)
->withHeaders([
'Accept' => 'application/vnd.github.v3+json',
'User-Agent' => 'AtomCMS-Emulator-Updater',
])
->get("https://api.github.com/repos/{$repo}/commits", [
'sha' => $branch,
'per_page' => 1,
]);
if (! $response->successful()) {
return null;
}
$commits = $response->json();
if (empty($commits) || ! isset($commits[0]['sha'])) {
return null;
}
$latestCommit = $commits[0];
$latestSha = $latestCommit['sha'];
$latestDate = $latestCommit['commit']['committer']['date'] ?? null;
$latestTimestamp = $latestDate ? strtotime((string) $latestDate) : time();
$this->persistSourceCommitInfo($latestSha, $latestTimestamp);
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
$storedSha = $this->settings->getOrDefault('emulator_source_commit', null);
$storedDate = $this->settings->getOrDefault('emulator_source_date', null);
if ($storedSha !== null && $storedSha === $latestSha) {
$isUpdate = false;
} elseif ($storedSha !== null && $storedSha !== $latestSha) {
$isUpdate = true;
} elseif ($installedDate !== null) {
$installedTimestamp = is_numeric($installedDate) ? (int) $installedDate : strtotime((string) $installedDate);
$isUpdate = $installedTimestamp < $latestTimestamp;
} else {
$isUpdate = false;
}
return [
'has_update' => $isUpdate,
'latest_sha' => $latestSha,
'latest_date' => $latestDate,
'latest_timestamp' => $latestTimestamp,
'latest_message' => $latestCommit['commit']['message'] ?? '',
'latest_author' => $latestCommit['commit']['author']['name'] ?? '',
'stored_sha' => $storedSha,
'stored_date' => $storedDate,
];
} catch (\Exception $e) {
Log::warning('[EmulatorSource] Could not check source updates via GitHub API', ['error' => $e->getMessage()]);
return null;
}
}
private function persistSourceCommitInfo(string $sha, int $timestamp): void
{
$this->settings->set('emulator_source_commit', $sha);
$this->settings->set('emulator_source_date', (string) $timestamp);
}
}
@@ -1,510 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Services\Emulator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
class EmulatorSqlService
{
use EmulatorConfiguration;
private const string SQL_TABLE = 'emulator_sql_updates';
public function __construct()
{
$this->loadConfiguration();
}
public function isConfigured(): bool
{
return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true);
}
public function checkForUpdates(bool $recentOnly = true): array
{
if (! $this->githubRepo) {
return [
'has_updates' => false,
'error' => 'Geen GitHub repo geconfigureerd',
];
}
$this->ensureSqlTableExists();
$result = $this->fetchSqlFilesFromGitHub($recentOnly);
if (isset($result['error'])) {
return [
'has_updates' => false,
'error' => $result['error'],
];
}
$sqlFiles = $result;
if ($sqlFiles === []) {
return [
'has_updates' => false,
'message' => $recentOnly
? 'Geen SQL updates van de afgelopen week gevonden'
: 'Geen SQL updates gevonden',
];
}
$appliedHashes = $this->getAppliedSqlHashes();
$newSqlFiles = [];
$alreadyApplied = [];
foreach ($sqlFiles as $file) {
$hash = $file['sha'] ?? md5((string) $file['name']);
if (in_array($hash, $appliedHashes)) {
$alreadyApplied[] = $file['name'];
continue;
}
$newSqlFiles[] = $file;
}
if ($newSqlFiles === []) {
return [
'has_updates' => false,
'message' => 'Alle SQL updates zijn al toegepast (' . count($alreadyApplied) . ' stuks)',
'applied_count' => count($alreadyApplied),
];
}
usort($newSqlFiles, fn ($a, $b) => strcmp((string) $a['name'], (string) $b['name']));
return [
'has_updates' => true,
'count' => count($newSqlFiles),
'files' => $newSqlFiles,
'message' => count($newSqlFiles) . ' nieuwe SQL update(s) van deze week',
'applied_count' => count($alreadyApplied),
];
}
public function runUpdates(): array
{
$sqlCheck = $this->checkForUpdates(false);
if (! ($sqlCheck['has_updates'] ?? false)) {
return ['success' => true, 'sql_updated' => false, 'message' => 'Geen nieuwe SQL updates'];
}
$results = [
'success' => true,
'sql_updated' => true,
'files_run' => [],
'errors' => [],
];
foreach ($sqlCheck['files'] as $file) {
$sqlResult = $this->downloadAndRunSql($file);
if ($sqlResult['success']) {
$results['files_run'][] = $file['name'];
$this->markSqlAsApplied($file);
} else {
$results['errors'][] = $file['name'] . ': ' . $sqlResult['error'];
}
}
if (count($results['errors']) > 0) {
$results['message'] = count($results['files_run']) . ' SQL updates succesvol, ' . count($results['errors']) . ' met fouten';
} else {
$results['message'] = count($results['files_run']) . ' SQL updates succesvol uitgevoerd!';
}
return $results;
}
public function getAppliedUpdates(): array
{
$this->ensureSqlTableExists();
return DB::table(self::SQL_TABLE)
->orderBy('applied_at', 'desc')
->get()
->toArray();
}
public function diagnose(): array
{
$diagnosis = [
'table_exists' => false,
'applied_count' => 0,
'pending_count' => 0,
'error' => null,
];
try {
$diagnosis['table_exists'] = Schema::hasTable(self::SQL_TABLE);
if ($diagnosis['table_exists']) {
$diagnosis['applied_count'] = DB::table(self::SQL_TABLE)->count();
}
if ($this->githubRepo && $diagnosis['table_exists']) {
$sqlCheck = $this->checkForUpdates(false);
if (isset($sqlCheck['count'])) {
$diagnosis['pending_count'] = $sqlCheck['count'];
}
}
} catch (\Exception $e) {
$diagnosis['error'] = $e->getMessage();
}
return $diagnosis;
}
public function repair(): array
{
$actions = [];
$errors = [];
try {
$this->ensureSqlTableExists();
$actions[] = 'SQL update tabel gecontroleerd';
$appliedCount = DB::table(self::SQL_TABLE)->count();
$actions[] = "SQL updates tabel OK ({$appliedCount} records)";
if ($this->githubRepo) {
$sqlCheck = $this->checkForUpdates(false);
if (isset($sqlCheck['error'])) {
$actions[] = 'SQL update check: ' . $sqlCheck['error'];
} elseif (! ($sqlCheck['has_updates'] ?? false)) {
$actions[] = 'SQL updates: ' . ($sqlCheck['message'] ?? 'Allemaal up-to-date');
} else {
$count = $sqlCheck['count'] ?? 0;
$actions[] = "{$count} nieuwe SQL updates beschikbaar";
if ($this->isConfigured()) {
$actions[] = 'SQL updates worden toegepast...';
$runResult = $this->runUpdates();
if ($runResult['success'] ?? false) {
$filesRun = count($runResult['files_run'] ?? []);
$actions[] = "{$filesRun} SQL updates toegepast";
} else {
$errors[] = 'SQL updates: ' . ($runResult['error'] ?? 'Onbekende fout');
}
}
}
}
} catch (\Exception $e) {
$errors[] = 'SQL repair: ' . $e->getMessage();
Log::error('[EmulatorSql] Repair failed', ['error' => $e->getMessage()]);
}
return [
'success' => $errors === [],
'actions' => $actions,
'errors' => $errors,
];
}
private function ensureSqlTableExists(): void
{
if (Schema::hasTable(self::SQL_TABLE)) {
return;
}
Schema::create(self::SQL_TABLE, function ($table) {
$table->id();
$table->string('file_name');
$table->string('file_hash')->unique();
$table->timestamp('applied_at');
$table->text('sql_content')->nullable();
});
}
private function getAppliedSqlHashes(): array
{
$this->ensureSqlTableExists();
return DB::table(self::SQL_TABLE)
->pluck('file_hash')
->toArray();
}
private function markSqlAsApplied(array $file): void
{
$this->ensureSqlTableExists();
$hash = $file['sha'] ?? md5((string) $file['name']);
DB::table(self::SQL_TABLE)->updateOrInsert(
['file_hash' => $hash],
[
'file_name' => $file['name'],
'applied_at' => now(),
],
);
}
private function fetchSqlFilesFromGitHub(bool $recentOnly = false): array
{
if (! $this->githubRepo) {
return [];
}
$branch = $this->githubBranch ?: 'main';
$folderNames = [
'Database%20Updates',
'Database Updates',
'database_updates',
'database/updates',
'sql/updates',
'sql',
'updates',
];
$knownSqlFiles = [
'07012026_UpdateDatabase_to_4-0-1.sql',
'09012026_UpdateDatabase_to_4-0-2.sql',
'12012026_Battle Banzai.sql',
'12012026_Breeding Fixes.sql',
'12012026_ChatBubbles.sql',
'16032026_updateall_command.sql',
'17032026_allow_underpass.sql',
'19032026_hotel_timezone.sql',
'21022026_user_prefixes.sql',
'Default_Camera.sql',
'UpdateDatabase_Allow_diagonale.sql',
'UpdateDatabase_BOT.sql',
'UpdateDatabase_Banners.sql',
'UpdateDatabase_DanceCMD.sql',
'UpdateDatabase_Happiness.sql',
'UpdateDatabase_Websocket.sql',
'UpdateDatabase_unignorable.sql',
];
try {
if ($recentOnly) {
return $this->fetchRecentSqlFiles($branch);
}
$sqlFiles = [];
foreach ($knownSqlFiles as $filename) {
foreach ($folderNames as $folderName) {
$encodedFilename = str_replace(' ', '%20', $filename);
$url = "https://raw.githubusercontent.com/{$this->githubRepo}/{$branch}/{$folderName}/{$encodedFilename}";
$response = Http::timeout(10)->get($url);
if ($response->successful()) {
$sqlFiles[] = [
'name' => $filename,
'url' => $url,
'sha' => md5($response->body()),
];
break;
}
}
}
if ($sqlFiles !== []) {
usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name']));
return $sqlFiles;
}
return [];
} catch (\Exception $e) {
Log::warning('[EmulatorSql] Could not fetch SQL files', ['error' => $e->getMessage()]);
return [];
}
}
private function fetchRecentSqlFiles(string $branch): array
{
$weekAgo = now()->subDays(7)->toIso8601String();
$githubToken = setting('github_token', '');
$headers = [
'Accept' => 'application/vnd.github+json',
'User-Agent' => 'AtomCMS-EmulatorUpdate/1.0',
];
if (! empty($githubToken)) {
$headers['Authorization'] = 'Bearer ' . $githubToken;
}
$folderNames = ['Database Updates', 'database_updates', 'sql', 'updates'];
try {
foreach ($folderNames as $folderName) {
$response = Http::withHeaders($headers)->timeout(30)->get("https://api.github.com/repos/{$this->githubRepo}/commits", [
'path' => $folderName,
'sha' => $branch,
'since' => $weekAgo,
'per_page' => 100,
]);
if ($response->successful()) {
$commits = $response->json();
if (! is_array($commits) || $commits === []) {
continue;
}
$sqlFiles = [];
foreach ($commits as $commit) {
$sha = $commit['sha'] ?? null;
$commitDate = $commit['commit']['committer']['date'] ?? null;
if (! $sha) {
continue;
}
$commitResponse = Http::withHeaders($headers)->timeout(30)->get("https://api.github.com/repos/{$this->githubRepo}/commits/{$sha}");
if (! $commitResponse->successful()) {
continue;
}
$commitData = $commitResponse->json();
$files = $commitData['files'] ?? [];
foreach ($files as $file) {
$filename = $file['filename'] ?? '';
if (! str_ends_with(strtolower((string) $filename), '.sql')) {
continue;
}
$name = basename((string) $filename);
$sqlFiles[] = [
'name' => $name,
'url' => $file['raw_url'] ?? "https://raw.githubusercontent.com/{$this->githubRepo}/{$sha}/{$filename}",
'sha' => $sha,
'date' => $commitDate,
];
}
}
if ($sqlFiles !== []) {
usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name']));
return $sqlFiles;
}
}
}
return [];
} catch (\Exception $e) {
Log::warning('[EmulatorSql] Could not fetch recent SQL files', ['error' => $e->getMessage()]);
return [];
}
}
private function downloadAndRunSql(array $file): array
{
try {
$response = Http::timeout(60)->get($file['url']);
if (! $response->successful()) {
return ['success' => false, 'error' => 'Download mislukt'];
}
$sql = $this->cleanSql($response->body());
if (in_array(trim($sql), ['', '0'], true)) {
return ['success' => true, 'message' => 'Lege SQL file overgeslagen'];
}
$host = setting('emulator_database_host', config('database.connections.emulator.host', '127.0.0.1'));
$port = setting('emulator_database_port', config('database.connections.emulator.port', '3306'));
$name = setting('emulator_database_name', config('database.connections.emulator.database', ''));
$username = setting('emulator_database_username', config('database.connections.emulator.username', ''));
$password = setting('emulator_database_password', config('database.connections.emulator.password', ''));
if (empty($name) || empty($username)) {
return ['success' => false, 'error' => 'Emulator database niet geconfigureerd'];
}
$pdo = new \PDO(
"mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4",
$username,
$password,
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION],
);
$statements = $this->splitSqlStatements($sql);
$successCount = 0;
$skipCount = 0;
foreach ($statements as $statement) {
$statement = trim((string) $statement);
if ($statement === '' || $statement === '0') {
continue;
}
try {
$pdo->exec($statement);
$successCount++;
} catch (\PDOException $e) {
if ($this->isDuplicateError($e)) {
$skipCount++;
continue;
}
Log::warning('[SQL Update] Statement error: ' . $e->getMessage());
}
}
Log::info('[SQL Update] Successfully ran: ' . $file['name'] . " ({$successCount} statements, {$skipCount} skipped)");
return ['success' => true, 'message' => "SQL uitgevoerd ({$successCount} statements, {$skipCount} duplicate)"];
} catch (\Exception $e) {
Log::error('[SQL Update] Failed: ' . $file['name'], ['error' => $e->getMessage()]);
return ['success' => false, 'error' => $e->getMessage()];
}
}
private function cleanSql(string $sql): string
{
$sql = preg_replace('/--.*$/m', '', $sql);
$sql = preg_replace('/#.*$/m', '', (string) $sql);
$sql = preg_replace('/SET FOREIGN_KEY_CHECKS.*?;/i', '', (string) $sql);
return preg_replace('/SET @@SESSION.SQL_MODE.*?;/i', '', (string) $sql);
}
private function splitSqlStatements(string $sql): array
{
$parts = explode(';', $sql);
return array_filter($parts, fn ($part) => trim((string) $part) !== '');
}
private function isDuplicateError(\PDOException $e): bool
{
$message = strtolower($e->getMessage());
$patterns = [
'duplicate entry',
"table '",
'already exists',
"can't create",
'duplicate key',
'duplicate index',
'error 1061',
'error 1062',
'error 1826',
];
return array_any($patterns, fn ($pattern) => str_contains($message, (string) $pattern));
}
}
-407
View File
@@ -1,407 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Miscellaneous\WebsiteSetting;
use App\Services\Emulator\EmulatorBackupService;
use App\Services\Emulator\EmulatorBuildService;
use App\Services\Emulator\EmulatorJarService;
use App\Services\Emulator\EmulatorSourceService;
use App\Services\Emulator\EmulatorSqlService;
use App\Services\Emulator\EmulatorStatusService;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
class EmulatorUpdateService
{
private readonly EmulatorStatusService $statusService;
private readonly EmulatorJarService $jarService;
private readonly EmulatorSourceService $sourceService;
private readonly EmulatorBuildService $buildService;
private readonly EmulatorSqlService $sqlService;
private readonly EmulatorBackupService $backupService;
private readonly SettingsService $settings;
public function __construct()
{
$this->statusService = new EmulatorStatusService;
$this->jarService = new EmulatorJarService;
$this->sourceService = new EmulatorSourceService;
$this->buildService = new EmulatorBuildService;
$this->sqlService = new EmulatorSqlService;
$this->backupService = new EmulatorBackupService;
$this->settings = app(SettingsService::class);
}
public function isConfigured(): bool
{
return $this->statusService->isConfigured();
}
public function getStatus(): array
{
$status = $this->statusService->getStatus();
$updateCheck = $this->jarService->checkForUpdates();
$sourceInfo = $this->sourceService->checkForUpdates();
return array_merge($status, [
'update_available' => $updateCheck['update_available'] ?? false,
'current_version' => $updateCheck['current_version'] ?? setting('emulator_version', 'N/A'),
'latest_version' => $updateCheck['latest_version'] ?? 'N/A',
'update_type' => $updateCheck['type'] ?? 'unknown',
'has_source_updates' => $sourceInfo['has_update'] ?? false,
'latest_sha' => $sourceInfo['latest_sha'] ?? null,
'latest_message' => $sourceInfo['latest_message'] ?? null,
'latest_author' => $sourceInfo['latest_author'] ?? null,
'latest_date' => $sourceInfo['latest_date'] ?? null,
'stored_sha' => $sourceInfo['stored_sha'] ?? null,
'stored_date' => $sourceInfo['stored_date'] ?? null,
'source_info' => $sourceInfo,
]);
}
public function checkForUpdates(): array
{
return $this->jarService->checkForUpdates();
}
public function checkForSqlUpdates(bool $recentOnly = true): array
{
return $this->sqlService->checkForUpdates($recentOnly);
}
public function runSqlUpdates(): array
{
return $this->sqlService->runUpdates();
}
public function getAppliedSqlUpdates(): array
{
return $this->sqlService->getAppliedUpdates();
}
public function updateEmulator(): array
{
if (! $this->isConfigured()) {
return ['success' => false, 'error' => 'Geen GitHub URL geconfigureerd'];
}
$check = $this->checkForUpdates();
if (! ($check['update_available'] ?? false)) {
if ($check['type'] === 'not_found' && ($check['source_available'] ?? false)) {
return $this->buildFromSource();
}
return ['success' => false, 'error' => 'Emulator is al up-to-date'];
}
$hasSourceUpdates = ($check['has_source_updates'] ?? false) || ($check['type'] ?? '') === 'source_build';
if ($hasSourceUpdates && $this->sourceService->isSourceBuildAvailable()) {
return $this->buildFromSource();
}
if ($check['type'] === 'source_build') {
return $this->buildFromSource();
}
if (! ($check['jar_url'] ?? null)) {
return ['success' => false, 'error' => 'Geen .jar gevonden'];
}
$result = $this->jarService->performUpdate($check);
if ($result['success']) {
$this->runSqlUpdates();
if ($this->restartEmulator()) {
$result['restarted'] = true;
$result['message'] = ($result['message'] ?? '') . ' | 🔄 Emulator herstart';
}
}
return $result;
}
public function performUpdate(array $check): array
{
return $this->jarService->performUpdate($check);
}
public function buildFromSource(bool $force = false): array
{
return $this->buildService->buildFromSource($force);
}
public function restartEmulator(): bool
{
$serviceName = $this->settings->getOrDefault('emulator_service_name', 'emulator');
try {
Log::info('[EmulatorUpdate] Restarting emulator service: ' . $serviceName);
$result = Process::timeout(30)->run("systemctl restart {$serviceName} 2>&1");
if ($result->successful()) {
return true;
}
$result = Process::timeout(30)->run("service {$serviceName} restart 2>&1");
return $result->successful();
} catch (\Exception $e) {
Log::error('[EmulatorUpdate] Failed to restart emulator', ['error' => $e->getMessage()]);
return false;
}
}
public function getBackupList(): array
{
return $this->backupService->getList();
}
public function restoreBackup(string $backupName): array
{
return $this->backupService->restore($backupName);
}
public function getInstalledVersion(): string
{
return $this->statusService->getInstalledVersion();
}
public function getInstalledJar(): ?string
{
return $this->statusService->getInstalledJar();
}
public function getInstalledJarInfo(): array
{
return $this->statusService->getInstalledJarInfo();
}
public function getLastSqlUpdate(): ?string
{
return setting('emulator_last_sql_update');
}
public function debugStatus(): array
{
return Cache::remember('emulator_debug_status', 120, function () {
$installedDate = $this->settings->getOrDefault('emulator_jar_installed_date', null);
$sourceCommit = $this->settings->getOrDefault('emulator_source_commit', null);
$sourceDate = $this->settings->getOrDefault('emulator_source_date', null);
$emulatorVersion = $this->settings->getOrDefault('emulator_version', null);
$jarFiles = $this->statusService->getInstalledJarInfo();
return [
'github_url' => $this->settings->getOrDefault('emulator_github_url', ''),
'github_repo' => $this->settings->getOrDefault('emulator_source_repo', ''),
'github_branch' => $this->settings->getOrDefault('emulator_github_branch', 'main'),
'source_repo' => $this->settings->getOrDefault('emulator_source_repo', ''),
'source_branch' => $this->settings->getOrDefault('emulator_github_branch', 'main'),
'installed_date' => $installedDate,
'installed_date_formatted' => $installedDate ? date('Y-m-d H:i:s', (int) $installedDate) : null,
'source_commit' => $sourceCommit,
'source_date' => $sourceDate,
'source_date_formatted' => $sourceDate ? date('Y-m-d H:i:s', (int) $sourceDate) : null,
'emulator_version' => $emulatorVersion,
'jar_files' => $jarFiles,
'installed_branch' => $this->settings->getOrDefault('emulator_installed_branch', null),
];
});
}
public function resetInstalledDate(): void
{
WebsiteSetting::where('key', 'emulator_jar_installed_date')->delete();
WebsiteSetting::where('key', 'emulator_source_commit')->delete();
WebsiteSetting::where('key', 'emulator_source_date')->delete();
}
public function clearAllLogs(): array
{
$cleared = [];
$paths = [
storage_path('logs') => 'Laravel Logs',
storage_path('logs/emulator.log') => 'Emulator Log',
'/tmp/emulator-update-*' => 'Emulator Update Temp',
'/tmp/nitro-switch-*' => 'Nitro Switch Logs',
'/tmp/nitro_*' => 'Nitro Temp',
'/var/www/Emulator/logs' => 'Emulator Folder Logs',
];
foreach ($paths as $path => $label) {
try {
if (str_contains($path, '*')) {
Process::timeout(10)->run("rm -f {$path} 2>/dev/null || true");
$cleared[] = $label;
} elseif (is_dir($path)) {
Process::timeout(10)->run("find {$path} -name '*.log' -mtime +1 -delete 2>/dev/null || true");
$cleared[] = $label;
} elseif (is_file($path)) {
@unlink($path);
$cleared[] = $label;
}
} catch (\Exception) {
}
}
try {
$laravelLog = storage_path('logs/laravel.log');
if (is_file($laravelLog)) {
file_put_contents($laravelLog, '');
$cleared[] = 'laravel.log';
}
} catch (\Exception) {
}
Process::timeout(10)->run("find /tmp -name 'emulator_*' -mtime +1 -delete 2>/dev/null || true");
Process::timeout(10)->run("find /tmp -name 'nitro_*' -mtime +1 -delete 2>/dev/null || true");
Process::timeout(10)->run("find /tmp -name 'deploy_*' -mtime +1 -delete 2>/dev/null || true");
return [
'success' => true,
'cleared' => $cleared,
'message' => count($cleared) . ' log locaties geleegd',
];
}
public function repairEmulator(): array
{
$actions = [];
$errors = [];
Log::info('[EmulatorUpdate] Starting repair process');
try {
$status = $this->getStatus();
if (! ($status['jar_exists'] ?? false)) {
$actions[] = 'JAR bestand ontbreekt - downloaden...';
$updateResult = $this->updateEmulator();
if (! $updateResult['success']) {
$errors[] = 'Kon JAR niet herstellen: ' . ($updateResult['error'] ?? 'Onbekende fout');
} else {
$actions[] = 'JAR bestand hersteld';
}
}
if (! ($status['service_running'] ?? false)) {
$actions[] = 'Emulator service niet actief - starten...';
if ($this->restartEmulator()) {
$actions[] = 'Emulator service gestart';
} else {
$errors[] = 'Kon emulator service niet starten';
}
}
$sqlRepairResult = $this->sqlService->repair();
if (! empty($sqlRepairResult['actions'])) {
$actions = array_merge($actions, $sqlRepairResult['actions']);
}
if (! empty($sqlRepairResult['errors'])) {
$errors = array_merge($errors, $sqlRepairResult['errors']);
}
if ($errors !== []) {
return [
'success' => false,
'actions' => $actions,
'errors' => $errors,
'error' => implode('; ', $errors),
];
}
return [
'success' => true,
'actions' => $actions,
'message' => count($actions) . ' acties uitgevoerd',
];
} catch (\Exception $e) {
Log::error('[EmulatorUpdate] Repair exception', ['error' => $e->getMessage()]);
return [
'success' => false,
'actions' => $actions,
'error' => $e->getMessage(),
];
}
}
public function diagnoseSqlUpdates(): array
{
return $this->sqlService->diagnose();
}
public function diagnose(): array
{
$diagnosis = [
'timestamp' => now()->toIso8601String(),
'checks' => [],
'issues' => [],
'recommendations' => [],
];
try {
$status = $this->getStatus();
$diagnosis['checks']['jar_exists'] = $status['jar_exists'] ?? false;
$diagnosis['checks']['jar_files'] = $status['jar_files'] ?? [];
$diagnosis['checks']['service_running'] = $status['service_running'] ?? false;
$diagnosis['checks']['source_exists'] = $status['source_exists'] ?? false;
$diagnosis['checks']['emulator_db_connected'] = $status['emulator_db_connected'] ?? false;
$diagnosis['checks']['is_configured'] = $this->isConfigured();
$diagnosis['checks']['update_available'] = $status['update_available'] ?? false;
$sqlDiagnosis = $this->sqlService->diagnose();
$diagnosis['checks']['sql_table_exists'] = $sqlDiagnosis['table_exists'] ?? false;
$diagnosis['checks']['sql_updates_applied'] = $sqlDiagnosis['applied_count'] ?? 0;
$diagnosis['checks']['sql_pending'] = $sqlDiagnosis['pending_count'] ?? 0;
if (! ($status['jar_exists'] ?? false)) {
$diagnosis['issues'][] = 'JAR bestand ontbreekt';
$diagnosis['recommendations'][] = 'Voer emulator:update uit om de JAR te downloaden';
}
if (! ($status['service_running'] ?? false)) {
$diagnosis['issues'][] = 'Emulator service draait niet';
$diagnosis['recommendations'][] = 'Start de service met: sudo systemctl start ' . $this->settings->getOrDefault('emulator_service_name', 'emulator');
}
if (! ($status['emulator_db_connected'] ?? false)) {
$diagnosis['issues'][] = 'Emulator database niet bereikbaar';
$diagnosis['recommendations'][] = 'Controleer de database credentials in de settings';
}
if (! ($status['source_exists'] ?? false) && $this->sourceService->isSourceBuildAvailable()) {
$diagnosis['issues'][] = 'Source code niet gevonden';
$diagnosis['recommendations'][] = 'Voer emulator:update --rebuild uit om vanaf source te bouwen';
}
if (! ($sqlDiagnosis['table_exists'] ?? false)) {
$diagnosis['issues'][] = 'SQL update tabel ontbreekt';
$diagnosis['recommendations'][] = 'Reparatie zal de tabel aanmaken';
}
if (($sqlDiagnosis['pending_count'] ?? 0) > 0) {
$diagnosis['issues'][] = $sqlDiagnosis['pending_count'] . ' SQL updates pending';
$diagnosis['recommendations'][] = 'Voer reparatie uit om SQL updates toe te passen';
}
} catch (\Exception $e) {
$diagnosis['error'] = $e->getMessage();
}
return $diagnosis;
}
}
File diff suppressed because it is too large Load Diff
-186
View File
@@ -1,186 +0,0 @@
<?php
namespace App\Services;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class UpdateHistoryService
{
private const string TABLE = 'update_history';
public function ensureTableExists(): void
{
if (! Schema::hasTable(self::TABLE)) {
Schema::create(self::TABLE, function ($table) {
$table->id();
$table->string('type'); // nitro, emulator, sql, config
$table->string('action'); // update, build, deploy, fix
$table->string('item')->nullable(); // filename, version, etc
$table->string('status'); // success, failed, pending
$table->text('message')->nullable();
$table->string('user')->nullable();
$table->string('ip')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index(['type', 'created_at']);
$table->index('status');
});
}
}
public function log(string $type, string $action, ?string $item = null, string $status = 'success', ?string $message = null): void
{
$this->ensureTableExists();
DB::table(self::TABLE)->insert([
'type' => $type,
'action' => $action,
'item' => $item,
'status' => $status,
'message' => $message,
'user' => auth()->user()?->name ?? 'System',
'ip' => request()->ip(),
'created_at' => now(),
]);
}
public function getRecent(int $limit = 50): array
{
$this->ensureTableExists();
return DB::table(self::TABLE)
->orderBy('created_at', 'desc')
->limit($limit)
->get()
->toArray();
}
public function getByType(string $type, int $limit = 50): array
{
$this->ensureTableExists();
return DB::table(self::TABLE)
->where('type', $type)
->orderBy('created_at', 'desc')
->limit($limit)
->get()
->toArray();
}
public function getByStatus(string $status, int $limit = 50): array
{
$this->ensureTableExists();
return DB::table(self::TABLE)
->where('status', $status)
->orderBy('created_at', 'desc')
->limit($limit)
->get()
->toArray();
}
public function getStats(): array
{
$this->ensureTableExists();
$total = DB::table(self::TABLE)->count();
$success = DB::table(self::TABLE)->where('status', 'success')->count();
$failed = DB::table(self::TABLE)->where('status', 'failed')->count();
$lastUpdate = DB::table(self::TABLE)
->orderBy('created_at', 'desc')
->first();
$byType = DB::table(self::TABLE)
->select('type', DB::raw('count(*) as count'))
->groupBy('type')
->pluck('count', 'type')
->toArray();
return [
'total' => $total,
'success' => $success,
'failed' => $failed,
'success_rate' => $total > 0 ? round(($success / $total) * 100) : 100,
'last_update' => $lastUpdate,
'by_type' => $byType,
];
}
public function getHtml(): string
{
$this->ensureTableExists();
$updates = $this->getRecent(20);
$stats = $this->getStats();
$html = '<div style="font-family: -apple-system, BlinkMacSystemFont, sans-serif;">';
// Stats
$html .= '<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px;">';
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
$html .= '<div style="font-size: 24px; font-weight: 700; color: #4ade80;">' . $stats['total'] . '</div>';
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Totaal</div></div>';
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
$html .= '<div style="font-size: 24px; font-weight: 700; color: #4ade80;">' . $stats['success'] . '</div>';
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Geslaagd</div></div>';
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
$html .= '<div style="font-size: 24px; font-weight: 700; color: #f87171;">' . $stats['failed'] . '</div>';
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Mislukt</div></div>';
$html .= '<div style="background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; text-align: center;">';
$html .= '<div style="font-size: 24px; font-weight: 700; color: #60a5fa;">' . $stats['success_rate'] . '%</div>';
$html .= '<div style="font-size: 10px; color: #64748b; text-transform: uppercase;">Succes</div></div>';
$html .= '</div>';
// History list
if ($updates === []) {
$html .= '<div style="text-align: center; padding: 24px; color: #64748b;">';
$html .= '<div style="font-size: 32px; margin-bottom: 8px;">📋</div>';
$html .= 'Nog geen update geschiedenis</div>';
} else {
$html .= '<div style="max-height: 400px; overflow-y: auto;">';
foreach ($updates as $update) {
$icon = match ($update->status) {
'success' => '✅',
'failed' => '❌',
'pending' => '⏳',
default => '⚪'
};
$typeColor = match ($update->type) {
'nitro' => '#4ade80',
'emulator' => '#60a5fa',
'sql' => '#fbbf24',
'config' => '#a78bfa',
default => '#94a3b8'
};
$time = Carbon::parse($update->created_at)->diffForHumans();
$html .= '<div style="display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: rgba(255,255,255,0.03); border-radius: 8px; margin-bottom: 6px;">';
$html .= '<div style="font-size: 16px;">' . $icon . '</div>';
$html .= '<div style="flex: 1;">';
$html .= '<div style="display: flex; align-items: center; gap: 8px;">';
$html .= '<span style="background: ' . $typeColor . '20; color: ' . $typeColor . '; padding: 2px 8px; border-radius: 12px; font-size: 10px; font-weight: 600; text-transform: uppercase;">' . e($update->type) . '</span>';
$html .= '<span style="color: #e2e8f0; font-size: 13px;">' . e($update->action) . '</span>';
if ($update->item) {
$html .= '<span style="color: #94a3b8; font-size: 12px;">' . e($update->item) . '</span>';
}
$html .= '</div>';
if ($update->message) {
$html .= '<div style="color: #64748b; font-size: 11px; margin-top: 2px;">' . e($update->message) . '</div>';
}
$html .= '</div>';
$html .= '<div style="text-align: right;">';
$html .= '<div style="color: #64748b; font-size: 10px;">' . e($update->user) . '</div>';
$html .= '<div style="color: #475569; font-size: 9px;">' . e($time) . '</div>';
$html .= '</div>';
$html .= '</div>';
}
$html .= '</div>';
}
return $html . '</div>';
}
}
Regular → Executable
View File
+4 -4
View File
@@ -17,15 +17,15 @@ return [
| |
*/ */
'paths' => ['api/*', 'sanctum/csrf-cookie', 'client/*'], 'paths' => ['api/*', 'sanctum/csrf-cookie', 'client/*', 'imaging/*'],
'allowed_methods' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_METHODS', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'))), fn ($v) => $v !== ''), 'allowed_methods' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_METHODS', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'))), fn ($v) => $v !== ''),
'allowed_origins' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_ORIGINS', ''))), fn ($v) => $v !== ''), 'allowed_origins' => ['*'], // Zorgt ervoor dat alle origins (zoals je client/CMS) de imaging mogen inladen
'allowed_origins_patterns' => [], 'allowed_origins_patterns' => [],
'allowed_headers' => ['Content-Type', 'X-Requested-With', 'Authorization', 'X-XSRF-TOKEN'], 'allowed_headers' => ['*'], // Flexibel instellen zodat er geen headers geblokkeerd worden
'exposed_headers' => [], 'exposed_headers' => [],
@@ -33,4 +33,4 @@ return [
'supports_credentials' => true, 'supports_credentials' => true,
]; ];
-43
View File
@@ -1,43 +0,0 @@
#!/bin/bash
set -e
echo "=========================================="
echo " AtomCMS Deployment Script"
echo "=========================================="
PROJECT_DIR="/var/www/atomcms"
WEB_USER="www-data"
cd "$PROJECT_DIR"
echo "[1/8] Installing PHP dependencies..."
composer install --no-dev --optimize-autoloader --no-interaction
echo "[2/8] Installing JS dependencies..."
npm install --production=false
echo "[3/8] Building frontend assets..."
npm run build
echo "[4/8] Running database migrations..."
php artisan migrate --force
echo "[5/8] Clearing all caches..."
php artisan optimize:clear
echo "[6/8] Caching configuration, routes, and views..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
echo "[7/8] Fixing file permissions..."
chown -R "$WEB_USER":"$WEB_USER" storage bootstrap/cache public/build
chmod -R 775 storage bootstrap/cache public/build
echo "[8/8] Clearing OPcache..."
php -r "if (function_exists('opcache_reset')) { opcache_reset(); echo 'OPcache cleared'.PHP_EOL; } else { echo 'OPcache not enabled'.PHP_EOL; }"
echo "=========================================="
echo " Deployment complete!"
echo "=========================================="
Regular → Executable
View File
Executable → Regular
+1509 -1513
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1509 -1513
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+304 -349
View File
@@ -1,351 +1,306 @@
{ {
"commandocentrum.live_status": "Live Status", "commandocentrum.live_status": "Live Status",
"commandocentrum.live_status_desc": "Real-time hotel statistics", "commandocentrum.live_status_desc": "Real-time hotel statistics",
"commandocentrum.online": "Online", "commandocentrum.online": "Online",
"commandocentrum.emulator": "Emulator", "commandocentrum.emulator": "Emulator",
"commandocentrum.database": "Database", "commandocentrum.database": "Database",
"commandocentrum.load": "Load", "commandocentrum.load": "Load",
"commandocentrum.server_info": "Server Information", "commandocentrum.server_info": "Server Information",
"commandocentrum.server_info_desc": "Detailed server status", "commandocentrum.server_info_desc": "Detailed server status",
"commandocentrum.php_laravel": "PHP & Laravel", "commandocentrum.php_laravel": "PHP & Laravel",
"commandocentrum.memory_disk": "Memory & Disk", "commandocentrum.memory_disk": "Memory & Disk",
"commandocentrum.memory": "Memory", "commandocentrum.memory": "Memory",
"commandocentrum.disk": "Disk", "commandocentrum.disk": "Disk",
"commandocentrum.uptime": "Uptime", "commandocentrum.uptime": "Uptime",
"commandocentrum.system_health": "System Health", "commandocentrum.system_health": "System Health",
"commandocentrum.system_health_desc": "Automatic system diagnostics", "commandocentrum.system_health_desc": "Automatic system diagnostics",
"commandocentrum.refresh": "Refresh", "commandocentrum.refresh": "Refresh",
"commandocentrum.healthy": "Healthy", "commandocentrum.healthy": "Healthy",
"commandocentrum.warnings": "Warnings", "commandocentrum.warnings": "Warnings",
"commandocentrum.errors": "Errors", "commandocentrum.errors": "Errors",
"commandocentrum.system_status": "System Status", "commandocentrum.system_status": "System Status",
"commandocentrum.critical_issues": "Critical Issues", "commandocentrum.critical_issues": "Critical Issues",
"commandocentrum.hotel_status": "Hotel Status", "commandocentrum.hotel_status": "Hotel Status",
"commandocentrum.hotel_status_desc": "Emulator and Nitro status", "commandocentrum.hotel_status_desc": "Emulator and Nitro status",
"commandocentrum.hotel_alert": "Hotel Alert", "commandocentrum.hotel_alert": "Hotel Alert",
"commandocentrum.hotel_alert_desc": "Send a message to all online users", "commandocentrum.hotel_alert_desc": "Send a message to all online users",
"commandocentrum.send_alert": "Send Alert", "commandocentrum.send_alert": "Send Alert",
"commandocentrum.alert_message_placeholder": "Type your alert message here...", "commandocentrum.alert_message_placeholder": "Type your alert message here...",
"commandocentrum.emulator_logs": "Emulator Logs", "commandocentrum.emulator_logs": "Emulator Logs",
"commandocentrum.emulator_logs_desc": "Live emulator log viewer", "commandocentrum.emulator_logs_desc": "Live emulator log viewer",
"commandocentrum.emulator_control": "Emulator Control", "commandocentrum.emulator_control": "Emulator Control",
"commandocentrum.emulator_control_desc": "Full emulator control", "commandocentrum.emulator_control_desc": "Full emulator control",
"commandocentrum.start": "Start", "commandocentrum.start": "Start",
"commandocentrum.stop": "Stop", "commandocentrum.stop": "Stop",
"commandocentrum.restart": "Restart", "commandocentrum.restart": "Restart",
"commandocentrum.check": "Check", "commandocentrum.check": "Check",
"commandocentrum.version": "Version", "commandocentrum.version": "Version",
"commandocentrum.service": "Service", "commandocentrum.service": "Service",
"commandocentrum.status": "Status", "commandocentrum.status": "Status",
"commandocentrum.emulator_updates": "Emulator Updates", "commandocentrum.emulator_updates_desc": "Configure and update the emulator",
"commandocentrum.emulator_updates_desc": "Configure and update the emulator", "commandocentrum.build": "Build",
"commandocentrum.check_updates": "Check Updates", "commandocentrum.save": "Save",
"commandocentrum.build": "Build", "commandocentrum.github_url": "GitHub URL",
"commandocentrum.sql_updates": "SQL Updates", "commandocentrum.jar_direct_url": "JAR Direct URL",
"commandocentrum.save": "Save", "commandocentrum.jar_path": "JAR Path",
"commandocentrum.github_url": "GitHub URL", "commandocentrum.source_repo": "Source Repo",
"commandocentrum.jar_direct_url": "JAR Direct URL", "commandocentrum.source_path": "Source Path",
"commandocentrum.jar_path": "JAR Path", "commandocentrum.branch": "Branch",
"commandocentrum.source_repo": "Source Repo", "commandocentrum.db_host": "DB Host",
"commandocentrum.source_path": "Source Path", "commandocentrum.db_name": "DB Name",
"commandocentrum.branch": "Branch", "commandocentrum.service_name": "Service Name",
"commandocentrum.db_host": "DB Host", "commandocentrum.emulator_backups_desc": "View and restore emulator backups",
"commandocentrum.db_name": "DB Name", "commandocentrum.restore": "Restore",
"commandocentrum.service_name": "Service Name", "commandocentrum.nitro_client": "Nitro Client",
"commandocentrum.emulator_backups": "Emulator Backups", "commandocentrum.clothing_sync": "Clothing Sync",
"commandocentrum.emulator_backups_desc": "View and restore emulator backups", "commandocentrum.clothing_sync_desc": "Sync catalog clothing from FigureMap",
"commandocentrum.no_backups": "No backups available yet", "commandocentrum.sync": "Sync",
"commandocentrum.backups_auto": "Backups are automatically created on every emulator update", "commandocentrum.clothing_items": "Clothing Items",
"commandocentrum.restore": "Restore", "commandocentrum.notifications": "Notifications",
"commandocentrum.nitro_client": "Nitro Client", "commandocentrum.notifications_desc": "Email and Discord alerts",
"commandocentrum.nitro_client_desc": "Configure and update Nitro", "commandocentrum.test_discord": "Test Discord",
"commandocentrum.auto_detect": "Auto Detect", "commandocentrum.email_notifications": "Email Notifications",
"commandocentrum.generate_configs": "Generate Configs", "commandocentrum.email_address": "Email Address",
"commandocentrum.client_path": "Client Path", "commandocentrum.discord_notifications": "Discord Notifications",
"commandocentrum.renderer_path": "Renderer Path", "commandocentrum.webhook_url": "Webhook URL",
"commandocentrum.build_path": "Build Path", "commandocentrum.discord_ranks": "Ranks that receive Discord notifications",
"commandocentrum.webroot": "Webroot", "commandocentrum.discord_ranks_helper": "Leave empty for staff only (min_staff_rank)",
"commandocentrum.site_url": "Site URL", "commandocentrum.social_login": "Social Login (v1.4)",
"commandocentrum.auto_updates": "Automatic Updates", "commandocentrum.social_login_desc": "Enable social login providers",
"commandocentrum.auto_updates_desc": "Configure automatic updates", "commandocentrum.google_login": "Google Login",
"commandocentrum.enable_auto_updates": "Enable Automatic Updates", "commandocentrum.google_login_helper": "Allow users to login with Google",
"commandocentrum.schedule": "Schedule (HH:MM)", "commandocentrum.google_client_id": "Google Client ID",
"commandocentrum.days": "Days (0-6)", "commandocentrum.google_client_id_helper": "From Google Cloud Console",
"commandocentrum.clothing_sync": "Clothing Sync", "commandocentrum.google_client_secret": "Google Client Secret",
"commandocentrum.clothing_sync_desc": "Sync catalog clothing from FigureMap", "commandocentrum.discord_login": "Discord Login",
"commandocentrum.sync": "Sync", "commandocentrum.discord_login_helper": "Allow users to login with Discord",
"commandocentrum.clothing_items": "Clothing Items", "commandocentrum.discord_client_id": "Discord Client ID",
"commandocentrum.notifications": "Notifications", "commandocentrum.discord_client_id_helper": "From Discord Developer Portal",
"commandocentrum.notifications_desc": "Email and Discord alerts", "commandocentrum.discord_client_secret": "Discord Client Secret",
"commandocentrum.test_discord": "Test Discord", "commandocentrum.github_login": "GitHub Login",
"commandocentrum.email_notifications": "Email Notifications", "commandocentrum.github_login_helper": "Allow users to login with GitHub",
"commandocentrum.email_address": "Email Address", "commandocentrum.github_client_id": "GitHub Client ID",
"commandocentrum.discord_notifications": "Discord Notifications", "commandocentrum.github_client_id_helper": "From GitHub Developer Settings",
"commandocentrum.webhook_url": "Webhook URL", "commandocentrum.github_client_secret": "GitHub Client Secret",
"commandocentrum.discord_ranks": "Ranks that receive Discord notifications", "commandocentrum.staff_activity": "Staff Activity Log",
"commandocentrum.discord_ranks_helper": "Leave empty for staff only (min_staff_rank)", "commandocentrum.staff_activity_desc": "Recent staff activities in the housekeeping (v1.2)",
"commandocentrum.update_history": "Update History", "commandocentrum.recent_staff_activities": "Recent Staff Activities",
"commandocentrum.update_history_desc": "Latest system updates", "commandocentrum.last_20_actions": "Last 20 actions",
"commandocentrum.no_updates_found": "No updates found", "commandocentrum.no_staff_activities": "No staff activities recorded yet.",
"commandocentrum.social_login": "Social Login (v1.4)", "commandocentrum.staff_actions_auto": "Staff actions will appear here automatically.",
"commandocentrum.social_login_desc": "Enable social login providers", "commandocentrum.error_loading_activities": "Error loading staff activities",
"commandocentrum.google_login": "Google Login", "commandocentrum.run_migrations": "Make sure to run: php artisan migrate",
"commandocentrum.google_login_helper": "Allow users to login with Google", "commandocentrum.just_now": "Just now",
"commandocentrum.google_client_id": "Google Client ID", "commandocentrum.minutes_ago": "m ago",
"commandocentrum.google_client_id_helper": "From Google Cloud Console", "commandocentrum.hours_ago": "h ago",
"commandocentrum.google_client_secret": "Google Client Secret", "commandocentrum.days_ago": "d ago",
"commandocentrum.discord_login": "Discord Login", "commandocentrum.success": "Success",
"commandocentrum.discord_login_helper": "Allow users to login with Discord", "commandocentrum.error": "Error",
"commandocentrum.discord_client_id": "Discord Client ID", "commandocentrum.warning": "Warning",
"commandocentrum.discord_client_id_helper": "From Discord Developer Portal", "commandocentrum.info": "Info",
"commandocentrum.discord_client_secret": "Discord Client Secret", "commandocentrum.emulator_started": "Emulator started!",
"commandocentrum.github_login": "GitHub Login", "commandocentrum.emulator_start_failed": "Could not start emulator",
"commandocentrum.github_login_helper": "Allow users to login with GitHub", "commandocentrum.emulator_stopped": "Emulator stopped!",
"commandocentrum.github_client_id": "GitHub Client ID", "commandocentrum.emulator_stop_failed": "Could not stop emulator",
"commandocentrum.github_client_id_helper": "From GitHub Developer Settings", "commandocentrum.emulator_restarted": "Emulator restarted!",
"commandocentrum.github_client_secret": "GitHub Client Secret", "commandocentrum.emulator_restart_failed": "Could not restart emulator",
"commandocentrum.staff_activity": "Staff Activity Log", "commandocentrum.emulator_online": "Emulator is online and responding!",
"commandocentrum.staff_activity_desc": "Recent staff activities in the housekeeping (v1.2)", "commandocentrum.emulator_unreachable": "Emulator is not reachable via RCON",
"commandocentrum.recent_staff_activities": "Recent Staff Activities", "commandocentrum.emulator_settings_saved": "Emulator settings saved!",
"commandocentrum.last_20_actions": "Last 20 actions", "commandocentrum.alerts_saved": "Notifications saved!",
"commandocentrum.no_staff_activities": "No staff activities recorded yet.", "commandocentrum.test_sent": "Test message sent!",
"commandocentrum.staff_actions_auto": "Staff actions will appear here automatically.", "commandocentrum.webhook_empty": "Webhook URL is empty",
"commandocentrum.error_loading_activities": "Error loading staff activities", "commandocentrum.diagnostics_refreshed": "Diagnostics refreshed",
"commandocentrum.run_migrations": "Make sure to run: php artisan migrate", "commandocentrum.unknown": "Unknown",
"commandocentrum.just_now": "Just now", "commandocentrum.not_applicable": "N/A",
"commandocentrum.minutes_ago": "m ago", "commandocentrum.offline": "Offline",
"commandocentrum.hours_ago": "h ago", "commandocentrum.active": "Active",
"commandocentrum.days_ago": "d ago", "commandocentrum.inactive": "Inactive",
"commandocentrum.success": "Success", "commandocentrum.not_found": "Not found",
"commandocentrum.error": "Error", "commandocentrum.ok": "OK",
"commandocentrum.warning": "Warning", "commandocentrum.missing": "Missing",
"commandocentrum.info": "Info", "commandocentrum.jars": "JARs",
"commandocentrum.emulator_started": "Emulator started!", "commandocentrum.source": "Source",
"commandocentrum.emulator_start_failed": "Could not start emulator", "commandocentrum.method": "Method",
"commandocentrum.emulator_stopped": "Emulator stopped!", "commandocentrum.jar_download_restart": "JAR Download & Restart",
"commandocentrum.emulator_stop_failed": "Could not stop emulator", "commandocentrum.maven_build_restart": "Maven Build & Restart",
"commandocentrum.emulator_restarted": "Emulator restarted!", "commandocentrum.manual_download": "Manual: Download JAR from GitHub",
"commandocentrum.emulator_restart_failed": "Could not restart emulator", "commandocentrum.maven_pom": "Maven (pom.xml)",
"commandocentrum.emulator_online": "Emulator is online and responding!", "commandocentrum.no_pom": "No pom.xml",
"commandocentrum.emulator_unreachable": "Emulator is not reachable via RCON", "commandocentrum.update_available": "Update available",
"commandocentrum.building_emulator": "Emulator is being built from source...", "commandocentrum.up_to_date": "Up-to-date",
"commandocentrum.emulator_built": "Emulator built!", "commandocentrum.update": "Update",
"commandocentrum.build_failed": "Build failed", "commandocentrum.rebuild": "Rebuild",
"commandocentrum.configure_github_url": "Please configure the Emulator GitHub URL first", "commandocentrum.latest": "Latest",
"commandocentrum.maven_not_installed": "Maven (mvn) is not installed - cannot build", "commandocentrum.remote": "Remote",
"commandocentrum.building_maven": "Building emulator with Maven...", "commandocentrum.local": "Local",
"commandocentrum.build_success_jar": "Build successful! JAR moved to :jar. Restart the emulator.", "commandocentrum.client": "Client",
"commandocentrum.build_success": "Build successful! Restart the emulator.", "commandocentrum.renderer": "Renderer",
"commandocentrum.build_failed_logs": "Build failed - check logs", "commandocentrum.webroot_status": "Webroot",
"commandocentrum.no_pom_xml": "No pom.xml found - cannot build from source", "commandocentrum.rank": "Rank",
"commandocentrum.sql_applied": "SQL updates applied!", "radio.title": "Radio",
"commandocentrum.update_complete": "Update check completed", "radio.music": "Music",
"commandocentrum.emulator_settings_saved": "Emulator settings saved!", "radio.loading": "Loading...",
"commandocentrum.nitro_updated": "Nitro updated! Build again with \"Build\" button.", "radio.navigation_label": "Radio",
"commandocentrum.nitro_up_to_date": "Nitro is already up-to-date!", "radio.setup_page_title": "Radio Setup",
"commandocentrum.building_nitro": "Building Nitro...", "radio.setup_page_subtitle": "Configure your radio system in one go",
"commandocentrum.nitro_build_success": "Nitro build successful!", "radio.setup.success_title": "Radio Installed!",
"commandocentrum.nitro_build_warning": "Build started - check manually", "radio.setup.success_body": "Radio system has been successfully installed and configured!",
"commandocentrum.valid_url_required": "Please enter a valid URL (e.g. https://epicnabbo.nl)", "radio.setup.error_title": "Installation Failed",
"commandocentrum.configs_generated": "Configs generated & existing settings preserved!", "radio.setup.error_body": "An error occurred: :message",
"commandocentrum.config_generated_warning": "Config generated (check manually)", "radio.setup.button_label": "Install Everything",
"commandocentrum.paths_detected": "Paths detected and saved!", "radio.setup.modal_heading": "Install Radio?",
"commandocentrum.nitro_settings_saved": "Nitro settings saved!", "radio.setup.modal_description": "This will configure all radio settings with default values.",
"commandocentrum.auto_update_saved": "Auto update settings saved!", "radio.setup.modal_submit": "Yes, install!",
"commandocentrum.alerts_saved": "Notifications saved!", "radio.setup.tooltip": "Install the complete radio system",
"commandocentrum.test_sent": "Test message sent!", "radio.setup_complete": "✅ Installation Complete!",
"commandocentrum.webhook_empty": "Webhook URL is empty", "radio.what_gets_configured": "What gets configured?",
"commandocentrum.diagnostics_refreshed": "Diagnostics refreshed", "radio.radio_stream": "Radio Stream",
"commandocentrum.unknown": "Unknown", "radio.radio_stream_desc": "Set your stream URL with support for SHOUTcast, Icecast, AzureCast and other streaming platforms.",
"commandocentrum.not_applicable": "N/A", "radio.points_system": "Points System",
"commandocentrum.offline": "Offline", "radio.points_system_desc": "Let users earn points by listening, requesting songs and participating in contests.",
"commandocentrum.active": "Active", "radio.community_features": "Community Features",
"commandocentrum.inactive": "Inactive", "radio.community_features_desc": "Shouts, song requests, DJ applications and more community interactions.",
"commandocentrum.not_found": "Not found", "radio.dj_management": "DJ Management",
"commandocentrum.ok": "OK", "radio.dj_management_desc": "DJ ranks, schedule, auto-detection and Sambroadcaster/Virtual DJ integration.",
"commandocentrum.missing": "Missing", "radio.monitoring": "Stream Monitoring",
"commandocentrum.jars": "JARs", "radio.monitoring_desc": "Monitor your stream uptime with real-time monitoring.",
"commandocentrum.source": "Source", "radio.display_options": "Display Options",
"commandocentrum.method": "Method", "radio.display_options_desc": "Widget, player styles, colors and custom CSS/JS.",
"commandocentrum.jar_download_restart": "JAR Download & Restart", "radio.default_settings": "Default Settings",
"commandocentrum.maven_build_restart": "Maven Build & Restart", "radio.radio_label": "Radio",
"commandocentrum.manual_download": "Manual: Download JAR from GitHub", "radio.enabled": "Enabled",
"commandocentrum.maven_pom": "Maven (pom.xml)", "radio.points_label": "Points",
"commandocentrum.no_pom": "No pom.xml", "radio.per_min": " per min",
"commandocentrum.update_available": "Update available", "radio.daily_limit": "Daily limit",
"commandocentrum.up_to_date": "Up-to-date", "radio.shouts_label": "Shouts",
"commandocentrum.update": "Update", "radio.on": "On",
"commandocentrum.rebuild": "Rebuild", "radio.widget": "Widget",
"commandocentrum.latest": "Latest", "radio.global": "Global",
"commandocentrum.remote": "Remote", "radio.dj_apps": "DJ Applications",
"commandocentrum.local": "Local", "radio.open": "Open",
"commandocentrum.client": "Client", "radio.monitoring_label": "Monitoring",
"commandocentrum.renderer": "Renderer", "radio.contests_label": "Contests",
"commandocentrum.webroot_status": "Webroot", "radio.install_radio_system": "🚀 Install Radio System",
"commandocentrum.rank": "Rank", "radio.reset_settings": "Reset Settings",
"radio.title": "Radio", "radio.reset_confirm": "Are you sure you want to reset all radio settings?",
"radio.music": "Music", "radio.go_to_radio_settings": "Go to Radio Settings",
"radio.loading": "Loading...", "radio.open_wizard": "🎯 Open Radio Wizard",
"radio.navigation_label": "Radio", "radio.wizard_desc": "Step-by-step wizard with connection test",
"radio.setup_page_title": "Radio Setup", "radio.wizard.title": "Radio Installation Wizard",
"radio.setup_page_subtitle": "Configure your radio system in one go", "radio.wizard.step_short": "Step",
"radio.setup.success_title": "Radio Installed!", "radio.wizard.step_prefix": "Step",
"radio.setup.success_body": "Radio system has been successfully installed and configured!", "radio.wizard.of": "of",
"radio.setup.error_title": "Installation Failed", "radio.wizard.next_step": "Next Step →",
"radio.setup.error_body": "An error occurred: :message", "radio.wizard.previous_step": "← Previous Step",
"radio.setup.button_label": "Install Everything", "radio.wizard.back_to_setup": "Back to setup",
"radio.setup.modal_heading": "Install Radio?", "radio.wizard.step1_label": "Platform",
"radio.setup.modal_description": "This will configure all radio settings with default values.", "radio.wizard.step2_label": "Stream",
"radio.setup.modal_submit": "Yes, install!", "radio.wizard.step3_label": "API",
"radio.setup.tooltip": "Install the complete radio system", "radio.wizard.step4_label": "Features",
"radio.setup_complete": "✅ Installation Complete!", "radio.wizard.step5_label": "Test",
"radio.what_gets_configured": "What gets configured?", "radio.wizard.step1_subtitle": "Choose your streaming platform",
"radio.radio_stream": "Radio Stream", "radio.wizard.step2_title": "Stream Configuration",
"radio.radio_stream_desc": "Set your stream URL with support for SHOUTcast, Icecast, AzureCast and other streaming platforms.", "radio.wizard.step3_title": "API Configuration",
"radio.points_system": "Points System", "radio.wizard.step3_subtitle": "Now Playing & Listeners",
"radio.points_system_desc": "Let users earn points by listening, requesting songs and participating in contests.", "radio.wizard.step4_title": "Configure Features",
"radio.community_features": "Community Features", "radio.wizard.step4_subtitle": "Choose which radio features to enable",
"radio.community_features_desc": "Shouts, song requests, DJ applications and more community interactions.", "radio.wizard.step5_title": "Test & Install",
"radio.dj_management": "DJ Management", "radio.wizard.step5_subtitle": "Check the connection and complete the installation",
"radio.dj_management_desc": "DJ ranks, schedule, auto-detection and Sambroadcaster/Virtual DJ integration.", "radio.wizard.platform_shoutcast": "SHOUTcast",
"radio.monitoring": "Stream Monitoring", "radio.wizard.platform_shoutcast_desc": "For SHOUTcast servers. Auto-detection of now playing and listeners via stats endpoint.",
"radio.monitoring_desc": "Monitor your stream uptime with real-time monitoring.", "radio.wizard.platform_icecast": "Icecast",
"radio.display_options": "Display Options", "radio.wizard.platform_icecast_desc": "For Icecast servers. Uses status-json.xsl for auto-detection.",
"radio.display_options_desc": "Widget, player styles, colors and custom CSS/JS.", "radio.wizard.platform_azurecast": "AzureCast",
"radio.default_settings": "Default Settings", "radio.wizard.platform_azurecast_desc": "AzureCast hosting. Full API integration with now-playing, listeners and auto-configuration.",
"radio.radio_label": "Radio", "radio.wizard.platform_other": "Other",
"radio.enabled": "Enabled", "radio.wizard.platform_other_desc": "Another stream provider. Manual configuration of stream URL and API endpoints.",
"radio.points_label": "Points", "radio.wizard.shoutcast_info_title": "SHOUTcast",
"radio.per_min": " per min", "radio.wizard.shoutcast_info_desc": "Enter your SHOUTcast stream URL. The wizard will try to find the stats endpoint automatically.",
"radio.daily_limit": "Daily limit", "radio.wizard.icecast_info_title": "Icecast",
"radio.shouts_label": "Shouts", "radio.wizard.icecast_info_desc": "Enter your Icecast stream URL. The wizard uses status-json.xsl for auto-detection.",
"radio.on": "On", "radio.wizard.azurecast_info_title": "AzureCast",
"radio.widget": "Widget", "radio.wizard.azurecast_info_desc": "AzureCast stream URL + server configuration. The wizard configures everything via the AzureCast API.",
"radio.global": "Global", "radio.wizard.other_info_title": "Other Stream",
"radio.dj_apps": "DJ Applications", "radio.wizard.other_info_desc": "Enter your stream URL. You can manually configure API endpoints for now playing and listeners later.",
"radio.open": "Open", "radio.wizard.stream_url_label": "Stream URL *",
"radio.monitoring_label": "Monitoring", "radio.wizard.stream_url_hint": "The direct URL to your audio stream (MP3, AAC, OGG, etc.)",
"radio.contests_label": "Contests", "radio.wizard.stream_name_label": "Stream Name",
"radio.install_radio_system": "🚀 Install Radio System", "radio.wizard.stream_name_placeholder": "My Radio",
"radio.reset_settings": "Reset Settings", "radio.wizard.stream_name_hint": "A name for your radio stream (optional)",
"radio.reset_confirm": "Are you sure you want to reset all radio settings?", "radio.wizard.azurecast_section": "AzureCast Server Configuration",
"radio.go_to_radio_settings": "Go to Radio Settings", "radio.wizard.azurecast_base_url_label": "AzureCast Base URL",
"radio.open_wizard": "🎯 Open Radio Wizard", "radio.wizard.azurecast_base_url_hint": "The base URL of your AzureCast server. Auto-detected if left empty.",
"radio.wizard_desc": "Step-by-step wizard with connection test", "radio.wizard.azurecast_station_id_label": "Station ID",
"radio.wizard.title": "Radio Installation Wizard", "radio.wizard.azurecast_station_id_hint": "The station ID in AzureCast (default: 1)",
"radio.wizard.step_short": "Step", "radio.wizard.enable_now_playing": "Enable Now Playing",
"radio.wizard.step_prefix": "Step", "radio.wizard.now_playing_api_label": "Now Playing API URL",
"radio.wizard.of": "of", "radio.wizard.now_playing_api_hint": "API endpoint that returns the current song. Usually auto-detected.",
"radio.wizard.next_step": "Next Step →", "radio.wizard.enable_listeners": "Enable Listeners Counter",
"radio.wizard.previous_step": "← Previous Step", "radio.wizard.listeners_api_label": "Listeners API URL",
"radio.wizard.back_to_setup": "Back to setup", "radio.wizard.listeners_api_hint": "API endpoint that returns the listener count.",
"radio.wizard.step1_label": "Platform", "radio.wizard.enable_current_dj": "Show Current DJ",
"radio.wizard.step2_label": "Stream", "radio.wizard.detected": "detected!",
"radio.wizard.step3_label": "API", "radio.wizard.detected_desc": "API endpoints were automatically found and filled in.",
"radio.wizard.step4_label": "Features", "radio.wizard.not_detected": "No automatic detection",
"radio.wizard.step5_label": "Test", "radio.wizard.not_detected_desc": "Fill in the API URLs manually or skip this step.",
"radio.wizard.step1_subtitle": "Choose your streaming platform", "radio.wizard.section_community": "Community Features",
"radio.wizard.step2_title": "Stream Configuration", "radio.wizard.feature_shouts": "Shouts",
"radio.wizard.step3_title": "API Configuration", "radio.wizard.feature_shouts_desc": "Leave messages",
"radio.wizard.step3_subtitle": "Now Playing & Listeners", "radio.wizard.feature_applications": "DJ Applications",
"radio.wizard.step4_title": "Configure Features", "radio.wizard.feature_applications_desc": "Apply as DJ",
"radio.wizard.step4_subtitle": "Choose which radio features to enable", "radio.wizard.feature_requests": "Song Requests",
"radio.wizard.step5_title": "Test & Install", "radio.wizard.feature_requests_desc": "Request songs",
"radio.wizard.step5_subtitle": "Check the connection and complete the installation", "radio.wizard.section_display": "Display",
"radio.wizard.platform_shoutcast": "SHOUTcast", "radio.wizard.feature_widget": "Radio Widget",
"radio.wizard.platform_shoutcast_desc": "For SHOUTcast servers. Auto-detection of now playing and listeners via stats endpoint.", "radio.wizard.feature_widget_desc": "Mini player on the site",
"radio.wizard.platform_icecast": "Icecast", "radio.wizard.feature_widget_global": "Widget Everywhere",
"radio.wizard.platform_icecast_desc": "For Icecast servers. Uses status-json.xsl for auto-detection.", "radio.wizard.feature_widget_global_desc": "Show on all pages",
"radio.wizard.platform_azurecast": "AzureCast", "radio.wizard.widget_position_label": "Widget Position",
"radio.wizard.platform_azurecast_desc": "AzureCast hosting. Full API integration with now-playing, listeners and auto-configuration.", "radio.wizard.position_bottom_right": "Bottom Right",
"radio.wizard.platform_other": "Other", "radio.wizard.position_bottom_left": "Bottom Left",
"radio.wizard.platform_other_desc": "Another stream provider. Manual configuration of stream URL and API endpoints.", "radio.wizard.position_top_right": "Top Right",
"radio.wizard.shoutcast_info_title": "SHOUTcast", "radio.wizard.position_top_left": "Top Left",
"radio.wizard.shoutcast_info_desc": "Enter your SHOUTcast stream URL. The wizard will try to find the stats endpoint automatically.", "radio.wizard.section_gamification": "Gamification",
"radio.wizard.icecast_info_title": "Icecast", "radio.wizard.feature_points": "Points System",
"radio.wizard.icecast_info_desc": "Enter your Icecast stream URL. The wizard uses status-json.xsl for auto-detection.", "radio.wizard.feature_points_desc": "Earn points by listening",
"radio.wizard.azurecast_info_title": "AzureCast", "radio.wizard.feature_contests": "Contests",
"radio.wizard.azurecast_info_desc": "AzureCast stream URL + server configuration. The wizard configures everything via the AzureCast API.", "radio.wizard.feature_contests_desc": "Organize competitions",
"radio.wizard.other_info_title": "Other Stream", "radio.wizard.feature_giveaways": "Giveaways",
"radio.wizard.other_info_desc": "Enter your stream URL. You can manually configure API endpoints for now playing and listeners later.", "radio.wizard.feature_giveaways_desc": "Give away prizes",
"radio.wizard.stream_url_label": "Stream URL *", "radio.wizard.section_integrations": "Integrations",
"radio.wizard.stream_url_hint": "The direct URL to your audio stream (MP3, AAC, OGG, etc.)", "radio.wizard.feature_discord": "Discord Notifications",
"radio.wizard.stream_name_label": "Stream Name", "radio.wizard.feature_discord_desc": "Notifications when DJ goes live / song changes",
"radio.wizard.stream_name_placeholder": "My Radio", "radio.wizard.discord_webhook_label": "Discord Webhook URL",
"radio.wizard.stream_name_hint": "A name for your radio stream (optional)", "radio.wizard.discord_webhook_hint": "Create a webhook in your Discord server channel.",
"radio.wizard.azurecast_section": "AzureCast Server Configuration", "radio.wizard.test_title": "Test Connection",
"radio.wizard.azurecast_base_url_label": "AzureCast Base URL", "radio.wizard.test_desc": "Click Test Connection to check if your stream and APIs are reachable.",
"radio.wizard.azurecast_base_url_hint": "The base URL of your AzureCast server. Auto-detected if left empty.", "radio.wizard.test_loading": "Testing connection...",
"radio.wizard.azurecast_station_id_label": "Station ID", "radio.wizard.test_prompt": "Click the button to test the connection.",
"radio.wizard.azurecast_station_id_hint": "The station ID in AzureCast (default: 1)", "radio.wizard.test_button": "Test Connection",
"radio.wizard.enable_now_playing": "Enable Now Playing", "radio.wizard.test_retry": "Test Again",
"radio.wizard.now_playing_api_label": "Now Playing API URL", "radio.wizard.settings_overview": "Settings Overview",
"radio.wizard.now_playing_api_hint": "API endpoint that returns the current song. Usually auto-detected.", "radio.wizard.settings_overview_desc": "These are the settings that will be saved:",
"radio.wizard.enable_listeners": "Enable Listeners Counter", "radio.wizard.install_confirm": "Are you sure you want to install the radio with these settings?",
"radio.wizard.listeners_api_label": "Listeners API URL", "radio.wizard.install_button": "Install Radio",
"radio.wizard.listeners_api_hint": "API endpoint that returns the listener count.", "radio.wizard.test_result_stream": "Stream Connection",
"radio.wizard.enable_current_dj": "Show Current DJ", "radio.wizard.test_result_now_playing": "Now Playing",
"radio.wizard.detected": "detected!", "radio.wizard.test_result_listeners": "Listeners",
"radio.wizard.detected_desc": "API endpoints were automatically found and filled in.", "radio.wizard.status_success": "Success",
"radio.wizard.not_detected": "No automatic detection", "radio.wizard.status_warning": "Warning",
"radio.wizard.not_detected_desc": "Fill in the API URLs manually or skip this step.", "radio.wizard.status_error": "Error",
"radio.wizard.section_community": "Community Features", "radio.wizard.status_skipped": "Skipped",
"radio.wizard.feature_shouts": "Shouts", "radio.wizard.status_untested": "Not tested",
"radio.wizard.feature_shouts_desc": "Leave messages", "radio.wizard.content_type": "Content-Type",
"radio.wizard.feature_applications": "DJ Applications", "radio.wizard.http_status": "HTTP Status",
"radio.wizard.feature_applications_desc": "Apply as DJ", "radio.wizard.song": "Song",
"radio.wizard.feature_requests": "Song Requests", "radio.wizard.artist": "Artist",
"radio.wizard.feature_requests_desc": "Request songs", "radio.wizard.listeners": "Listeners",
"radio.wizard.section_display": "Display", "radio.wizard.api_url": "API URL",
"radio.wizard.feature_widget": "Radio Widget", "radio.wizard.test_stream_ok": "Stream is reachable! You can install the radio.",
"radio.wizard.feature_widget_desc": "Mini player on the site", "radio.wizard.test_stream_fail": "Stream is not reachable. Check the URL and try again.",
"radio.wizard.feature_widget_global": "Widget Everywhere", "radio.wizard.test_not_run": "Not tested yet.",
"radio.wizard.feature_widget_global_desc": "Show on all pages", "radio.wizard.test_connection_fail": "Could not run test: ",
"radio.wizard.widget_position_label": "Widget Position", "radio.wizard.error": "Error",
"radio.wizard.position_bottom_right": "Bottom Right", "radio.wizard.unknown_error": "Unknown error"
"radio.wizard.position_bottom_left": "Bottom Left",
"radio.wizard.position_top_right": "Top Right",
"radio.wizard.position_top_left": "Top Left",
"radio.wizard.section_gamification": "Gamification",
"radio.wizard.feature_points": "Points System",
"radio.wizard.feature_points_desc": "Earn points by listening",
"radio.wizard.feature_contests": "Contests",
"radio.wizard.feature_contests_desc": "Organize competitions",
"radio.wizard.feature_giveaways": "Giveaways",
"radio.wizard.feature_giveaways_desc": "Give away prizes",
"radio.wizard.section_integrations": "Integrations",
"radio.wizard.feature_discord": "Discord Notifications",
"radio.wizard.feature_discord_desc": "Notifications when DJ goes live / song changes",
"radio.wizard.discord_webhook_label": "Discord Webhook URL",
"radio.wizard.discord_webhook_hint": "Create a webhook in your Discord server channel.",
"radio.wizard.test_title": "Test Connection",
"radio.wizard.test_desc": "Click Test Connection to check if your stream and APIs are reachable.",
"radio.wizard.test_loading": "Testing connection...",
"radio.wizard.test_prompt": "Click the button to test the connection.",
"radio.wizard.test_button": "Test Connection",
"radio.wizard.test_retry": "Test Again",
"radio.wizard.settings_overview": "Settings Overview",
"radio.wizard.settings_overview_desc": "These are the settings that will be saved:",
"radio.wizard.install_confirm": "Are you sure you want to install the radio with these settings?",
"radio.wizard.install_button": "Install Radio",
"radio.wizard.test_result_stream": "Stream Connection",
"radio.wizard.test_result_now_playing": "Now Playing",
"radio.wizard.test_result_listeners": "Listeners",
"radio.wizard.status_success": "Success",
"radio.wizard.status_warning": "Warning",
"radio.wizard.status_error": "Error",
"radio.wizard.status_skipped": "Skipped",
"radio.wizard.status_untested": "Not tested",
"radio.wizard.content_type": "Content-Type",
"radio.wizard.http_status": "HTTP Status",
"radio.wizard.song": "Song",
"radio.wizard.artist": "Artist",
"radio.wizard.listeners": "Listeners",
"radio.wizard.api_url": "API URL",
"radio.wizard.test_stream_ok": "Stream is reachable! You can install the radio.",
"radio.wizard.test_stream_fail": "Stream is not reachable. Check the URL and try again.",
"radio.wizard.test_not_run": "Not tested yet.",
"radio.wizard.test_connection_fail": "Could not run test: ",
"radio.wizard.error": "Error",
"radio.wizard.unknown_error": "Unknown error"
} }
Executable → Regular
+1509 -1513
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1509 -1513
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1504 -1508
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1509 -1513
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+305 -350
View File
@@ -1,352 +1,307 @@
{ {
"commandocentrum.live_status": "Live Status", "commandocentrum.live_status": "Live Status",
"commandocentrum.live_status_desc": "Real-time hotel statistieken", "commandocentrum.live_status_desc": "Real-time hotel statistieken",
"commandocentrum.online": "Online", "commandocentrum.online": "Online",
"commandocentrum.emulator": "Emulator", "commandocentrum.emulator": "Emulator",
"commandocentrum.database": "Database", "commandocentrum.database": "Database",
"commandocentrum.load": "Load", "commandocentrum.load": "Load",
"commandocentrum.server_info": "Server Informatie", "commandocentrum.server_info": "Server Informatie",
"commandocentrum.server_info_desc": "Gedetailleerde server status", "commandocentrum.server_info_desc": "Gedetailleerde server status",
"commandocentrum.php_laravel": "PHP & Laravel", "commandocentrum.php_laravel": "PHP & Laravel",
"commandocentrum.memory_disk": "Memory & Disk", "commandocentrum.memory_disk": "Memory & Disk",
"commandocentrum.memory": "Memory", "commandocentrum.memory": "Memory",
"commandocentrum.disk": "Disk", "commandocentrum.disk": "Disk",
"commandocentrum.uptime": "Uptime", "commandocentrum.uptime": "Uptime",
"commandocentrum.system_health": "Systeem Gezondheid", "commandocentrum.system_health": "Systeem Gezondheid",
"commandocentrum.system_health_desc": "Automatische systeem diagnostiek", "commandocentrum.system_health_desc": "Automatische systeem diagnostiek",
"commandocentrum.refresh": "Vernieuwen", "commandocentrum.refresh": "Vernieuwen",
"commandocentrum.healthy": "Gezond", "commandocentrum.healthy": "Gezond",
"commandocentrum.warnings": "Waarschuwingen", "commandocentrum.warnings": "Waarschuwingen",
"commandocentrum.errors": "Fouten", "commandocentrum.errors": "Fouten",
"commandocentrum.system_status": "Systeem Status", "commandocentrum.system_status": "Systeem Status",
"commandocentrum.critical_issues": "Kritieke Problemen", "commandocentrum.critical_issues": "Kritieke Problemen",
"commandocentrum.hotel_status": "Hotel Status", "commandocentrum.hotel_status": "Hotel Status",
"commandocentrum.hotel_status_desc": "Emulator en Nitro status", "commandocentrum.hotel_status_desc": "Emulator en Nitro status",
"commandocentrum.hotel_alert": "Hotel Alert", "commandocentrum.hotel_alert": "Hotel Alert",
"commandocentrum.hotel_alert_desc": "Stuur een bericht naar alle online gebruikers", "commandocentrum.hotel_alert_desc": "Stuur een bericht naar alle online gebruikers",
"commandocentrum.send_alert": "Verstuur Alert", "commandocentrum.send_alert": "Verstuur Alert",
"commandocentrum.alert_message_placeholder": "Typ hier je alert bericht...", "commandocentrum.alert_message_placeholder": "Typ hier je alert bericht...",
"commandocentrum.emulator_logs": "Emulator Logs", "commandocentrum.emulator_logs": "Emulator Logs",
"commandocentrum.emulator_logs_desc": "Live emulator log viewer", "commandocentrum.emulator_logs_desc": "Live emulator log viewer",
"commandocentrum.emulator_control": "Emulator Control", "commandocentrum.emulator_control": "Emulator Control",
"commandocentrum.emulator_control_desc": "Volledige emulator controle", "commandocentrum.emulator_control_desc": "Volledige emulator controle",
"commandocentrum.start": "Start", "commandocentrum.start": "Start",
"commandocentrum.stop": "Stop", "commandocentrum.stop": "Stop",
"commandocentrum.restart": "Restart", "commandocentrum.restart": "Restart",
"commandocentrum.check": "Check", "commandocentrum.check": "Check",
"commandocentrum.version": "Versie", "commandocentrum.version": "Versie",
"commandocentrum.service": "Service", "commandocentrum.service": "Service",
"commandocentrum.status": "Status", "commandocentrum.status": "Status",
"commandocentrum.emulator_updates": "Emulator Updates", "commandocentrum.emulator_updates_desc": "Configureer en update de emulator",
"commandocentrum.emulator_updates_desc": "Configureer en update de emulator", "commandocentrum.build": "Bouwen",
"commandocentrum.check_updates": "Check Updates", "commandocentrum.save": "Opslaan",
"commandocentrum.build": "Bouwen", "commandocentrum.github_url": "GitHub URL",
"commandocentrum.sql_updates": "SQL Updates", "commandocentrum.jar_direct_url": "JAR Direct URL",
"commandocentrum.save": "Opslaan", "commandocentrum.jar_path": "JAR Pad",
"commandocentrum.github_url": "GitHub URL", "commandocentrum.source_repo": "Source Repo",
"commandocentrum.jar_direct_url": "JAR Direct URL", "commandocentrum.source_path": "Source Pad",
"commandocentrum.jar_path": "JAR Pad", "commandocentrum.branch": "Branch",
"commandocentrum.source_repo": "Source Repo", "commandocentrum.db_host": "DB Host",
"commandocentrum.source_path": "Source Pad", "commandocentrum.db_name": "DB Naam",
"commandocentrum.branch": "Branch", "commandocentrum.service_name": "Service Naam",
"commandocentrum.db_host": "DB Host", "commandocentrum.emulator_backups_desc": "Bekijk en herstel emulator backups",
"commandocentrum.db_name": "DB Naam", "commandocentrum.restore": "Herstellen",
"commandocentrum.service_name": "Service Naam", "commandocentrum.nitro_client": "Nitro Client",
"commandocentrum.emulator_backups": "Emulator Backups", "commandocentrum.clothing_sync": "Kleding Sync",
"commandocentrum.emulator_backups_desc": "Bekijk en herstel emulator backups", "commandocentrum.clothing_sync_desc": "Sync catalogus kleding uit FigureMap",
"commandocentrum.no_backups": "Nog geen backups beschikbaar", "commandocentrum.sync": "Sync",
"commandocentrum.backups_auto": "Backups worden automatisch aangemaakt bij elke emulator update", "commandocentrum.clothing_items": "Kleding Items",
"commandocentrum.restore": "Herstellen", "commandocentrum.notifications": "Meldingen",
"commandocentrum.nitro_client": "Nitro Client", "commandocentrum.notifications_desc": "E-mail en Discord alerts",
"commandocentrum.nitro_client_desc": "Configureer en update Nitro", "commandocentrum.test_discord": "Test Discord",
"commandocentrum.auto_detect": "Auto Detect", "commandocentrum.email_notifications": "E-mail Meldingen",
"commandocentrum.generate_configs": "Genereer Configs", "commandocentrum.email_address": "E-mail Adres",
"commandocentrum.client_path": "Client Pad", "commandocentrum.discord_notifications": "Discord Meldingen",
"commandocentrum.renderer_path": "Renderer Pad", "commandocentrum.webhook_url": "Webhook URL",
"commandocentrum.build_path": "Build Pad", "commandocentrum.discord_ranks": "Ranks die Discord notificatie krijgen",
"commandocentrum.webroot": "Webroot", "commandocentrum.discord_ranks_helper": "Laat leeg voor alleen staff (min_staff_rank)",
"commandocentrum.site_url": "Site URL", "commandocentrum.social_login": "Social Login (v1.4)",
"commandocentrum.auto_updates": "Automatische Updates", "commandocentrum.social_login_desc": "Enable social login providers",
"commandocentrum.auto_updates_desc": "Configureer automatische updates", "commandocentrum.google_login": "Google Login",
"commandocentrum.enable_auto_updates": "Automatische Updates Inschakelen", "commandocentrum.google_login_helper": "Allow users to login with Google",
"commandocentrum.schedule": "Schema (HH:MM)", "commandocentrum.google_client_id": "Google Client ID",
"commandocentrum.days": "Dagen (0-6)", "commandocentrum.google_client_id_helper": "From Google Cloud Console",
"commandocentrum.clothing_sync": "Kleding Sync", "commandocentrum.google_client_secret": "Google Client Secret",
"commandocentrum.clothing_sync_desc": "Sync catalogus kleding uit FigureMap", "commandocentrum.discord_login": "Discord Login",
"commandocentrum.sync": "Sync", "commandocentrum.discord_login_helper": "Allow users to login with Discord",
"commandocentrum.clothing_items": "Kleding Items", "commandocentrum.discord_client_id": "Discord Client ID",
"commandocentrum.notifications": "Meldingen", "commandocentrum.discord_client_id_helper": "From Discord Developer Portal",
"commandocentrum.notifications_desc": "E-mail en Discord alerts", "commandocentrum.discord_client_secret": "Discord Client Secret",
"commandocentrum.test_discord": "Test Discord", "commandocentrum.github_login": "GitHub Login",
"commandocentrum.email_notifications": "E-mail Meldingen", "commandocentrum.github_login_helper": "Allow users to login with GitHub",
"commandocentrum.email_address": "E-mail Adres", "commandocentrum.github_client_id": "GitHub Client ID",
"commandocentrum.discord_notifications": "Discord Meldingen", "commandocentrum.github_client_id_helper": "From GitHub Developer Settings",
"commandocentrum.webhook_url": "Webhook URL", "commandocentrum.github_client_secret": "GitHub Client Secret",
"commandocentrum.discord_ranks": "Ranks die Discord notificatie krijgen", "commandocentrum.staff_activity": "Staff Activity Log",
"commandocentrum.discord_ranks_helper": "Laat leeg voor alleen staff (min_staff_rank)", "commandocentrum.staff_activity_desc": "Recent staff activities in the housekeeping (v1.2)",
"commandocentrum.update_history": "Update Geschiedenis", "commandocentrum.recent_staff_activities": "Recent Staff Activities",
"commandocentrum.update_history_desc": "Laatste systeem updates", "commandocentrum.last_20_actions": "Last 20 actions",
"commandocentrum.no_updates_found": "Geen updates gevonden", "commandocentrum.no_staff_activities": "No staff activities recorded yet.",
"commandocentrum.social_login": "Social Login (v1.4)", "commandocentrum.staff_actions_auto": "Staff actions will appear here automatically.",
"commandocentrum.social_login_desc": "Enable social login providers", "commandocentrum.error_loading_activities": "Error loading staff activities",
"commandocentrum.google_login": "Google Login", "commandocentrum.run_migrations": "Make sure to run: php artisan migrate",
"commandocentrum.google_login_helper": "Allow users to login with Google", "commandocentrum.just_now": "Just now",
"commandocentrum.google_client_id": "Google Client ID", "commandocentrum.minutes_ago": "m ago",
"commandocentrum.google_client_id_helper": "From Google Cloud Console", "commandocentrum.hours_ago": "h ago",
"commandocentrum.google_client_secret": "Google Client Secret", "commandocentrum.days_ago": "d ago",
"commandocentrum.discord_login": "Discord Login", "commandocentrum.success": "Success",
"commandocentrum.discord_login_helper": "Allow users to login with Discord", "commandocentrum.error": "Error",
"commandocentrum.discord_client_id": "Discord Client ID", "commandocentrum.warning": "Warning",
"commandocentrum.discord_client_id_helper": "From Discord Developer Portal", "commandocentrum.info": "Info",
"commandocentrum.discord_client_secret": "Discord Client Secret", "commandocentrum.emulator_started": "Emulator gestart!",
"commandocentrum.github_login": "GitHub Login", "commandocentrum.emulator_start_failed": "Kon emulator niet starten",
"commandocentrum.github_login_helper": "Allow users to login with GitHub", "commandocentrum.emulator_stopped": "Emulator gestopt!",
"commandocentrum.github_client_id": "GitHub Client ID", "commandocentrum.emulator_stop_failed": "Kon emulator niet stoppen",
"commandocentrum.github_client_id_helper": "From GitHub Developer Settings", "commandocentrum.emulator_restarted": "Emulator herstart!",
"commandocentrum.github_client_secret": "GitHub Client Secret", "commandocentrum.emulator_restart_failed": "Kon emulator niet herstarten",
"commandocentrum.staff_activity": "Staff Activity Log", "commandocentrum.emulator_online": "Emulator is online en reageert!",
"commandocentrum.staff_activity_desc": "Recent staff activities in the housekeeping (v1.2)", "commandocentrum.emulator_unreachable": "Emulator is niet bereikbaar via RCON",
"commandocentrum.recent_staff_activities": "Recent Staff Activities", "commandocentrum.emulator_settings_saved": "Emulator instellingen opgeslagen!",
"commandocentrum.last_20_actions": "Last 20 actions", "commandocentrum.alerts_saved": "Meldingen opgeslagen!",
"commandocentrum.no_staff_activities": "No staff activities recorded yet.", "commandocentrum.test_sent": "Test bericht verzonden!",
"commandocentrum.staff_actions_auto": "Staff actions will appear here automatically.", "commandocentrum.webhook_empty": "Webhook URL is leeg",
"commandocentrum.error_loading_activities": "Error loading staff activities", "commandocentrum.diagnostics_refreshed": "Diagnostiek vernieuwd",
"commandocentrum.run_migrations": "Make sure to run: php artisan migrate", "commandocentrum.unknown": "Unknown",
"commandocentrum.just_now": "Just now", "commandocentrum.not_applicable": "N/B",
"commandocentrum.minutes_ago": "m ago", "commandocentrum.offline": "Offline",
"commandocentrum.hours_ago": "h ago", "commandocentrum.active": "Active",
"commandocentrum.days_ago": "d ago", "commandocentrum.inactive": "Inactive",
"commandocentrum.success": "Success", "commandocentrum.not_found": "Niet gevonden",
"commandocentrum.error": "Error", "commandocentrum.ok": "OK",
"commandocentrum.warning": "Warning", "commandocentrum.missing": "Ontbreekt",
"commandocentrum.info": "Info", "commandocentrum.jars": "JARs",
"commandocentrum.emulator_started": "Emulator gestart!", "commandocentrum.source": "Source",
"commandocentrum.emulator_start_failed": "Kon emulator niet starten", "commandocentrum.method": "Methode",
"commandocentrum.emulator_stopped": "Emulator gestopt!", "commandocentrum.jar_download_restart": "JAR Download & Herstart",
"commandocentrum.emulator_stop_failed": "Kon emulator niet stoppen", "commandocentrum.maven_build_restart": "Maven Build & Herstart",
"commandocentrum.emulator_restarted": "Emulator herstart!", "commandocentrum.manual_download": "Handmatig: Download JAR van GitHub",
"commandocentrum.emulator_restart_failed": "Kon emulator niet herstarten", "commandocentrum.maven_pom": "Maven (pom.xml)",
"commandocentrum.emulator_online": "Emulator is online en reageert!", "commandocentrum.no_pom": "Geen pom.xml",
"commandocentrum.emulator_unreachable": "Emulator is niet bereikbaar via RCON", "commandocentrum.update_available": "Update beschikbaar",
"commandocentrum.building_emulator": "Emulator wordt gebouwd vanaf source...", "commandocentrum.up_to_date": "Up-to-date",
"commandocentrum.emulator_built": "Emulator gebouwd!", "commandocentrum.update": "Updaten",
"commandocentrum.build_failed": "Build mislukt", "commandocentrum.rebuild": "Herbouwen",
"commandocentrum.configure_github_url": "Configureer eerst de Emulator GitHub URL", "commandocentrum.latest": "Latest",
"commandocentrum.maven_not_installed": "Maven (mvn) is niet geïnstalleerd - kan niet bouwen", "commandocentrum.remote": "Remote",
"commandocentrum.building_maven": "Building emulator with Maven...", "commandocentrum.local": "Local",
"commandocentrum.build_success_jar": "Build succesvol! JAR verplaatst naar :jar. Herstart de emulator.", "commandocentrum.client": "Client",
"commandocentrum.build_success": "Build succesvol! Herstart de emulator.", "commandocentrum.renderer": "Renderer",
"commandocentrum.build_failed_logs": "Build mislukt - controleer logs", "commandocentrum.webroot_status": "Webroot",
"commandocentrum.no_pom_xml": "Geen pom.xml gevonden - kan niet bouwen vanaf source", "commandocentrum.rank": "Rank",
"commandocentrum.sql_applied": "SQL updates toegepast!", "radio.title": "Radio",
"commandocentrum.update_complete": "Update controle voltooid", "radio.music": "Muziek",
"commandocentrum.emulator_settings_saved": "Emulator instellingen opgeslagen!", "radio.loading": "Laden...",
"commandocentrum.nitro_updated": "Nitro bijgewerkt! Build opnieuw met \"Build\" knop.", "radio.navigation_label": "Radio",
"commandocentrum.nitro_up_to_date": "Nitro is al up-to-date!", "radio.setup_page_title": "Radio Setup",
"commandocentrum.building_nitro": "Building Nitro...", "radio.setup_page_subtitle": "Configureer je radio systeem in één keer",
"commandocentrum.nitro_build_success": "Nitro build succesvol!", "radio.setup.success_title": "Radio Geïnstalleerd!",
"commandocentrum.nitro_build_warning": "Build gestart - controleer handmatig", "radio.setup.success_body": "Radio systeem is succesvol geïnstalleerd en geconfigureerd!",
"commandocentrum.valid_url_required": "Voer een geldige URL in (bijv. https://epicnabbo.nl)", "radio.setup.error_title": "Installatie Mislukt",
"commandocentrum.configs_generated": "Configs gegenereerd & bestaande instellingen behouden!", "radio.setup.error_body": "Er is een fout opgetreden: :message",
"commandocentrum.config_generated_warning": "Config gegenereerd (controleer handmatig)", "radio.setup.button_label": "Alles Installeren",
"commandocentrum.paths_detected": "Paths gedetecteerd en opgeslagen!", "radio.setup.modal_heading": "Radio Installeren?",
"commandocentrum.nitro_settings_saved": "Nitro instellingen opgeslagen!", "radio.setup.modal_description": "Dit zal alle radio instellingen configureren met standaard waarden.",
"commandocentrum.auto_update_saved": "Auto update instellingen opgeslagen!", "radio.setup.modal_submit": "Ja, installeer!",
"commandocentrum.alerts_saved": "Meldingen opgeslagen!", "radio.setup.tooltip": "Installeer het complete radio systeem",
"commandocentrum.test_sent": "Test bericht verzonden!", "radio.setup_complete": "✅ Installatie Voltooid!",
"commandocentrum.webhook_empty": "Webhook URL is leeg", "radio.what_gets_configured": "Wat wordt er geconfigureerd?",
"commandocentrum.diagnostics_refreshed": "Diagnostiek vernieuwd", "radio.radio_stream": "Radio Stream",
"commandocentrum.unknown": "Unknown", "radio.radio_stream_desc": "Stel je stream URL in met ondersteuning voor SHOUTcast, Icecast, AzureCast en andere streaming platforms.",
"commandocentrum.not_applicable": "N/B", "radio.points_system": "Punten Systeem",
"commandocentrum.offline": "Offline", "radio.points_system_desc": "Laat gebruikers punten verdienen door te luisteren, nummers aan te vragen en deel te nemen aan contests.",
"commandocentrum.active": "Active", "radio.community_features": "Community Functies",
"commandocentrum.inactive": "Inactive", "radio.community_features_desc": "Shouts, song requests, DJ aanmeldingen en meer community interacties.",
"commandocentrum.not_found": "Niet gevonden", "radio.dj_management": "DJ Beheer",
"commandocentrum.ok": "OK", "radio.dj_management_desc": "DJ ranks, schema, auto-detectie en Sambroadcaster/Virtual DJ integratie.",
"commandocentrum.missing": "Ontbreekt", "radio.monitoring": "Stream Monitoring",
"commandocentrum.jars": "JARs", "radio.monitoring_desc": "Houd je stream uptime in de gaten met real-time monitoring.",
"commandocentrum.source": "Source", "radio.display_options": "Weergave Opties",
"commandocentrum.method": "Methode", "radio.display_options_desc": "Widget, player stijlen, kleuren en aanpasbare CSS/JS.",
"commandocentrum.jar_download_restart": "JAR Download & Herstart", "radio.default_settings": "Standaard Instellingen",
"commandocentrum.maven_build_restart": "Maven Build & Herstart", "radio.radio_label": "Radio",
"commandocentrum.manual_download": "Handmatig: Download JAR van GitHub", "radio.enabled": "Ingeschakeld",
"commandocentrum.maven_pom": "Maven (pom.xml)", "radio.points_label": "Punten",
"commandocentrum.no_pom": "Geen pom.xml", "radio.per_min": " per min",
"commandocentrum.update_available": "Update beschikbaar", "radio.daily_limit": "Dagelijkse limiet",
"commandocentrum.up_to_date": "Up-to-date", "radio.shouts_label": "Shouts",
"commandocentrum.update": "Updaten", "radio.on": "Aan",
"commandocentrum.rebuild": "Herbouwen", "radio.widget": "Widget",
"commandocentrum.latest": "Latest", "radio.global": "Globaal",
"commandocentrum.remote": "Remote", "radio.dj_apps": "DJ Aanmeldingen",
"commandocentrum.local": "Local", "radio.open": "Open",
"commandocentrum.client": "Client", "radio.monitoring_label": "Monitoring",
"commandocentrum.renderer": "Renderer", "radio.contests_label": "Contesten",
"commandocentrum.webroot_status": "Webroot", "radio.install_radio_system": "🚀 Radio Systeem Installeren",
"commandocentrum.rank": "Rank", "radio.reset_settings": "Instellingen Resetten",
"radio.title": "Radio", "radio.reset_confirm": "Weet je zeker dat je alle radio instellingen wilt resetten?",
"radio.music": "Muziek", "radio.go_to_radio_settings": "Naar Radio Instellingen",
"radio.loading": "Laden...", "radio.open_wizard": "🎯 Open Radio Wizard",
"radio.navigation_label": "Radio", "radio.wizard_desc": "Stap-voor-stap wizard met verbindingstest",
"radio.setup_page_title": "Radio Setup", "radio.wizard.title": "Radio Installatie Wizard",
"radio.setup_page_subtitle": "Configureer je radio systeem in één keer", "radio.wizard.step_short": "Stap",
"radio.setup.success_title": "Radio Geïnstalleerd!", "radio.wizard.step_prefix": "Stap",
"radio.setup.success_body": "Radio systeem is succesvol geïnstalleerd en geconfigureerd!", "radio.wizard.of": "van",
"radio.setup.error_title": "Installatie Mislukt", "radio.wizard.next_step": "Volgende Stap →",
"radio.setup.error_body": "Er is een fout opgetreden: :message", "radio.wizard.previous_step": "← Vorige Stap",
"radio.setup.button_label": "Alles Installeren", "radio.wizard.back_to_setup": "Terug naar setup",
"radio.setup.modal_heading": "Radio Installeren?", "radio.wizard.step1_label": "Platform",
"radio.setup.modal_description": "Dit zal alle radio instellingen configureren met standaard waarden.", "radio.wizard.step2_label": "Stream",
"radio.setup.modal_submit": "Ja, installeer!", "radio.wizard.step3_label": "API",
"radio.setup.tooltip": "Installeer het complete radio systeem", "radio.wizard.step4_label": "Functies",
"radio.setup_complete": "✅ Installatie Voltooid!", "radio.wizard.step5_label": "Testen",
"radio.what_gets_configured": "Wat wordt er geconfigureerd?", "radio.wizard.step1_subtitle": "Kies je streaming platform",
"radio.radio_stream": "Radio Stream", "radio.wizard.step2_title": "Stream Configuratie",
"radio.radio_stream_desc": "Stel je stream URL in met ondersteuning voor SHOUTcast, Icecast, AzureCast en andere streaming platforms.", "radio.wizard.step3_title": "API Configuratie",
"radio.points_system": "Punten Systeem", "radio.wizard.step3_subtitle": "Now Playing & Luisteraars",
"radio.points_system_desc": "Laat gebruikers punten verdienen door te luisteren, nummers aan te vragen en deel te nemen aan contests.", "radio.wizard.step4_title": "Functies Configureren",
"radio.community_features": "Community Functies", "radio.wizard.step4_subtitle": "Kies welke radio functies je wilt inschakelen",
"radio.community_features_desc": "Shouts, song requests, DJ aanmeldingen en meer community interacties.", "radio.wizard.step5_title": "Test & Installeren",
"radio.dj_management": "DJ Beheer", "radio.wizard.step5_subtitle": "Controleer de verbinding en voltooi de installatie",
"radio.dj_management_desc": "DJ ranks, schema, auto-detectie en Sambroadcaster/Virtual DJ integratie.", "radio.wizard.platform_shoutcast": "SHOUTcast",
"radio.monitoring": "Stream Monitoring", "radio.wizard.platform_shoutcast_desc": "Geschikt voor SHOUTcast servers. Automatische detectie van nu afspelen en luisteraars via stats endpoint.",
"radio.monitoring_desc": "Houd je stream uptime in de gaten met real-time monitoring.", "radio.wizard.platform_icecast": "Icecast",
"radio.display_options": "Weergave Opties", "radio.wizard.platform_icecast_desc": "Geschikt voor Icecast servers. Gebruikt status-json.xsl voor automatische detectie.",
"radio.display_options_desc": "Widget, player stijlen, kleuren en aanpasbare CSS/JS.", "radio.wizard.platform_azurecast": "AzureCast",
"radio.default_settings": "Standaard Instellingen", "radio.wizard.platform_azurecast_desc": "AzureCast hosting. Volledige API integratie met now-playing, listeners en auto-configuratie.",
"radio.radio_label": "Radio", "radio.wizard.platform_other": "Anders",
"radio.enabled": "Ingeschakeld", "radio.wizard.platform_other_desc": "Een andere stream provider. Handmatige configuratie van stream URL en API endpoints.",
"radio.points_label": "Punten", "radio.wizard.shoutcast_info_title": "SHOUTcast",
"radio.per_min": " per min", "radio.wizard.shoutcast_info_desc": "Voer je SHOUTcast stream URL in. De wizard probeert automatisch de stats endpoint te vinden.",
"radio.daily_limit": "Dagelijkse limiet", "radio.wizard.icecast_info_title": "Icecast",
"radio.shouts_label": "Shouts", "radio.wizard.icecast_info_desc": "Voer je Icecast stream URL in. De wizard gebruikt status-json.xsl voor automatische detectie.",
"radio.on": "Aan", "radio.wizard.azurecast_info_title": "AzureCast",
"radio.widget": "Widget", "radio.wizard.azurecast_info_desc": "AzureCast stream URL + server configuratie. De wizard configureert alles via de AzureCast API.",
"radio.global": "Globaal", "radio.wizard.other_info_title": "Andere Stream",
"radio.dj_apps": "DJ Aanmeldingen", "radio.wizard.other_info_desc": "Voer je stream URL in. Je kunt later handmatig API endpoints configureren voor nu afspelen en luisteraars.",
"radio.open": "Open", "radio.wizard.stream_url_label": "Stream URL *",
"radio.monitoring_label": "Monitoring", "radio.wizard.stream_url_hint": "De directe URL naar je audiostreem (MP3, AAC, OGG, etc.)",
"radio.contests_label": "Contesten", "radio.wizard.stream_name_label": "Stream Naam",
"radio.install_radio_system": "🚀 Radio Systeem Installeren", "radio.wizard.stream_name_placeholder": "Mijn Radio",
"radio.reset_settings": "Instellingen Resetten", "radio.wizard.stream_name_hint": "Een naam voor je radiostream (optioneel)",
"radio.reset_confirm": "Weet je zeker dat je alle radio instellingen wilt resetten?", "radio.wizard.azurecast_section": "AzureCast Server Configuratie",
"radio.go_to_radio_settings": "Naar Radio Instellingen", "radio.wizard.azurecast_base_url_label": "AzureCast Basis URL",
"radio.open_wizard": "🎯 Open Radio Wizard", "radio.wizard.azurecast_base_url_hint": "De basis URL van je AzureCast server. Wordt automatisch gedetecteerd als leeg gelaten.",
"radio.wizard_desc": "Stap-voor-stap wizard met verbindingstest", "radio.wizard.azurecast_station_id_label": "Station ID",
"radio.wizard.title": "Radio Installatie Wizard", "radio.wizard.azurecast_station_id_hint": "Het station ID in AzureCast (standaard: 1)",
"radio.wizard.step_short": "Stap", "radio.wizard.enable_now_playing": "Nu Afspelen inschakelen",
"radio.wizard.step_prefix": "Stap", "radio.wizard.now_playing_api_label": "Now Playing API URL",
"radio.wizard.of": "van", "radio.wizard.now_playing_api_hint": "API endpoint dat het huidige nummer teruggeeft. Meestal automatisch gedetecteerd.",
"radio.wizard.next_step": "Volgende Stap →", "radio.wizard.enable_listeners": "Luisteraars teller inschakelen",
"radio.wizard.previous_step": "← Vorige Stap", "radio.wizard.listeners_api_label": "Listeners API URL",
"radio.wizard.back_to_setup": "Terug naar setup", "radio.wizard.listeners_api_hint": "API endpoint dat het aantal luisteraars teruggeeft.",
"radio.wizard.step1_label": "Platform", "radio.wizard.enable_current_dj": "Huidige DJ tonen",
"radio.wizard.step2_label": "Stream", "radio.wizard.detected": "gedetecteerd!",
"radio.wizard.step3_label": "API", "radio.wizard.detected_desc": "API endpoints zijn automatisch gevonden en ingevuld.",
"radio.wizard.step4_label": "Functies", "radio.wizard.not_detected": "Geen automatische detectie",
"radio.wizard.step5_label": "Testen", "radio.wizard.not_detected_desc": "Vul de API URLs handmatig in of sla deze stap over.",
"radio.wizard.step1_subtitle": "Kies je streaming platform", "radio.wizard.section_community": "Community Functies",
"radio.wizard.step2_title": "Stream Configuratie", "radio.wizard.feature_shouts": "Shouts",
"radio.wizard.step3_title": "API Configuratie", "radio.wizard.feature_shouts_desc": "Berichten achterlaten",
"radio.wizard.step3_subtitle": "Now Playing & Luisteraars", "radio.wizard.feature_applications": "DJ Aanmeldingen",
"radio.wizard.step4_title": "Functies Configureren", "radio.wizard.feature_applications_desc": "Solliciteren als DJ",
"radio.wizard.step4_subtitle": "Kies welke radio functies je wilt inschakelen", "radio.wizard.feature_requests": "Song Verzoeken",
"radio.wizard.step5_title": "Test & Installeren", "radio.wizard.feature_requests_desc": "Nummers aanvragen",
"radio.wizard.step5_subtitle": "Controleer de verbinding en voltooi de installatie", "radio.wizard.section_display": "Weergave",
"radio.wizard.platform_shoutcast": "SHOUTcast", "radio.wizard.feature_widget": "Radio Widget",
"radio.wizard.platform_shoutcast_desc": "Geschikt voor SHOUTcast servers. Automatische detectie van nu afspelen en luisteraars via stats endpoint.", "radio.wizard.feature_widget_desc": "Miniplayer op de site",
"radio.wizard.platform_icecast": "Icecast", "radio.wizard.feature_widget_global": "Widget Overal",
"radio.wizard.platform_icecast_desc": "Geschikt voor Icecast servers. Gebruikt status-json.xsl voor automatische detectie.", "radio.wizard.feature_widget_global_desc": "Op alle pagina's tonen",
"radio.wizard.platform_azurecast": "AzureCast", "radio.wizard.widget_position_label": "Widget Positie",
"radio.wizard.platform_azurecast_desc": "AzureCast hosting. Volledige API integratie met now-playing, listeners en auto-configuratie.", "radio.wizard.position_bottom_right": "Rechtsonder",
"radio.wizard.platform_other": "Anders", "radio.wizard.position_bottom_left": "Linksonder",
"radio.wizard.platform_other_desc": "Een andere stream provider. Handmatige configuratie van stream URL en API endpoints.", "radio.wizard.position_top_right": "Rechtsboven",
"radio.wizard.shoutcast_info_title": "SHOUTcast", "radio.wizard.position_top_left": "Linksboven",
"radio.wizard.shoutcast_info_desc": "Voer je SHOUTcast stream URL in. De wizard probeert automatisch de stats endpoint te vinden.", "radio.wizard.section_gamification": "Gamification",
"radio.wizard.icecast_info_title": "Icecast", "radio.wizard.feature_points": "Punten Systeem",
"radio.wizard.icecast_info_desc": "Voer je Icecast stream URL in. De wizard gebruikt status-json.xsl voor automatische detectie.", "radio.wizard.feature_points_desc": "Verdien punten door te luisteren",
"radio.wizard.azurecast_info_title": "AzureCast", "radio.wizard.feature_contests": "Contesten",
"radio.wizard.azurecast_info_desc": "AzureCast stream URL + server configuratie. De wizard configureert alles via de AzureCast API.", "radio.wizard.feature_contests_desc": "Wedstrijden organiseren",
"radio.wizard.other_info_title": "Andere Stream", "radio.wizard.feature_giveaways": "Giveaways",
"radio.wizard.other_info_desc": "Voer je stream URL in. Je kunt later handmatig API endpoints configureren voor nu afspelen en luisteraars.", "radio.wizard.feature_giveaways_desc": "Cadeautjes weggeven",
"radio.wizard.stream_url_label": "Stream URL *", "radio.wizard.section_integrations": "Integraties",
"radio.wizard.stream_url_hint": "De directe URL naar je audiostreem (MP3, AAC, OGG, etc.)", "radio.wizard.feature_discord": "Discord Notificaties",
"radio.wizard.stream_name_label": "Stream Naam", "radio.wizard.feature_discord_desc": "Meldingen bij DJ live / nummer wijziging",
"radio.wizard.stream_name_placeholder": "Mijn Radio", "radio.wizard.discord_webhook_label": "Discord Webhook URL",
"radio.wizard.stream_name_hint": "Een naam voor je radiostream (optioneel)", "radio.wizard.discord_webhook_hint": "Maak een webhook aan in je Discord server kanaal.",
"radio.wizard.azurecast_section": "AzureCast Server Configuratie", "radio.wizard.test_title": "Verbinding Testen",
"radio.wizard.azurecast_base_url_label": "AzureCast Basis URL", "radio.wizard.test_desc": "Klik op Test Verbinding om te controleren of je stream en APIs bereikbaar zijn.",
"radio.wizard.azurecast_base_url_hint": "De basis URL van je AzureCast server. Wordt automatisch gedetecteerd als leeg gelaten.", "radio.wizard.test_loading": "Bezig met testen van de verbinding...",
"radio.wizard.azurecast_station_id_label": "Station ID", "radio.wizard.test_prompt": "Klik op de knop om de verbinding te testen.",
"radio.wizard.azurecast_station_id_hint": "Het station ID in AzureCast (standaard: 1)", "radio.wizard.test_button": "Test Verbinding",
"radio.wizard.enable_now_playing": "Nu Afspelen inschakelen", "radio.wizard.test_retry": "Opnieuw Testen",
"radio.wizard.now_playing_api_label": "Now Playing API URL", "radio.wizard.settings_overview": "Overzicht van Instellingen",
"radio.wizard.now_playing_api_hint": "API endpoint dat het huidige nummer teruggeeft. Meestal automatisch gedetecteerd.", "radio.wizard.settings_overview_desc": "Dit zijn de instellingen die worden opgeslagen:",
"radio.wizard.enable_listeners": "Luisteraars teller inschakelen", "radio.wizard.install_confirm": "Weet je zeker dat je de radio wilt installeren met deze instellingen?",
"radio.wizard.listeners_api_label": "Listeners API URL", "radio.wizard.install_button": "Radio Installeren",
"radio.wizard.listeners_api_hint": "API endpoint dat het aantal luisteraars teruggeeft.", "radio.wizard.test_result_stream": "Stream Verbinding",
"radio.wizard.enable_current_dj": "Huidige DJ tonen", "radio.wizard.test_result_now_playing": "Now Playing",
"radio.wizard.detected": "gedetecteerd!", "radio.wizard.test_result_listeners": "Luisteraars",
"radio.wizard.detected_desc": "API endpoints zijn automatisch gevonden en ingevuld.", "radio.wizard.status_success": "Succes",
"radio.wizard.not_detected": "Geen automatische detectie", "radio.wizard.status_warning": "Waarschuwing",
"radio.wizard.not_detected_desc": "Vul de API URLs handmatig in of sla deze stap over.", "radio.wizard.status_error": "Fout",
"radio.wizard.section_community": "Community Functies", "radio.wizard.status_skipped": "Overgeslagen",
"radio.wizard.feature_shouts": "Shouts", "radio.wizard.status_untested": "Niet getest",
"radio.wizard.feature_shouts_desc": "Berichten achterlaten", "radio.wizard.content_type": "Content-Type",
"radio.wizard.feature_applications": "DJ Aanmeldingen", "radio.wizard.http_status": "HTTP Status",
"radio.wizard.feature_applications_desc": "Solliciteren als DJ", "radio.wizard.song": "Nummer",
"radio.wizard.feature_requests": "Song Verzoeken", "radio.wizard.artist": "Artiest",
"radio.wizard.feature_requests_desc": "Nummers aanvragen", "radio.wizard.listeners": "Luisteraars",
"radio.wizard.section_display": "Weergave", "radio.wizard.api_url": "API URL",
"radio.wizard.feature_widget": "Radio Widget", "radio.wizard.test_stream_ok": "Stream is bereikbaar! Je kunt de radio installeren.",
"radio.wizard.feature_widget_desc": "Miniplayer op de site", "radio.wizard.test_stream_fail": "Stream is niet bereikbaar. Controleer de URL en probeer het opnieuw.",
"radio.wizard.feature_widget_global": "Widget Overal", "radio.wizard.test_not_run": "Nog niet getest.",
"radio.wizard.feature_widget_global_desc": "Op alle pagina's tonen", "radio.wizard.test_connection_fail": "Kon de test niet uitvoeren: ",
"radio.wizard.widget_position_label": "Widget Positie", "radio.wizard.error": "Fout",
"radio.wizard.position_bottom_right": "Rechtsonder", "radio.wizard.unknown_error": "Onbekende fout",
"radio.wizard.position_bottom_left": "Linksonder", "Homepage": "Homepage"
"radio.wizard.position_top_right": "Rechtsboven",
"radio.wizard.position_top_left": "Linksboven",
"radio.wizard.section_gamification": "Gamification",
"radio.wizard.feature_points": "Punten Systeem",
"radio.wizard.feature_points_desc": "Verdien punten door te luisteren",
"radio.wizard.feature_contests": "Contesten",
"radio.wizard.feature_contests_desc": "Wedstrijden organiseren",
"radio.wizard.feature_giveaways": "Giveaways",
"radio.wizard.feature_giveaways_desc": "Cadeautjes weggeven",
"radio.wizard.section_integrations": "Integraties",
"radio.wizard.feature_discord": "Discord Notificaties",
"radio.wizard.feature_discord_desc": "Meldingen bij DJ live / nummer wijziging",
"radio.wizard.discord_webhook_label": "Discord Webhook URL",
"radio.wizard.discord_webhook_hint": "Maak een webhook aan in je Discord server kanaal.",
"radio.wizard.test_title": "Verbinding Testen",
"radio.wizard.test_desc": "Klik op Test Verbinding om te controleren of je stream en APIs bereikbaar zijn.",
"radio.wizard.test_loading": "Bezig met testen van de verbinding...",
"radio.wizard.test_prompt": "Klik op de knop om de verbinding te testen.",
"radio.wizard.test_button": "Test Verbinding",
"radio.wizard.test_retry": "Opnieuw Testen",
"radio.wizard.settings_overview": "Overzicht van Instellingen",
"radio.wizard.settings_overview_desc": "Dit zijn de instellingen die worden opgeslagen:",
"radio.wizard.install_confirm": "Weet je zeker dat je de radio wilt installeren met deze instellingen?",
"radio.wizard.install_button": "Radio Installeren",
"radio.wizard.test_result_stream": "Stream Verbinding",
"radio.wizard.test_result_now_playing": "Now Playing",
"radio.wizard.test_result_listeners": "Luisteraars",
"radio.wizard.status_success": "Succes",
"radio.wizard.status_warning": "Waarschuwing",
"radio.wizard.status_error": "Fout",
"radio.wizard.status_skipped": "Overgeslagen",
"radio.wizard.status_untested": "Niet getest",
"radio.wizard.content_type": "Content-Type",
"radio.wizard.http_status": "HTTP Status",
"radio.wizard.song": "Nummer",
"radio.wizard.artist": "Artiest",
"radio.wizard.listeners": "Luisteraars",
"radio.wizard.api_url": "API URL",
"radio.wizard.test_stream_ok": "Stream is bereikbaar! Je kunt de radio installeren.",
"radio.wizard.test_stream_fail": "Stream is niet bereikbaar. Controleer de URL en probeer het opnieuw.",
"radio.wizard.test_not_run": "Nog niet getest.",
"radio.wizard.test_connection_fail": "Kon de test niet uitvoeren: ",
"radio.wizard.error": "Fout",
"radio.wizard.unknown_error": "Onbekende fout",
"Homepage": "Homepage"
} }
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1509 -1513
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1509 -1513
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1509 -1513
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1509 -1513
View File
File diff suppressed because it is too large Load Diff
Executable → Regular
+1503 -1507
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-8
View File
@@ -1,8 +0,0 @@
includes:
- phpstan-baseline.neon
parameters:
level: 5
paths:
- app
reportUnmatchedIgnoredErrors: false
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
:root{--swiper-navigation-size: 44px}.swiper-button-prev,.swiper-button-next{position:absolute;width:var(--swiper-navigation-size);height:var(--swiper-navigation-size);z-index:10;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--swiper-navigation-color, var(--swiper-theme-color));&.swiper-button-disabled{opacity:.35;cursor:auto;pointer-events:none}&.swiper-button-hidden{opacity:0;cursor:auto;pointer-events:none}.swiper-navigation-disabled &{display:none!important}::slotted(svg),svg{width:100%;height:100%;-o-object-fit:contain;object-fit:contain;transform-origin:center;fill:currentColor;pointer-events:none}}.swiper-button-lock{display:none}.swiper-button-prev,.swiper-button-next{top:var(--swiper-navigation-top-offset, 50%);margin-top:calc(0px - (var(--swiper-navigation-size) / 2))}.swiper-button-prev{left:var(--swiper-navigation-sides-offset, 4px);right:auto;::slotted(.swiper-navigation-icon),.swiper-navigation-icon{transform:rotate(180deg)}}.swiper-button-next{right:var(--swiper-navigation-sides-offset, 4px);left:auto}.swiper-horizontal{.swiper-button-prev,.swiper-button-next,~.swiper-button-prev,~.swiper-button-next{top:var(--swiper-navigation-top-offset, 50%);margin-top:calc(0px - (var(--swiper-navigation-size) / 2));margin-left:0}.swiper-button-prev,~.swiper-button-prev,&.swiper-rtl .swiper-button-next,&.swiper-rtl~.swiper-button-next{left:var(--swiper-navigation-sides-offset, 4px);right:auto}.swiper-button-next,~.swiper-button-next,&.swiper-rtl .swiper-button-prev,&.swiper-rtl~.swiper-button-prev{right:var(--swiper-navigation-sides-offset, 4px);left:auto}.swiper-button-prev,~.swiper-button-prev,&.swiper-rtl .swiper-button-next,&.swiper-rtl~.swiper-button-next{::slotted(.swiper-navigation-icon),.swiper-navigation-icon{transform:rotate(180deg)}}&.swiper-rtl .swiper-button-prev,&.swiper-rtl~.swiper-button-prev{::slotted(.swiper-navigation-icon),.swiper-navigation-icon{transform:rotate(0)}}}.swiper-vertical{.swiper-button-prev,.swiper-button-next,~.swiper-button-prev,~.swiper-button-next{left:var(--swiper-navigation-top-offset, 50%);right:auto;margin-left:calc(0px - (var(--swiper-navigation-size) / 2));margin-top:0}.swiper-button-prev,~.swiper-button-prev{top:var(--swiper-navigation-sides-offset, 4px);bottom:auto;::slotted(.swiper-navigation-icon),.swiper-navigation-icon{transform:rotate(-90deg)}}.swiper-button-next,~.swiper-button-next{bottom:var(--swiper-navigation-sides-offset, 4px);top:auto;::slotted(.swiper-navigation-icon),.swiper-navigation-icon{transform:rotate(90deg)}}}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
View File

Before

Width:  |  Height:  |  Size: 260 B

After

Width:  |  Height:  |  Size: 260 B

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

View File

Before

Width:  |  Height:  |  Size: 678 B

After

Width:  |  Height:  |  Size: 678 B

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 381 B

After

Width:  |  Height:  |  Size: 381 B

-1
View File
@@ -1 +0,0 @@
var c=Object.create,_=Object.defineProperty,v=Object.getOwnPropertyDescriptor,O=Object.getOwnPropertyNames,b=Object.getPrototypeOf,s=Object.prototype.hasOwnProperty,g=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports),f=(e,r)=>{let t={};for(var n in e)_(t,n,{get:e[n],enumerable:!0});return r||_(t,Symbol.toStringTag,{value:"Module"}),t},P=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(var o=O(r),p=0,l=o.length,a;p<l;p++)a=o[p],!s.call(e,a)&&a!==t&&_(e,a,{get:(u=>r[u]).bind(null,a),enumerable:!(n=v(r,a))||n.enumerable});return e},i=(e,r,t)=>(t=e!=null?c(b(e)):{},P(r||!e||!e.__esModule?_(t,"default",{value:e,enumerable:!0}):t,e));export{f as n,i as r,g as t};
View File

Before

Width:  |  Height:  |  Size: 279 B

After

Width:  |  Height:  |  Size: 279 B

View File

Before

Width:  |  Height:  |  Size: 473 B

After

Width:  |  Height:  |  Size: 473 B

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 471 B

View File

Before

Width:  |  Height:  |  Size: 690 B

After

Width:  |  Height:  |  Size: 690 B

View File

Before

Width:  |  Height:  |  Size: 361 B

After

Width:  |  Height:  |  Size: 361 B

View File

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 472 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 626 B

After

Width:  |  Height:  |  Size: 626 B

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 491 B

View File

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 477 B

View File

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 258 KiB

View File

Before

Width:  |  Height:  |  Size: 166 B

After

Width:  |  Height:  |  Size: 166 B

View File

Before

Width:  |  Height:  |  Size: 485 B

After

Width:  |  Height:  |  Size: 485 B

File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
View File

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 364 B

View File

Before

Width:  |  Height:  |  Size: 630 B

After

Width:  |  Height:  |  Size: 630 B

View File

Before

Width:  |  Height:  |  Size: 350 B

After

Width:  |  Height:  |  Size: 350 B

Regular → Executable
+24 -75
View File
@@ -1,25 +1,7 @@
{ {
"_axios-BgauIbGG.js": { "_axios-B2OU-7LW.js": {
"file": "assets/axios-BgauIbGG.js", "file": "assets/axios-B2OU-7LW.js",
"name": "axios", "name": "axios"
"imports": [
"_chunk-BqhQeaEc.js"
]
},
"_chunk-BqhQeaEc.js": {
"file": "assets/chunk-BqhQeaEc.js",
"name": "chunk"
},
"_swiper-B7Yxk788.js": {
"file": "assets/swiper-B7Yxk788.js",
"name": "swiper",
"css": [
"assets/swiper-CrMA9oas.css"
]
},
"_swiper-CrMA9oas.css": {
"file": "assets/swiper-CrMA9oas.css",
"src": "_swiper-CrMA9oas.css"
}, },
"public/assets/images/background-dark.jpg": { "public/assets/images/background-dark.jpg": {
"file": "assets/background-dark-BfkMu3-0.jpg", "file": "assets/background-dark-BfkMu3-0.jpg",
@@ -29,18 +11,6 @@
"file": "assets/background-light-CP7oKwVT.jpg", "file": "assets/background-light-CP7oKwVT.jpg",
"src": "public/assets/images/background-light.jpg" "src": "public/assets/images/background-light.jpg"
}, },
"public/assets/images/dusk/background_image.png": {
"file": "assets/background_image-BH7pVpv1.png",
"src": "public/assets/images/dusk/background_image.png"
},
"public/assets/images/dusk/leaderboard_circle_image.png": {
"file": "assets/leaderboard_circle_image-BYkDVX69.png",
"src": "public/assets/images/dusk/leaderboard_circle_image.png"
},
"public/assets/images/dusk/store_icon.png": {
"file": "assets/store_icon-B52tsSKO.png",
"src": "public/assets/images/dusk/store_icon.png"
},
"public/assets/images/icons/article.gif": { "public/assets/images/icons/article.gif": {
"file": "assets/article-CYhGsSKA.gif", "file": "assets/article-CYhGsSKA.gif",
"src": "public/assets/images/icons/article.gif" "src": "public/assets/images/icons/article.gif"
@@ -134,7 +104,7 @@
"src": "public/assets/images/profile/profile-bg.png" "src": "public/assets/images/profile/profile-bg.png"
}, },
"resources/css/global.css": { "resources/css/global.css": {
"file": "assets/global-DVSCrBhf.css", "file": "assets/global-owlIrRiH.css",
"name": "global", "name": "global",
"names": [ "names": [
"global.css" "global.css"
@@ -170,57 +140,30 @@
] ]
}, },
"resources/js/global.js": { "resources/js/global.js": {
"file": "assets/global-Bkbv5Qui.js", "file": "assets/global-TU7mFC54.js",
"name": "global", "name": "global",
"src": "resources/js/global.js", "src": "resources/js/global.js",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_chunk-BqhQeaEc.js", "_axios-B2OU-7LW.js"
"_axios-BgauIbGG.js"
]
},
"resources/js/ssr.jsx": {
"file": "assets/ssr-dFzH1dUH.js",
"name": "ssr",
"src": "resources/js/ssr.jsx",
"isEntry": true,
"imports": [
"_chunk-BqhQeaEc.js"
] ]
}, },
"resources/themes/atom/css/app.css": { "resources/themes/atom/css/app.css": {
"file": "assets/app-DrKvMbgn.css", "file": "assets/app-9mCg36im.css",
"src": "resources/themes/atom/css/app.css"
},
"resources/themes/atom/js/app.js": {
"file": "assets/app-BBe1NbvP.js",
"name": "app",
"src": "resources/themes/atom/js/app.js",
"isEntry": true,
"imports": [
"_chunk-BqhQeaEc.js",
"_swiper-B7Yxk788.js",
"_axios-BgauIbGG.js"
]
},
"resources/themes/dusk/css/app.css": {
"file": "assets/app-tY2AX6sE.css",
"name": "app", "name": "app",
"names": [ "names": [
"app.css" "app.css"
], ],
"src": "resources/themes/dusk/css/app.css", "src": "resources/themes/atom/css/app.css",
"isEntry": true, "isEntry": true,
"css": [
"assets/app-DrKvMbgn.css"
],
"assets": [ "assets": [
"assets/background_image-BH7pVpv1.png", "assets/camera-fu-bmGhB.png",
"assets/discord-rbcnEh-j.png",
"assets/feeds-BtHcJdHX.png", "assets/feeds-BtHcJdHX.png",
"assets/chat-r5H1PnTg.png", "assets/chat-r5H1PnTg.png",
"assets/article-CYhGsSKA.gif", "assets/article-CYhGsSKA.gif",
"assets/lighthouse-BON6qnQ0.png", "assets/lighthouse-BON6qnQ0.png",
"assets/store_icon-B52tsSKO.png", "assets/currency-LKXzczCA.png",
"assets/catalog-D-956oDx.png", "assets/catalog-D-956oDx.png",
"assets/inventory-BlHYLNGT.png", "assets/inventory-BlHYLNGT.png",
"assets/due-chat-CeO4yxLu.png", "assets/due-chat-CeO4yxLu.png",
@@ -228,23 +171,29 @@
"assets/credits-Dpg5Nmby.png", "assets/credits-Dpg5Nmby.png",
"assets/duckets-CaGJI1Oy.png", "assets/duckets-CaGJI1Oy.png",
"assets/diamonds-BtfqKoQu.png", "assets/diamonds-BtfqKoQu.png",
"assets/profile-bg-BWx4iuHa.png",
"assets/trophy-gold-bbKmpkii.png", "assets/trophy-gold-bbKmpkii.png",
"assets/trophy-silver-bGfHJkQ_.png", "assets/trophy-silver-bGfHJkQ_.png",
"assets/trophy-bronze-CgV5j1MU.png", "assets/trophy-bronze-CgV5j1MU.png",
"assets/leaderboard_circle_image-BYkDVX69.png" "assets/background-light-CP7oKwVT.jpg",
"assets/background-dark-BfkMu3-0.jpg",
"assets/shop-D3NfN6cF.png",
"assets/leaderboards-CGasq3cL.png",
"assets/rules--xzBmecz.gif",
"assets/home-DIMFC97Y.png",
"assets/community-Do_t1zw9.png"
] ]
}, },
"resources/themes/dusk/js/app.js": { "resources/themes/atom/js/app.js": {
"file": "assets/app-Dnwim5HE.js", "file": "assets/app-wUWplMFd.js",
"name": "app", "name": "app",
"src": "resources/themes/dusk/js/app.js", "src": "resources/themes/atom/js/app.js",
"isEntry": true, "isEntry": true,
"imports": [ "imports": [
"_swiper-B7Yxk788.js", "_axios-B2OU-7LW.js"
"_axios-BgauIbGG.js"
], ],
"css": [ "css": [
"assets/app-DU8Y3NnC.css" "assets/app-CeYfhhVD.css"
] ]
} }
} }
@@ -55,7 +55,7 @@
<div class="w-22 h-22 rounded-full overflow-hidden border-4 transition-all duration-300 group-hover:scale-110 group-hover:rotate-3" <div class="w-22 h-22 rounded-full overflow-hidden border-4 transition-all duration-300 group-hover:scale-110 group-hover:rotate-3"
style="border-color: {{ $staffColor }}; box-shadow: 0 0 15px {{ $staffColor }}50, inset 0 0 15px {{ $staffColor }}20;"> style="border-color: {{ $staffColor }}; box-shadow: 0 0 15px {{ $staffColor }}50, inset 0 0 15px {{ $staffColor }}20;">
<img style="image-rendering: pixelated;" <img style="image-rendering: pixelated;"
src="{{ setting('avatar_imager') }}{{ $user->look }}&direction=2&head_direction=3&gesture=sml&action=wav&size=n" src="{{ setting('avatar_imager') }}{{ $user->look }}&direction=3&head_direction=3&gesture=sml&action=wlk%2Cwav&size=l&img_format=gif&size=n"
alt="{{ $user->username }}" alt="{{ $user->username }}"
class="w-full h-full object-cover"> class="w-full h-full object-cover">
</div> </div>
+122 -123
View File
@@ -1,140 +1,139 @@
<x-app-layout> <x-app-layout>
@push('title', auth()->user()->username) @push('title', auth()->user()->username)
<div class="col-span-12 space-y-4 lg:col-span-9"> <div class="col-span-12 space-y-4 lg:col-span-9">
<x-user.me-backdrop :user="$user" /> <x-user.me-backdrop :user="$user" />
<div class="rounded-xl border-2 shadow-sm lg:flex lg:items-center lg:justify-between overflow-hidden" <div class="rounded-xl border-2 shadow-sm lg:flex lg:items-center lg:justify-between overflow-hidden"
style="background-color: var(--color-surface); border-color: var(--border-color);"> style="background-color: var(--color-surface); border-color: var(--border-color);">
<div class="flex items-center gap-3 p-3" style="background-color: var(--color-primary);"> <div class="flex items-center gap-3 p-3" style="background-color: var(--color-primary);">
<img src="{{ asset('/assets/images/icons/online-friends.png') }}" alt="{{ __('Online Friends') }}" <img src="{{ asset('/assets/images/icons/online-friends.png') }}" alt="{{ __('Online Friends') }}"
class="w-6 h-6"> class="w-6 h-6">
<span class="text-sm font-semibold" style="color: var(--button-text-color)">{{ __('Online Friends') }}</span> <span class="text-sm font-semibold" style="color: var(--button-text-color)">{{ __('Online Friends') }}</span>
</div> </div>
<div class="relative flex flex-wrap items-center justify-center gap-3 p-3"> <div class="relative flex flex-wrap items-center justify-center gap-3 p-3">
@foreach ($onlineFriends as $friend) @foreach ($onlineFriends as $friend)
<div data-popover-target="friend-{{ $friend->username }}" <div data-popover-target="friend-{{ $friend->username }}"
style="image-rendering: pixelated; background-image: url({{ setting('avatar_imager') }}{{ $friend->look }}&direction=2&head_direction=3&gesture=sml&action=wav&headonly=1&size=s)" class="inline-block h-12 w-12 rounded-full border-2 bg-center bg-no-repeat hover:border-[var(--border-color)] hover:scale-110 transition-all duration-300 cursor-pointer"
class="inline-block h-12 w-12 rounded-full border-2 bg-center bg-no-repeat hover:border-[var(--border-color)] hover:scale-110 transition-all duration-300 cursor-pointer" style="image-rendering: pixelated; border-color: var(--color-text-muted); background-image: url({{ setting('avatar_imager') }}{{ $friend->look }}&direction=3&head_direction=3&gesture=sml&action=wlk,wav&size=l&img_format=gif);">
style="border-color: var(--color-text-muted)"> </div>
</div>
<div data-popover id="friend-{{ $friend->username }}" role="tooltip" <div data-popover id="friend-{{ $friend->username }}" role="tooltip"
class="invisible absolute z-10 inline-block w-64 rounded-lg border shadow-xl opacity-0 transition-opacity duration-300" class="invisible absolute z-10 inline-block w-64 rounded-lg border shadow-xl opacity-0 transition-opacity duration-300"
style="background-color: var(--color-surface); border-color: var(--color-text-muted); color: var(--color-text);"> style="background-color: var(--color-surface); border-color: var(--color-text-muted); color: var(--color-text);">
<div class="rounded-t-lg border-b px-4 py-3" style="border-color: var(--color-text-muted); background-color: var(--color-surface);"> <div class="rounded-t-lg border-b px-4 py-3" style="border-color: var(--color-text-muted); background-color: var(--color-surface);">
<div class="flex items-center justify-between font-semibold" style="color: var(--color-text)"> <div class="flex items-center justify-between font-semibold" style="color: var(--color-text)">
{{ $friend->username }} {{ $friend->username }}
<span class="w-2 h-2 rounded-full bg-green-500"></span> <span class="w-2 h-2 rounded-full bg-green-500"></span>
</div> </div>
</div> </div>
<div class="px-4 py-3 space-y-1"> <div class="px-4 py-3 space-y-1">
<div class="text-sm"> <div class="text-sm">
<span class="font-medium" style="color: var(--color-text-muted)">{{ __('Motto') }}:</span> <span class="font-medium" style="color: var(--color-text-muted)">{{ __('Motto') }}:</span>
<span class="ml-1" style="color: var(--color-text)">{{ $friend->motto }}</span> <span class="ml-1" style="color: var(--color-text)">{{ $friend->motto }}</span>
</div> </div>
<div class="text-sm"> <div class="text-sm">
<span class="font-medium" style="color: var(--color-text-muted)">{{ __('Online Since') }}:</span> <span class="font-medium" style="color: var(--color-text-muted)">{{ __('Online Since') }}:</span>
<span class="ml-1" style="color: var(--color-text)">{{ date(config('habbo.site.date_format'), $friend->last_online) }}</span> <span class="ml-1" style="color: var(--color-text)">{{ date(config('habbo.site.date_format'), $friend->last_online) }}</span>
</div> </div>
</div> </div>
<div data-popper-arrow class="absolute h-2 w-2 rotate-45 border-l border-b" style="border-color: var(--color-text-muted); background-color: var(--color-surface);"></div> <div data-popper-arrow class="absolute h-2 w-2 rotate-45 border-l border-b" style="border-color: var(--color-text-muted); background-color: var(--color-surface);"></div>
</div> </div>
@endforeach @endforeach
</div> </div>
</div> </div>
<x-content.content-card icon="friends-icon" classes="border-2 dark:border-gray-900" style="border-color: var(--border-color);"> <x-content.content-card icon="friends-icon" classes="border-2 dark:border-gray-900" style="border-color: var(--border-color);">
<x-slot:title> <x-slot:title>
{{ sprintf(__('User Referrals (%s/%s)'), auth()->user()->referrals->referrals_total ?? 0, setting('referrals_needed')) }} {{ sprintf(__('User Referrals (%s/%s)'), auth()->user()->referrals->referrals_total ?? 0, setting('referrals_needed')) }}
</x-slot:title> </x-slot:title>
<x-slot:under-title> <x-slot:under-title>
{{ __('Referral new users and be rewarded by in-game goods') }} {{ __('Referral new users and be rewarded by in-game goods') }}
</x-slot:under-title> </x-slot:under-title>
<div class="px-2 text-sm text-[var(--color-text)] space-y-4"> <div class="px-2 text-sm text-[var(--color-text)] space-y-4">
<p> <p>
{{ __('Here at :hotel we have added a referral system, allowing you to obtain a bonus for every :needed users that registers through your referral link will allow you to claim a reward of :amount diamonds!', ['hotel' => setting('hotel_name'), 'needed' => setting('referrals_needed'), 'amount' => setting('referral_reward_amount')]) }} {{ __('Here at :hotel we have added a referral system, allowing you to obtain a bonus for every :needed users that registers through your referral link will allow you to claim a reward of :amount diamonds!', ['hotel' => setting('hotel_name'), 'needed' => setting('referrals_needed'), 'amount' => setting('referral_reward_amount')]) }}
</p> </p>
<div class="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-3"> <div class="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-3">
<p class="text-yellow-400/80 text-xs"> <p class="text-yellow-400/80 text-xs">
⚠️ {{ __('Boosting referrals by making own accounts will lead to removal of all progress, currency, inventory and a potential ban') }} ⚠️ {{ __('Boosting referrals by making own accounts will lead to removal of all progress, currency, inventory and a potential ban') }}
</p> </p>
</div> </div>
<div class="grid grid-cols-12 gap-2"> <div class="grid grid-cols-12 gap-2">
<x-form.input classes="col-span-12 md:col-span-10 text-sm" name="referral" <x-form.input classes="col-span-12 md:col-span-10 text-sm" name="referral"
value="{{ sprintf('%s/register/%s', config('habbo.site.site_url'), auth()->user()->referral_code) }}" value="{{ sprintf('%s/register/%s', config('habbo.site.site_url'), auth()->user()->referral_code) }}"
:autofocus="false" :readonly="true" /> :autofocus="false" :readonly="true" />
<div class="col-span-12 flex md:col-span-2" onclick="copyCode()"> <div class="col-span-12 flex md:col-span-2" onclick="copyCode()">
<x-form.secondary-button> <x-form.secondary-button>
{{ __('Copy code') }} {{ __('Copy code') }}
</x-form.secondary-button> </x-form.secondary-button>
</div> </div>
</div> </div>
@if (auth()->user()->referrals?->referrals_total >= (int) setting('referrals_needed')) @if ((auth()->user()->referrals->referrals_total ?? 0) >= (int) setting('referrals_needed'))
<a href="{{ route('claim.referral-reward') }}" class="text-decoration-none"> <a href="{{ route('claim.referral-reward') }}" class="text-decoration-none">
<x-form.secondary-button classes="mt-2"> <x-form.secondary-button classes="mt-2">
{{ __('Claim your referrals reward!') }} {{ __('Claim your referrals reward!') }}
</x-form.secondary-button> </x-form.secondary-button>
</a> </a>
@else @else
<button disabled class="mt-2 w-full rounded bg-[var(--color-text-muted)] p-3 text-[var(--color-text)] cursor-not-allowed opacity-60"> <button disabled class="mt-2 w-full rounded bg-[var(--color-text-muted)] p-3 text-[var(--color-text)] cursor-not-allowed opacity-60">
{{ sprintf(__('You need to refer :needed more users, before being able to claim your reward', ['needed' =>auth()->user()->referralsNeeded() ?? 0]),auth()->user()->referrals->referrals_total ?? 0) }} {{ sprintf(__('You need to refer :needed more users, before being able to claim your reward', ['needed' => max(0, (int) setting('referrals_needed') - (auth()->user()->referrals->referrals_total ?? 0))]), auth()->user()->referrals->referrals_total ?? 0) }}
</button> </button>
@endif @endif
</div> </div>
</x-content.content-card> </x-content.content-card>
</div> </div>
<div class="col-span-12 space-y-4 lg:col-span-3"> <div class="col-span-12 space-y-4 lg:col-span-3">
<div class="relative w-full" style="height: 213px"> <div class="relative w-full" style="height: 213px">
<div class="relative swiper articles-slider"> <div class="relative swiper articles-slider">
<div class="swiper-wrapper"> <div class="swiper-wrapper">
@forelse ($articles as $article) @forelse ($articles as $article)
<x-article-card :for-slider="true" :article="$article" /> <x-article-card :for-slider="true" :article="$article" />
@empty @empty
<x-filler-article-card /> <x-filler-article-card />
@endforelse @endforelse
</div> </div>
</div> </div>
<div class="swiper-pagination" style="bottom: 0px !important; z-index: 0;"></div> <div class="swiper-pagination" style="bottom: 0px !important; z-index: 0;"></div>
</div> </div>
<div class="!mt-3"> <div class="!mt-3">
<x-user.discord-widget /> <x-user.discord-widget />
</div> </div>
</div> </div>
@push('javascript') @push('javascript')
<script> <script>
var Toast = Swal.mixin({ var Toast = Swal.mixin({
toast: true, toast: true,
position: "top-end", position: "top-end",
showConfirmButton: false, showConfirmButton: false,
timer: 4000, timer: 4000,
timerProgressBar: true, timerProgressBar: true,
didOpen: (toast) => { didOpen: (toast) => {
toast.addEventListener("mouseenter", Swal.stopTimer); toast.addEventListener("mouseenter", Swal.stopTimer);
toast.addEventListener("mouseleave", Swal.resumeTimer); toast.addEventListener("mouseleave", Swal.resumeTimer);
} }
}); });
function copyCode() { function copyCode() {
let copyText = document.querySelector("#referral"); let copyText = document.querySelector("#referral");
copyText.select(); copyText.select();
document.execCommand("copy"); document.execCommand("copy");
Toast.fire({ Toast.fire({
icon: "success", icon: "success",
title: '{{ __('Your referral code has been copied to your clipbord!') }}' title: '{{ __('Your referral code has been copied to your clipboard!') }}'
}); });
} }
</script> </script>
@endpush @endpush
</x-app-layout> </x-app-layout>
@@ -1,38 +0,0 @@
@props(['backups'])
@if (empty($backups))
<div style="text-align:center;padding:32px;color:#64748b;background:#f8fafc;border-radius:12px;border:1px solid #e2e8f0;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="2" style="margin:0 auto 16px;display:block;">
<path d="M21 8v13H3V8M1 3h22v5H1zM10 12h4"/>
</svg>
<div style="font-size:14px;">{{ __('commandocentrum.no_backups') }}</div>
<div style="font-size:12px;margin-top:8px;">{{ __('commandocentrum.backups_auto') }}</div>
</div>
@else
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;">
@foreach ($backups as $backup)
@php
$dateFormatted = str_replace('_', ' ', $backup['date']);
@endphp
<div style="background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:16px;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:8px;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2">
<path d="M21 8v13H3V8M1 3h22v5H1zM10 12h4"/>
</svg>
<span style="font-weight:600;color:#1e293b;font-size:14px;">{{ $backup['jar'] }}</span>
</div>
</div>
<div style="font-size:12px;color:#64748b;margin-bottom:12px;">{{ $dateFormatted }}</div>
<button
wire:click="restoreBackup('{{ $backup['name'] }}')"
style="width:100%;background:linear-gradient(135deg,#3b82f6,#2563eb);padding:10px;border-radius:8px;color:white;border:none;cursor:pointer;font-weight:600;font-size:13px;box-shadow:0 2px 8px rgba(37,99,235,0.3);transition:transform 0.2s;"
onmouseover="this.style.transform='translateY(-1px)'"
onmouseout="this.style.transform='translateY(0)'"
>
🔄 {{ __('commandocentrum.restore') }}
</button>
</div>
@endforeach
</div>
@endif
@@ -1,49 +0,0 @@
@props([
'emulatorBranchesHtml',
'emulatorStatusHtml',
])
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">{{ __('commandocentrum.github_url') }}</label>
<input type="text" wire:model="data.emulator_github_url" placeholder="https://github.com/..." style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">{{ __('commandocentrum.jar_direct_url') }}</label>
<input type="text" wire:model="data.emulator_jar_direct_url" placeholder="https://..." style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">{{ __('commandocentrum.jar_path') }}</label>
<input type="text" wire:model="data.emulator_jar_path" placeholder="/root/emulator" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">{{ __('commandocentrum.source_repo') }}</label>
<input type="text" wire:model="data.emulator_source_repo" placeholder="user/repo" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">{{ __('commandocentrum.source_path') }}</label>
<input type="text" wire:model="data.emulator_source_path" placeholder="/var/www/emulator" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">{{ __('commandocentrum.branch') }}</label>
<select wire:model="data.emulator_github_branch" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;background:#fff;">
{!! $emulatorBranchesHtml !!}
</select>
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">{{ __('commandocentrum.db_host') }}</label>
<input type="text" wire:model="data.emulator_database_host" placeholder="127.0.0.1" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">{{ __('commandocentrum.db_name') }}</label>
<input type="text" wire:model="data.emulator_database_name" placeholder="habbo" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div>
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">{{ __('commandocentrum.service_name') }}</label>
<input type="text" wire:model="data.emulator_service_name" placeholder="arcturus" style="width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;" />
</div>
<div style="grid-column:span 3;">
<label style="display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:6px;">{{ __('commandocentrum.status') }}</label>
{!! $emulatorStatusHtml !!}
</div>
</div>
@@ -1,85 +0,0 @@
@props([
'emulatorOnline',
'jarExists',
'serviceName',
'sourceCommit',
'remoteVersion',
'canBuild',
'jarPath',
'sourcePath',
])
@php
$sourceCommitShort = substr($sourceCommit, 0, 7);
$hasUpdate = $sourceCommit !== 'N/A' && $remoteVersion !== 'N/A' && $sourceCommitShort !== $remoteVersion;
$updateColor = $hasUpdate ? '#f59e0b' : '#22c55e';
$updateText = $hasUpdate ? '🔄 ' . __('commandocentrum.update_available') : '✓ ' . __('commandocentrum.up_to_date');
$btnColor = $hasUpdate ? '#f59e0b' : '#3b82f6';
$btnGradient = $hasUpdate ? 'linear-gradient(135deg,#f59e0b,#d97706)' : 'linear-gradient(135deg,#3b82f6,#2563eb)';
$btnText = $hasUpdate ? '⚡ ' . __('commandocentrum.update') : '🔄 ' . __('commandocentrum.rebuild');
$jarFileName = '';
if ($jarExists) {
$jarSize = shell_exec('ls -lh ' . escapeshellarg($jarPath) . '/*.jar 2>/dev/null | head -1');
if ($jarSize) {
preg_match('/(\S+\.jar)/', $jarSize, $matches);
if (isset($matches[1])) {
$jarFileName = basename($matches[1]);
}
}
}
@endphp
<div style="display:flex;flex-direction:column;gap:8px;font-size:13px;">
<div style="display:flex;gap:16px;">
@if ($emulatorOnline)
<span style="color:#22c55e;"> {{ __('commandocentrum.online') }}</span>
@else
<span style="color:#ef4444;"> {{ __('commandocentrum.offline') }}</span>
@endif
@if ($jarExists)
<span style="color:#22c55e;"> JAR {{ __('commandocentrum.ok') }}</span>
@else
<span style="color:#ef4444;"> JAR {{ __('commandocentrum.missing') }}</span>
@endif
<span style="color:#3b82f6;">{{ __('commandocentrum.service') }}: {{ e($serviceName) }}</span>
</div>
<div style="padding-top:8px;border-top:1px solid #e2e8f0;">
<div style="font-weight:600;color:#475569;margin-bottom:8px;">GitHub {{ __('commandocentrum.status') }}:</div>
<div style="background:{{ $hasUpdate ? '#fef3c7' : '#dcfce7' }};padding:12px;border-radius:8px;margin-bottom:12px;">
<div style="display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;font-weight:600;color:{{ $updateColor }};">
{{ $updateText }}
</div>
<button
wire:click="checkEmulatorUpdates"
style="background:{{ $btnGradient }};padding:8px 16px;border-radius:6px;color:white;border:none;cursor:pointer;font-weight:600;font-size:12px;box-shadow:0 2px 8px rgba(0,0,0,0.2);transition:transform 0.2s,box-shadow 0.2s;"
onmouseover="this.style.transform='translateY(-1px)';this.style.boxShadow='0 4px 12px rgba(0,0,0,0.3)'"
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='0 2px 8px rgba(0,0,0,0.2)'"
>
{{ $btnText }}
</button>
</div>
</div>
<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f1f5f9;"><span>{{ __('commandocentrum.latest') }}:</span><span style="color:#22c55e;font-weight:600;"> {{ e($remoteVersion) }}</span></div>
<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f1f5f9;"><span>{{ __('commandocentrum.source') }}:</span><span style="color:#22c55e;font-weight:600;"> {{ $sourceCommitShort }}</span></div>
@if ($jarFileName !== '')
<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f1f5f9;"><span>{{ __('commandocentrum.jars') }}:</span><span style="color:#22c55e;font-weight:600;"> {{ e($jarFileName) }}</span></div>
@endif
@if ($canBuild)
<div style="display:flex;justify-content:space-between;padding:6px 0;"><span>{{ __('commandocentrum.method') }}:</span><span style="color:#22c55e;font-weight:600;"> {{ __('commandocentrum.maven_pom') }}</span></div>
@else
<div style="display:flex;justify-content:space-between;padding:6px 0;"><span>{{ __('commandocentrum.method') }}:</span><span style="color:#f59e0b;font-weight:600;">⚠️ {{ __('commandocentrum.no_pom') }}</span></div>
@endif
<div style="margin-top:12px;padding:12px;background:#f8fafc;border-radius:8px;">
<div style="font-size:11px;color:#64748b;font-weight:600;margin-bottom:8px;">{{ __('commandocentrum.method') }}:</div>
@if ($jarExists)
<div style="color:#3b82f6;font-weight:600;">📦 {{ __('commandocentrum.jar_download_restart') }}</div>
@elseif ($canBuild)
<div style="color:#3b82f6;font-weight:600;">🔨 {{ __('commandocentrum.maven_build_restart') }}</div>
@else
<div style="color:#64748b;">{{ __('commandocentrum.manual_download') }}</div>
@endif
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More