From 27b27447868aecad0f69e92f460bf20fda4716ce Mon Sep 17 00:00:00 2001 From: "Claude (Chronicler #83 - The Compiler)" Date: Mon, 13 Apr 2026 06:09:05 -0500 Subject: [PATCH] Truth File strategy: never seed from latest, calibrate or detect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CheckModpackUpdates: - Reads .modpack-checker.json Truth File from server filesystem - Falls back to manifest.json, extracts fileID, writes Truth File - NEVER seeds current_version from latest API result - Unknown version → status: pending_calibration (not up_to_date) - Removed seedCurrentVersion heuristic — replaced with Truth File - writeTruthFile() helper writes .modpack-checker.json via Wings ModpackAPIController: - calibrate() now writes Truth File after DB update - Persists across server reinstalls and cron runs wrapper.tsx: - pending_calibration: shows "Version unknown" + "Identify Version" button - Ignored servers: muted card with "Resume" button (not hidden) - Extracted renderCalibrateDropdown() for reuse - Error state shows message instead of vanishing Migration: - Updates existing unknown+detected rows to pending_calibration Co-Authored-By: Claude Opus 4.6 (1M context) --- ...2026-04-13-truth-file-version-detection.md | 264 ++++++++++++++++++ .../Console/Commands/CheckModpackUpdates.php | 94 ++++--- .../Http/Controllers/ModpackAPIController.php | 15 + ..._000002_add_pending_calibration_status.php | 29 ++ .../views/server/wrapper.tsx | 115 ++++++-- 5 files changed, 453 insertions(+), 64 deletions(-) create mode 100644 docs/code-bridge/archive/MSG-2026-04-13-truth-file-version-detection.md create mode 100644 services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_13_000002_add_pending_calibration_status.php diff --git a/docs/code-bridge/archive/MSG-2026-04-13-truth-file-version-detection.md b/docs/code-bridge/archive/MSG-2026-04-13-truth-file-version-detection.md new file mode 100644 index 0000000..b00f076 --- /dev/null +++ b/docs/code-bridge/archive/MSG-2026-04-13-truth-file-version-detection.md @@ -0,0 +1,264 @@ +# MSG-2026-04-13-truth-file-version-detection + +**From:** Chronicler #85 +**Date:** 2026-04-13 +**Priority:** HIGH — needed before BuiltByBit listings, ideally before April 15 +**Status:** OPEN + +## Background + +After 3 rounds of Gemini consultation and live Wings API testing, we have a +complete architectural decision for version detection. Here's everything you +need to implement it. + +--- + +## What We Proved via Live Testing + +- `DaemonFileRepository::getContent()` and `putContent()` both work from Panel + PHP context (confirmed via tinker on live panel) +- `manifest.json` does NOT survive Pterodactyl modpack installation on any of + our 22 servers — installer discards it +- `modpack_installations` table has no version info — only provider + modpack_id +- Wings directory listing works — we can read the server filesystem + +--- + +## The Fix: Truth File Strategy + +### New Detection Order (replaces current fall-through logic) + +``` +1. DB: current_file_id present? → skip to comparison +2. Wings: read /.modpack-checker.json → parse file_id → store in DB → compare +3. Wings: read /manifest.json → parse file_id → write Truth File → store in DB → compare +4. Nothing found → set status = 'pending_calibration' → STOP (never seed from latest) +``` + +**CRITICAL RULE: Never seed current_version from latest API result.** +If we don't know the installed version, we say we don't know. Store `null` / +`pending_calibration`, never assume latest = installed. + +--- + +## The Truth File + +Write `.modpack-checker.json` to server root via `DaemonFileRepository::putContent()`. + +```json +{ + "extension": "modpackchecker", + "project_id": "490660", + "file_id": "7097953", + "version": "5.10.15", + "calibrated_at": "2026-04-13T05:00:00+00:00" +} +``` + +**Write the Truth File in TWO scenarios:** +1. When admin calibrates (picks version from dropdown) +2. When ANY detection method successfully finds the version (manifest.json etc.) + +This makes detection self-healing — once tracked by any method, always tracked +even if the original source file disappears in a future update. + +--- + +## New DB Status Value + +Add `pending_calibration` to the status enum in `modpackchecker_servers`: +```sql +ALTER TABLE modpackchecker_servers +MODIFY COLUMN status ENUM( + 'up_to_date','update_available','error','unknown','pending_calibration' +) NOT NULL DEFAULT 'unknown'; +``` + +--- + +## Files to Change + +### 1. `app/Console/Commands/CheckModpackUpdates.php` + +In `checkVersion()` — replace the seeding fallback with Truth File logic: + +```php +// After existing detection chain (egg var, existing DB)... + +// NEW: Check for Truth File on server filesystem +if (empty($currentVersion)) { + try { + $repo = app(\Pterodactyl\Repositories\Wings\DaemonFileRepository::class) + ->setServer($server); + $truthFile = json_decode($repo->getContent('/.modpack-checker.json'), true); + if (!empty($truthFile['file_id'])) { + $currentVersion = $truthFile['version'] ?? null; + $currentFileId = $truthFile['file_id']; + } + } catch (\Exception $e) { + // File doesn't exist — continue to next check + } +} + +// NEW: Check for legacy manifest.json +if (empty($currentVersion)) { + try { + $repo = $repo ?? app(\Pterodactyl\Repositories\Wings\DaemonFileRepository::class) + ->setServer($server); + $manifest = json_decode($repo->getContent('/manifest.json'), true); + if (!empty($manifest['files'][0]['fileID'])) { + $currentFileId = (string) $manifest['files'][0]['fileID']; + // Write Truth File immediately so it persists + $this->writeTruthFile($server, $modpackId, $currentFileId, null); + } + } catch (\Exception $e) { + // File doesn't exist — continue + } +} + +// REMOVE the seeding fallback entirely. Replace with: +if (empty($currentVersion) && empty($currentFileId)) { + $this->updateDatabase($server, [ + 'platform' => $platform, + 'modpack_id' => $modpackId, + 'modpack_name' => $latestData['name'], + 'status' => 'pending_calibration', + 'detection_method' => $method, + 'last_checked' => now(), + ]); + $this->info(" ⏳ PENDING: {$latestData['name']} — calibration required"); + return; +} +``` + +Add a `writeTruthFile()` helper method: +```php +private function writeTruthFile(Server $server, string $projectId, + string $fileId, ?string $version): void +{ + try { + $repo = app(\Pterodactyl\Repositories\Wings\DaemonFileRepository::class) + ->setServer($server); + $repo->putContent('/.modpack-checker.json', json_encode([ + 'extension' => 'modpackchecker', + 'project_id' => $projectId, + 'file_id' => $fileId, + 'version' => $version, + 'calibrated_at' => now()->toIso8601String(), + ], JSON_PRETTY_PRINT)); + } catch (\Exception $e) { + // Non-fatal — log and continue + \Log::warning('[MVC] Could not write Truth File: ' . $e->getMessage()); + } +} +``` + +### 2. `app/Http/Controllers/ModpackAPIController.php` + +In the `calibrate()` method — after storing the file_id in DB, write the Truth File: + +```php +// After DB update in calibrate()... +$this->writeTruthFileForServer($server, $modpackId, $fileId, $version); +``` + +Add the same `writeTruthFile` logic (or extract to a shared service). + +### 3. `views/server/wrapper.tsx` + +Add `pending_calibration` handling. When `data.configured === false` and +status is pending, show calibration prompt instead of empty/error state: + +```tsx +// After error state check, before main render... +if (!data.configured && !data.update_available) { + return ( +
+
+
+ + + {data.modpack_name || 'Modpack'} — Version unknown + +
+ +
+
+ ); +} +``` + +### 4. Ignore toggle — Muted Card (from previous Gemini consultation) + +Replace `if (data.is_ignored) return null;` with: + +```tsx +if (data.is_ignored) { + return ( +
+
+
+ + + {data.modpack_name || 'Modpack'} — Updates ignored + +
+ +
+
+ ); +} +``` + +--- + +## Migration Needed + +```sql +ALTER TABLE modpackchecker_servers +MODIFY COLUMN status ENUM( + 'up_to_date','update_available','error','unknown','pending_calibration' +) NOT NULL DEFAULT 'unknown'; +``` + +Add to a new migration file: +`database/migrations/2026_04_13_000002_add_pending_calibration_status.php` + +--- + +## Post-Launch Enhancement (NOT for April 15) + +Log parsing at server restart — possible future detection method. Every modpack +formats startup logs differently, too brittle for now. Flag as Task for after +launch. + +--- + +## Testing Checklist + +- [ ] Server with no Truth File → shows "Identify Version" button +- [ ] Admin selects version → Truth File written to server root → DB updated +- [ ] Cron reads Truth File on next run → correct status shown +- [ ] Server shows update available after Truth File written with old version +- [ ] Ignore toggle → muted card with Resume button (not vanish) +- [ ] Resume → normal card restored +- [ ] Never seeds current_version from latest API result + +--- +*— Chronicler #85* 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 032d377..301f33b 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 @@ -240,7 +240,7 @@ class CheckModpackUpdates extends Command $latestVersion = $latestData['version'] ?? 'Unknown'; $latestFileId = $latestData['file_id'] ?? null; - // Get current_version: manifest > egg variable > existing DB > seed with latest + // Get current_version: manifest arg > egg variable > existing DB $currentVersion = $installedVersion; $currentFileId = null; @@ -257,11 +257,53 @@ class CheckModpackUpdates extends Command } $currentFileId = $existing->current_file_id ?? null; - // First time — try date-time seeding, fall back to latest - if (empty($currentVersion)) { - $seeded = $this->seedCurrentVersion($server, $platform, $modpackId, $latestVersion, $latestFileId); - $currentVersion = $seeded['version']; - $currentFileId = $seeded['file_id']; + // Truth File: read .modpack-checker.json from server filesystem + if (empty($currentFileId)) { + try { + $this->fileRepository->setServer($server); + $truthRaw = $this->fileRepository->getContent('.modpack-checker.json'); + $truthFile = json_decode($truthRaw, true); + if (!empty($truthFile['file_id'])) { + $currentFileId = $truthFile['file_id']; + $currentVersion = $currentVersion ?: ($truthFile['version'] ?? null); + $this->line(" [truth] Read file_id {$currentFileId} from .modpack-checker.json"); + } + } catch (\Exception $e) { + // Truth File doesn't exist yet + } + } + + // Legacy manifest.json: extract fileID, write Truth File + if (empty($currentFileId)) { + try { + $this->fileRepository->setServer($server); + $manifest = json_decode($this->fileRepository->getContent('manifest.json'), true); + if (!empty($manifest['files'][0]['fileID'])) { + $currentFileId = (string) $manifest['files'][0]['fileID']; + $this->writeTruthFile($server, $modpackId, $currentFileId, $manifest['version'] ?? null); + $currentVersion = $currentVersion ?: ($manifest['version'] ?? null); + $this->line(" [manifest] Extracted file_id {$currentFileId}, wrote Truth File"); + } + } catch (\Exception $e) { + // No manifest + } + } + + // NEVER seed from latest — if unknown, mark pending_calibration + if (empty($currentVersion) && empty($currentFileId)) { + $this->updateDatabase($server, [ + 'platform' => $platform, + 'modpack_id' => $modpackId, + 'modpack_name' => $latestData['name'], + 'latest_version' => $latestVersion, + 'latest_file_id' => $latestFileId, + 'status' => 'pending_calibration', + 'detection_method' => $method, + 'error_message' => null, + 'last_checked' => now(), + ]); + $this->info(" ⏳ PENDING: {$latestData['name']} — calibration required"); + return; } // Compare: prefer file ID, fall back to string @@ -328,40 +370,22 @@ class CheckModpackUpdates extends Command } /** - * Seed current version using date-time heuristic. - * Finds the release closest to (but not after) the server's created_at date. + * Write .modpack-checker.json Truth File to server filesystem. */ - private function seedCurrentVersion(Server $server, string $platform, string $modpackId, string $fallbackVersion, ?string $fallbackFileId): array + private function writeTruthFile(Server $server, string $projectId, string $fileId, ?string $version): void { 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, - ]; - } + $this->fileRepository->setServer($server); + $this->fileRepository->putContent('.modpack-checker.json', json_encode([ + 'extension' => 'modpackchecker', + 'project_id' => $projectId, + 'file_id' => $fileId, + 'version' => $version, + 'calibrated_at' => now()->toIso8601String(), + ], JSON_PRETTY_PRINT)); } catch (\Exception $e) { - $this->line(" [seed] Heuristic failed: " . $e->getMessage()); + \Log::warning('[MVC] Could not write Truth File: ' . $e->getMessage()); } - - return ['version' => $fallbackVersion, 'file_id' => $fallbackFileId]; } private function getVariable(Server $server, string $name): ?string 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 b2a07de..fef7f52 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 @@ -394,6 +394,21 @@ class ModpackAPIController extends Controller 'status' => $updateAvailable ? 'update_available' : 'up_to_date', ]); + // Write Truth File to server filesystem + $modpackId = $cached->modpack_id ?? ''; + try { + $this->fileRepository->setServer($server); + $this->fileRepository->putContent('.modpack-checker.json', json_encode([ + 'extension' => 'modpackchecker', + 'project_id' => $modpackId, + 'file_id' => $fileId, + 'version' => $version, + 'calibrated_at' => now()->toIso8601String(), + ], JSON_PRETTY_PRINT)); + } catch (\Exception $e) { + \Log::warning('[MVC] Could not write Truth File on calibrate: ' . $e->getMessage()); + } + return response()->json([ 'success' => true, 'current_version' => $version, diff --git a/services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_13_000002_add_pending_calibration_status.php b/services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_13_000002_add_pending_calibration_status.php new file mode 100644 index 0000000..194993a --- /dev/null +++ b/services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_13_000002_add_pending_calibration_status.php @@ -0,0 +1,29 @@ +where('status', 'unknown') + ->whereNotNull('platform') + ->whereNull('current_version') + ->update(['status' => 'pending_calibration']); + } + + public function down(): void + { + DB::table('modpackchecker_servers') + ->where('status', 'pending_calibration') + ->update(['status' => 'unknown']); + } +}; diff --git a/services/modpack-version-checker/blueprint-extension/views/server/wrapper.tsx b/services/modpack-version-checker/blueprint-extension/views/server/wrapper.tsx index 6bfa2a3..82269e7 100644 --- a/services/modpack-version-checker/blueprint-extension/views/server/wrapper.tsx +++ b/services/modpack-version-checker/blueprint-extension/views/server/wrapper.tsx @@ -106,7 +106,30 @@ const ModpackVersionCard: React.FC = () => { }; if (loading) return null; - if (data?.is_ignored) return null; + + // Muted card for ignored servers (with Resume button) + if (data?.is_ignored) { + return ( +
+
+
+ + + {data.modpack_name || 'Modpack'} — Updates ignored + +
+ +
+
+ ); + } if (error && !data) return (
{
); + // Calibrate dropdown (reusable) + const renderCalibrateDropdown = () => ( +
+

Select your installed version:

+ {loadingReleases &&

Loading releases...

} + {!loadingReleases && releases.length === 0 && ( +

No releases found

+ )} + {releases.map((r) => ( + + ))} + +
+ ); + + // Pending calibration — show "Identify Version" prompt + if (data && !data.configured && data.modpack_name) { + return ( +
+
+
+ + + {data.modpack_name} — Version unknown + +
+
+ + +
+
+ {showCalibrate && renderCalibrateDropdown()} +
+ ); + } + const hasUpdate = data.update_available; const configured = data.configured; @@ -190,34 +274,7 @@ const ModpackVersionCard: React.FC = () => { {/* Calibrate dropdown */} - {showCalibrate && ( -
-

Select your installed version:

- {loadingReleases &&

Loading releases...

} - {!loadingReleases && releases.length === 0 && ( -

No releases found

- )} - {releases.map((r) => ( - - ))} - -
- )} + {showCalibrate && renderCalibrateDropdown()} ); };