diff --git a/docs/code-bridge/archive/MSG-2026-04-13-keep-going-p2.md b/docs/code-bridge/archive/MSG-2026-04-13-keep-going-p2.md new file mode 100644 index 0000000..1b80087 --- /dev/null +++ b/docs/code-bridge/archive/MSG-2026-04-13-keep-going-p2.md @@ -0,0 +1,15 @@ +# Chronicler Dispatch — Keep Going with Priority 2 + +**Date:** April 13, 2026 +**From:** Chronicler #84 — The Meridian +**To:** Code + +--- + +Keep going. Deploy and test can wait — consolidate when more pieces are ready. + +Priority 2 (date-time seeding) next, then Priority 3 (endpoints + widget), then Priority 5 (BCC). + +One consolidated deploy when they're all done. + +*— Chronicler #84* 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 72939f5..c7848b1 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 @@ -134,6 +134,10 @@ class CheckModpackUpdates extends Command $ftb = $this->detectFtb($server); if ($ftb) return $ftb; + // BCC: logs/latest.log + $bcc = $this->detectFromBccLog($server); + if ($bcc) return $bcc; + return null; } @@ -248,10 +252,11 @@ class CheckModpackUpdates extends Command } $currentFileId = $existing->current_file_id ?? null; - // First time — seed with latest + // First time — try date-time seeding, fall back to latest if (empty($currentVersion)) { - $currentVersion = $latestVersion; - $currentFileId = $latestFileId; + $seeded = $this->seedCurrentVersion($server, $platform, $modpackId, $latestVersion, $latestFileId); + $currentVersion = $seeded['version']; + $currentFileId = $seeded['file_id']; } // Compare: prefer file ID, fall back to string @@ -291,6 +296,69 @@ class CheckModpackUpdates extends Command } } + /** + * BCC log parsing — detect modpack from BetterCompatibilityChecker output. + */ + private function detectFromBccLog(Server $server): ?array + { + try { + $log = $this->fileRepository->getContent('logs/latest.log'); + if (preg_match('/Loaded BetterCompatibilityChecker - Modpack: (.+?) \| Version: (.+)/', $log, $m)) { + $name = trim($m[1]); + $version = trim($m[2]); + if ($name !== 'CHANGE_ME' && $version !== 'CHANGE_ME') { + $this->info(" Detected via BCC log: {$name} v{$version}"); + return [ + 'platform' => 'unknown', + 'modpack_id' => strtolower(str_replace(' ', '-', $name)), + 'name' => $name, + 'installed_version' => $version, + ]; + } + } + } catch (\Exception $e) { + $this->line(" [debug] BCC log: " . $e->getMessage()); + } + return null; + } + + /** + * Seed current version using date-time heuristic. + * Finds the release closest to (but not after) the server's created_at date. + */ + private function seedCurrentVersion(Server $server, string $platform, string $modpackId, string $fallbackVersion, ?string $fallbackFileId): array + { + try { + $installDate = $server->created_at?->toISOString(); + if (!$installDate) { + return ['version' => $fallbackVersion, 'file_id' => $fallbackFileId]; + } + + $history = $this->apiService->fetchFileHistory($platform, $modpackId); + if (empty($history)) { + return ['version' => $fallbackVersion, 'file_id' => $fallbackFileId]; + } + + // Find the release closest to but not after install date + $matched = collect($history) + ->filter(fn($f) => !empty($f['release_date']) && $f['release_date'] <= $installDate) + ->sortByDesc('release_date') + ->first(); + + if ($matched) { + $this->line(" [seed] Matched version {$matched['version']} (released {$matched['release_date']}) to install date {$installDate}"); + return [ + 'version' => $matched['version'], + 'file_id' => $matched['file_id'] ?? $fallbackFileId, + ]; + } + } catch (\Exception $e) { + $this->line(" [seed] Heuristic failed: " . $e->getMessage()); + } + + return ['version' => $fallbackVersion, 'file_id' => $fallbackFileId]; + } + private function getVariable(Server $server, string $name): ?string { $variable = $server->variables() diff --git a/services/modpack-version-checker/blueprint-extension/app/Http/Controllers/ModpackAPIController.php b/services/modpack-version-checker/blueprint-extension/app/Http/Controllers/ModpackAPIController.php index aa1c012..33dca20 100644 --- a/services/modpack-version-checker/blueprint-extension/app/Http/Controllers/ModpackAPIController.php +++ b/services/modpack-version-checker/blueprint-extension/app/Http/Controllers/ModpackAPIController.php @@ -287,4 +287,144 @@ class ModpackAPIController extends Controller return response()->json($result); } + + /** + * Get cached status for a single server (zero-click widget). + * Reads from DB only — no external API calls. + */ + public function serverStatus(Request $request, Server $server): JsonResponse + { + $cached = DB::table('modpackchecker_servers') + ->where('server_uuid', $server->uuid) + ->first(); + + if (!$cached || $cached->is_ignored ?? false) { + return response()->json([ + 'configured' => false, + 'is_ignored' => $cached->is_ignored ?? false, + ]); + } + + return response()->json([ + 'configured' => !empty($cached->platform) && $cached->status !== 'unconfigured', + 'platform' => $cached->platform, + 'modpack_name' => $cached->modpack_name, + 'current_version' => $cached->current_version, + 'latest_version' => $cached->latest_version, + 'current_file_id' => $cached->current_file_id ?? null, + 'latest_file_id' => $cached->latest_file_id ?? null, + 'update_available' => $cached->status === 'update_available', + 'last_checked' => $cached->last_checked, + 'detection_method' => $cached->detection_method ?? 'unknown', + 'is_ignored' => $cached->is_ignored ?? false, + ]); + } + + /** + * Get release history for recalibrate dropdown. + * Makes external API call — rate limited. + */ + public function releases(Request $request, Server $server): JsonResponse + { + $limitKey = 'modpack_releases_' . $server->uuid; + if (RateLimiter::tooManyAttempts($limitKey, 2)) { + $seconds = RateLimiter::availableIn($limitKey); + return response()->json(['error' => "Too many requests. Wait {$seconds}s."], 429); + } + RateLimiter::hit($limitKey, 60); + + $cached = DB::table('modpackchecker_servers') + ->where('server_uuid', $server->uuid) + ->first(); + + if (!$cached || empty($cached->platform) || empty($cached->modpack_id)) { + return response()->json(['releases' => []]); + } + + $releases = $this->apiService->fetchFileHistory($cached->platform, $cached->modpack_id, 10); + + return response()->json(['releases' => $releases]); + } + + /** + * Recalibrate current version (user selects from release list). + * Sets is_user_overridden = true. + */ + public function calibrate(Request $request, Server $server): JsonResponse + { + $fileId = $request->input('file_id'); + $version = $request->input('version'); + + if (empty($version)) { + return response()->json(['error' => 'version is required'], 400); + } + + $updated = DB::table('modpackchecker_servers') + ->where('server_uuid', $server->uuid) + ->update([ + 'current_version' => $version, + 'current_file_id' => $fileId, + 'is_user_overridden' => true, + 'updated_at' => now(), + ]); + + if (!$updated) { + return response()->json(['error' => 'Server not found in modpack cache'], 404); + } + + // Re-evaluate update status + $cached = DB::table('modpackchecker_servers') + ->where('server_uuid', $server->uuid) + ->first(); + + $updateAvailable = false; + if ($cached->latest_file_id && $fileId) { + $updateAvailable = $cached->latest_file_id !== $fileId; + } else { + $updateAvailable = $cached->latest_version !== $version; + } + + DB::table('modpackchecker_servers') + ->where('server_uuid', $server->uuid) + ->update([ + 'status' => $updateAvailable ? 'update_available' : 'up_to_date', + ]); + + return response()->json([ + 'success' => true, + 'current_version' => $version, + 'update_available' => $updateAvailable, + ]); + } + + /** + * Toggle is_ignored flag for a server. + */ + public function toggleIgnore(Request $request, Server $server): JsonResponse + { + $existing = DB::table('modpackchecker_servers') + ->where('server_uuid', $server->uuid) + ->first(); + + if (!$existing) { + // Create a record to track the ignore + DB::table('modpackchecker_servers')->insert([ + 'server_uuid' => $server->uuid, + 'is_ignored' => true, + 'status' => 'ignored', + 'updated_at' => now(), + ]); + return response()->json(['is_ignored' => true]); + } + + $newState = !($existing->is_ignored ?? false); + DB::table('modpackchecker_servers') + ->where('server_uuid', $server->uuid) + ->update([ + 'is_ignored' => $newState, + 'updated_at' => now(), + ]); + + return response()->json(['is_ignored' => $newState]); + } } diff --git a/services/modpack-version-checker/blueprint-extension/app/Services/ModpackApiService.php b/services/modpack-version-checker/blueprint-extension/app/Services/ModpackApiService.php index f5b45b9..d903979 100644 --- a/services/modpack-version-checker/blueprint-extension/app/Services/ModpackApiService.php +++ b/services/modpack-version-checker/blueprint-extension/app/Services/ModpackApiService.php @@ -168,6 +168,81 @@ class ModpackApiService ]; } + /** + * Fetch file/version history for date-time seeding. + * Returns array of ['file_id', 'version', 'display_name', 'release_date']. + */ + public function fetchFileHistory(string $platform, string $modpackId, int $limit = 20): array + { + return match($platform) { + 'curseforge' => $this->fetchCurseForgeHistory($modpackId, $limit), + 'modrinth' => $this->fetchModrinthHistory($modpackId, $limit), + 'ftb' => $this->fetchFtbHistory($modpackId, $limit), + default => [] + }; + } + + private function fetchCurseForgeHistory(string $modpackId, int $limit): array + { + $apiKey = trim($this->blueprint->dbGet('modpackchecker', 'curseforge_api_key') ?? ''); + if (empty($apiKey)) return []; + + $response = Http::timeout(15)->withHeaders([ + 'x-api-key' => $apiKey, + 'Accept' => 'application/json', + ])->get("https://api.curseforge.com/v1/mods/{$modpackId}/files", [ + 'pageSize' => $limit, + ]); + + if (!$response->successful()) return []; + + return collect($response->json()['data'] ?? []) + ->map(fn($f) => [ + 'file_id' => (string) $f['id'], + 'version' => $f['displayName'] ?? 'Unknown', + 'display_name' => $f['displayName'] ?? 'Unknown', + 'release_date' => $f['fileDate'] ?? null, + ]) + ->toArray(); + } + + private function fetchModrinthHistory(string $projectId, int $limit): array + { + $response = Http::timeout(15) + ->withHeaders(['User-Agent' => 'FirefrostGaming/ModpackChecker/1.0']) + ->get("https://api.modrinth.com/v2/project/{$projectId}/version"); + + if (!$response->successful()) return []; + + return collect($response->json() ?? []) + ->take($limit) + ->map(fn($v) => [ + 'file_id' => $v['id'] ?? null, + 'version' => $v['version_number'] ?? 'Unknown', + 'display_name' => $v['name'] ?? $v['version_number'] ?? 'Unknown', + 'release_date' => $v['date_published'] ?? null, + ]) + ->toArray(); + } + + private function fetchFtbHistory(string $modpackId, int $limit): array + { + $response = Http::timeout(15) + ->get("https://api.modpacks.ch/public/modpack/{$modpackId}"); + + if (!$response->successful()) return []; + + return collect($response->json()['versions'] ?? []) + ->take($limit) + ->map(fn($v) => [ + 'file_id' => (string) ($v['id'] ?? ''), + 'version' => $v['name'] ?? 'Unknown', + 'display_name' => $v['name'] ?? 'Unknown', + 'release_date' => isset($v['updated']) ? date('c', $v['updated']) : null, + ]) + ->toArray(); + } + /** * Query Technic Platform API for latest modpack version. * diff --git a/services/modpack-version-checker/blueprint-extension/routes/client.php b/services/modpack-version-checker/blueprint-extension/routes/client.php index 05d4a43..ada16ed 100644 --- a/services/modpack-version-checker/blueprint-extension/routes/client.php +++ b/services/modpack-version-checker/blueprint-extension/routes/client.php @@ -13,8 +13,20 @@ use Pterodactyl\Http\Controllers\ModpackAPIController; | */ -// Resulting URL: /api/client/extensions/modpackchecker/servers/{server}/check +// Manual check (POST — makes live API call) Route::post('/servers/{server}/check', [ModpackAPIController::class, 'manualCheck']); -// Resulting URL: /api/client/extensions/modpackchecker/status +// Dashboard badge status (GET — reads from DB cache only) Route::get('/status', [ModpackAPIController::class, 'getStatus']); + +// Server status from cache (GET — no external API calls) +Route::get('/servers/{server}/status', [ModpackAPIController::class, 'serverStatus']); + +// Release history for recalibrate dropdown (GET — makes external API call) +Route::get('/servers/{server}/releases', [ModpackAPIController::class, 'releases']); + +// Recalibrate current version (POST — saves user selection) +Route::post('/servers/{server}/calibrate', [ModpackAPIController::class, 'calibrate']); + +// Ignore/restore server (POST — toggles is_ignored flag) +Route::post('/servers/{server}/ignore', [ModpackAPIController::class, 'toggleIgnore']);