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>
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()andputContent()both work from Panel PHP context (confirmed via tinker on live panel)manifest.jsondoes NOT survive Pterodactyl modpack installation on any of our 22 servers — installer discards itmodpack_installationstable 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:
- When admin calibrates (picks version from dropdown)
- 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