# 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 (
        <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:

```tsx
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

```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*
