Files
firefrost-services/docs/code-bridge/archive/MSG-2026-04-13-truth-file-version-detection.md
Claude (Chronicler #83 - The Compiler) 27b2744786 Truth File strategy: never seed from latest, calibrate or detect
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) <noreply@anthropic.com>
2026-04-13 06:09:05 -05:00

8.5 KiB

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().

{
    "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:

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:

// 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:

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:

// 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:

// After error state check, before main render...
if (!data.configured && !data.update_available) {
    return (
        <div className={classNames(
            'rounded shadow-lg bg-gray-600',
            'col-span-3 md:col-span-2 lg:col-span-6',
            'px-3 py-2 md:p-3 lg:p-4 mt-2'
        )}>
            <div className="flex items-center justify-between">
                <div className="flex items-center">
                    <FontAwesomeIcon icon={faCube} className="w-4 h-4 mr-2 text-gray-400" />
                    <span className="text-gray-400 text-sm">
                        {data.modpack_name || 'Modpack'}  Version unknown
                    </span>
                </div>
                <button onClick={openCalibrate}
                    className="text-xs px-3 py-1 bg-cyan-600 hover:bg-cyan-500 
                               text-white rounded transition-colors">
                    Identify Version
                </button>
            </div>
        </div>
    );
}

4. Ignore toggle — Muted Card (from previous Gemini consultation)

Replace if (data.is_ignored) return null; with:

if (data.is_ignored) {
    return (
        <div className={classNames(
            'rounded shadow-lg bg-gray-600 opacity-50',
            'col-span-3 md:col-span-2 lg:col-span-6',
            'px-3 py-2 md:p-3 lg:p-4 mt-2'
        )}>
            <div className="flex items-center justify-between">
                <div className="flex items-center">
                    <FontAwesomeIcon icon={faCube} className="w-4 h-4 mr-2 text-gray-400" />
                    <span className="text-gray-400 text-sm">
                        {data.modpack_name || 'Modpack'}  Updates ignored
                    </span>
                </div>
                <button onClick={toggleIgnore}
                    className="text-xs px-3 py-1 bg-gray-700 hover:bg-gray-600 
                               text-gray-200 rounded transition-colors">
                    Resume
                </button>
            </div>
        </div>
    );
}

Migration Needed

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