diff --git a/docs/code-bridge/archive/MSG-2026-04-12-auto-detection-missing.md b/docs/code-bridge/archive/MSG-2026-04-12-auto-detection-missing.md new file mode 100644 index 0000000..7ef3fca --- /dev/null +++ b/docs/code-bridge/archive/MSG-2026-04-12-auto-detection-missing.md @@ -0,0 +1,46 @@ +# Chronicler Dispatch — Auto-Detection Not Implemented + +**Date:** 2026-04-12 +**From:** Chronicler #84 — The Meridian +**To:** Code + +--- + +## Production Issue — Found 0 servers + +`php artisan modpackchecker:check` returns "Found 0 servers with modpack configuration" on the live panel. Michael's servers use CurseForge and FTB packs but don't have `MODPACK_PLATFORM` egg variables set. + +## Root Cause + +`CheckModpackUpdates.php` only queries servers with `MODPACK_PLATFORM` egg variable. The `DaemonFileRepository` auto-detection from the Gemini hybrid detection consultation (April 6) was **never implemented**. + +## What Was Agreed With Gemini + +File: `docs/consultations/gemini-hybrid-detection-2026-04-06.md` + +The agreed "Magic & Manual" hybrid approach: +1. Check egg variables first (fastest) +2. If missing → use `DaemonFileRepository` to read `manifest.json` (CurseForge detection via `projectID`) +3. If missing → read `modrinth.index.json` (Modrinth) +4. If found → save to DB with `detection_method = 'file'` +5. If nothing found → mark `status = 'unconfigured'` + +**Key constraint from Gemini:** Never do this on page load. Background cron only. The `DaemonFileRepository` call is a network request to Wings. + +## What Needs to Be Built + +Update `CheckModpackUpdates.php` to: +1. Get ALL servers (not just ones with egg variables) +2. For servers without egg variables, attempt file-based detection via `DaemonFileRepository` +3. Read `manifest.json` → extract `projectID` for CurseForge +4. Read `modrinth.index.json` → extract for Modrinth +5. FTB: check for `version.json` or similar FTB manifest file +6. Save with `detection_method = 'file'` +7. Respect `is_user_overridden` flag — never overwrite manual configs + +Gemini's exact implementation code is in the consultation doc linked above. + +This is the core feature that makes ModpackChecker "plug and play" — without it, customers need to modify their eggs, which is a non-starter for a BuiltByBit product. + +*— Chronicler #84, The Meridian* +**Fire + Frost + Foundation** 💙🔥❄️ diff --git a/services/modpack-version-checker/blueprint-extension/app/Console/Commands/CheckModpackUpdates.php b/services/modpack-version-checker/blueprint-extension/app/Console/Commands/CheckModpackUpdates.php index 6ba18b8..a0b1220 100644 --- a/services/modpack-version-checker/blueprint-extension/app/Console/Commands/CheckModpackUpdates.php +++ b/services/modpack-version-checker/blueprint-extension/app/Console/Commands/CheckModpackUpdates.php @@ -1,36 +1,14 @@ > /dev/null 2>&1 - * - * HOW IT WORKS: - * 1. Finds all servers with MODPACK_PLATFORM egg variable set - * 2. Loops through each server, checking via ModpackApiService - * 3. Stores results in modpackchecker_servers database table - * 4. Dashboard badges read from this table (never calling APIs directly) - * - * RATE LIMITING: - * Each API call is followed by a 2-second sleep to avoid rate limits. - * For 50 servers, a full check takes ~2 minutes. - * - * @package Pterodactyl\Console\Commands - * @author Firefrost Gaming / Frostystyle - * @version 1.0.0 - * @see ModpackApiService.php (centralized API logic) - * @see ModpackAPIController.php (provides getStatus endpoint for badges) - * ============================================================================= + * ModpackChecker Cron Command — hybrid "Magic & Manual" detection. + * + * 1. Egg variables (fastest) + * 2. File detection via DaemonFileRepository (manifest.json, modrinth.index.json) + * 3. API version check for detected packs + * + * USAGE: php artisan modpackchecker:check + * CRON: 0 0,6,12,18 * * * cd /var/www/pterodactyl && php artisan modpackchecker:check */ namespace Pterodactyl\Console\Commands; @@ -38,37 +16,30 @@ namespace Pterodactyl\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; use Pterodactyl\Models\Server; +use Pterodactyl\Repositories\Wings\DaemonFileRepository; use Pterodactyl\Services\ModpackApiService; class CheckModpackUpdates extends Command { protected $signature = 'modpackchecker:check'; - protected $description = 'Check all servers for modpack updates'; - - public function __construct(private ModpackApiService $apiService) - { + protected $description = 'Check all servers for modpack updates (hybrid detection)'; + + public function __construct( + private ModpackApiService $apiService, + private DaemonFileRepository $fileRepository + ) { parent::__construct(); } - /** - * Execute the console command. - * - * @return int Exit code (0 = success) - */ public function handle(): int { - $this->info('Starting modpack update check...'); + $this->info('Starting modpack update check (hybrid detection)...'); - // Get all servers that have modpack variables set - $servers = Server::whereHas('variables', function ($q) { - $q->where('env_variable', 'MODPACK_PLATFORM'); - })->get(); - - $this->info("Found {$servers->count()} servers with modpack configuration"); + $servers = Server::all(); + $this->info("Scanning {$servers->count()} servers"); foreach ($servers as $server) { - $this->checkServer($server); - // Rate limiting - sleep between checks + $this->processServer($server); sleep(2); } @@ -76,26 +47,128 @@ class CheckModpackUpdates extends Command return 0; } - /** - * Check a single server for modpack updates. - * - * @param Server $server The server to check - * @return void - */ - private function checkServer(Server $server): void + private function processServer(Server $server): void { $this->line("Checking: {$server->name} ({$server->uuid})"); - try { - $platform = $this->getVariable($server, 'MODPACK_PLATFORM'); - $modpackId = $this->getVariable($server, 'MODPACK_ID'); + // Skip user-overridden servers (manual config) + $existing = DB::table('modpackchecker_servers') + ->where('server_uuid', $server->uuid) + ->first(); - if (!$platform || !$modpackId) { - $this->warn(" Skipping - missing platform or modpack ID"); - return; + if ($existing && $existing->is_user_overridden) { + // Still check for updates, just don't re-detect + if ($existing->platform && $existing->modpack_id) { + $this->checkVersion($server, $existing->platform, $existing->modpack_id, 'manual'); } + return; + } - // Centralized API Call via Service + // Step 1: Try egg variables + $platform = $this->getVariable($server, 'MODPACK_PLATFORM'); + $modpackId = $this->getVariable($server, 'MODPACK_ID'); + + if (!empty($modpackId)) { + $modpackId = $modpackId ?: match($platform) { + 'curseforge' => $this->getVariable($server, 'CURSEFORGE_ID'), + 'modrinth' => $this->getVariable($server, 'MODRINTH_PROJECT_ID'), + 'ftb' => $this->getVariable($server, 'FTB_MODPACK_ID'), + 'technic' => $this->getVariable($server, 'TECHNIC_SLUG'), + default => null + }; + } + + if (!empty($platform) && !empty($modpackId)) { + $this->checkVersion($server, $platform, $modpackId, 'egg'); + return; + } + + // Step 2: File-based detection via DaemonFileRepository + $detected = $this->detectFromFiles($server); + if ($detected) { + $this->checkVersion($server, $detected['platform'], $detected['modpack_id'], 'file'); + return; + } + + // Step 3: Nothing found + $this->warn(" No modpack detected"); + $this->updateDatabase($server, [ + 'status' => 'unconfigured', + 'detection_method' => 'unknown', + 'last_checked' => now(), + ]); + } + + private function detectFromFiles(Server $server): ?array + { + try { + $this->fileRepository->setServer($server); + } catch (\Exception $e) { + return null; + } + + // CurseForge: manifest.json + $cf = $this->detectCurseForge($server); + if ($cf) return $cf; + + // Modrinth: modrinth.index.json + $mr = $this->detectModrinth($server); + if ($mr) return $mr; + + return null; + } + + private function detectCurseForge(Server $server): ?array + { + try { + $content = $this->fileRepository->getContent('manifest.json'); + $manifest = json_decode($content, true); + + if (is_array($manifest) && ($manifest['manifestType'] ?? '') === 'minecraftModpack') { + $projectId = $manifest['projectID'] ?? null; + if ($projectId) { + $this->info(" Detected CurseForge pack (projectID: {$projectId})"); + return [ + 'platform' => 'curseforge', + 'modpack_id' => (string) $projectId, + 'name' => $manifest['name'] ?? null, + ]; + } + } + } catch (\Exception $e) { + // File doesn't exist or node offline + } + return null; + } + + private function detectModrinth(Server $server): ?array + { + try { + $content = $this->fileRepository->getContent('modrinth.index.json'); + $data = json_decode($content, true); + + if (is_array($data) && isset($data['formatVersion'])) { + $slug = isset($data['name']) + ? preg_replace('/[^a-z0-9-]/', '', strtolower(str_replace(' ', '-', $data['name']))) + : null; + if ($slug) { + $this->info(" Detected Modrinth pack (slug: {$slug})"); + return [ + 'platform' => 'modrinth', + 'modpack_id' => $slug, + 'name' => $data['name'] ?? null, + ]; + } + } + } catch (\Exception $e) { + // File doesn't exist or node offline + } + return null; + } + + private function checkVersion(Server $server, string $platform, string $modpackId, string $method): void + { + try { $latestData = $this->apiService->fetchLatestVersion($platform, $modpackId); $currentVersion = $this->getVariable($server, 'MODPACK_CURRENT_VERSION'); $updateAvailable = $currentVersion && $currentVersion !== $latestData['version']; @@ -107,16 +180,20 @@ class CheckModpackUpdates extends Command 'current_version' => $currentVersion, 'latest_version' => $latestData['version'], 'status' => $updateAvailable ? 'update_available' : 'up_to_date', + 'detection_method' => $method, 'error_message' => null, 'last_checked' => now(), ]); - $statusIcon = $updateAvailable ? '🟠 UPDATE AVAILABLE' : '🟢 Up to date'; - $this->info(" {$statusIcon}: {$latestData['name']} - {$latestData['version']}"); + $icon = $updateAvailable ? '🟠 UPDATE' : '🟢 OK'; + $this->info(" {$icon}: {$latestData['name']} — {$latestData['version']} [{$method}]"); } catch (\Exception $e) { $this->error(" Error: {$e->getMessage()}"); $this->updateDatabase($server, [ + 'platform' => $platform, + 'modpack_id' => $modpackId, + 'detection_method' => $method, 'status' => 'error', 'error_message' => $e->getMessage(), 'last_checked' => now(), @@ -124,13 +201,6 @@ class CheckModpackUpdates extends Command } } - /** - * Get an egg variable value from a server. - * - * @param Server $server The server to query - * @param string $name The variable name - * @return string|null The variable value, or null if not set - */ private function getVariable(Server $server, string $name): ?string { $variable = $server->variables() @@ -139,16 +209,6 @@ class CheckModpackUpdates extends Command return $variable?->server_value; } - /** - * Store or update the modpack check results in the database. - * - * Uses updateOrInsert for upsert behavior. - * The server_uuid column is the unique key for matching. - * - * @param Server $server The server being checked - * @param array $data The data to store - * @return void - */ private function updateDatabase(Server $server, array $data): void { DB::table('modpackchecker_servers')->updateOrInsert( diff --git a/services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_12_000000_add_detection_columns.php b/services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_12_000000_add_detection_columns.php new file mode 100644 index 0000000..0465728 --- /dev/null +++ b/services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_12_000000_add_detection_columns.php @@ -0,0 +1,23 @@ +string('detection_method')->default('unknown')->after('error_message'); + $table->boolean('is_user_overridden')->default(false)->after('detection_method'); + }); + } + + public function down(): void + { + Schema::table('modpackchecker_servers', function (Blueprint $table) { + $table->dropColumn(['detection_method', 'is_user_overridden']); + }); + } +};