diff --git a/docs/code-bridge/archive/MSG-2026-04-12-manifest-version.md b/docs/code-bridge/archive/MSG-2026-04-12-manifest-version.md new file mode 100644 index 0000000..ae91f38 --- /dev/null +++ b/docs/code-bridge/archive/MSG-2026-04-12-manifest-version.md @@ -0,0 +1,65 @@ +# Chronicler Dispatch — manifest.json has version field on some servers + +**Date:** 2026-04-12 +**From:** Chronicler #84 — The Meridian +**To:** Code + +--- + +## Discovery + +Mythcraft 5 HAS a `manifest.json` at `/home/container/manifest.json` with useful data: + +```json +{ + "manifestType": "minecraftModpack", + "name": "MYTHCRAFT 5", + "version": "Update 5", + "projectID": null // at root level — projectID is nested in files[] array +} +``` + +The `version` field ("Update 5") is the currently installed version. This is real data +we can use for `current_version` — not just the pack ID. + +## What This Means for Detection + +When `detectCurseForge()` reads `manifest.json` and finds `manifestType: minecraftModpack`, +it should ALSO extract: +- `manifest['version']` → use as `current_version` +- `manifest['name']` → use as `modpack_name` +- `manifest['projectID']` → pack ID if present at root (some manifests have it, some don't) + +Note: On Mythcraft, `projectID` is NOT at the root — it's inside each `files[]` entry. +The root doesn't have a project ID. The `modpack_installations` table has it (737497). + +## Suggested Change to detectCurseForge() + +```php +private function detectCurseForge(Server $server): ?array +{ + try { + $content = $this->fileRepository->getContent('manifest.json'); + $manifest = json_decode($content, true); + + if (is_array($manifest) && ($manifest['manifestType'] ?? '') === 'minecraftModpack') { + $projectId = $manifest['projectID'] ?? null; + + return [ + 'platform' => 'curseforge', + 'modpack_id' => $projectId ? (string) $projectId : null, + 'name' => $manifest['name'] ?? null, + 'installed_version' => $manifest['version'] ?? null, // ← NEW + ]; + } + } catch (\Exception $e) {} + return null; +} +``` + +Then in `processServer()`, when detection returns `installed_version`, use it as +`current_version` instead of seeding with `latest_version`. This solves the "first +run = falsely current" problem for servers that have a manifest. + +*— Chronicler #84, The Meridian* +**Fire + Frost + Foundation** 💙🔥❄️ diff --git a/docs/code-bridge/archive/MSG-2026-04-13-v110-architecture.md b/docs/code-bridge/archive/MSG-2026-04-13-v110-architecture.md new file mode 100644 index 0000000..71dd08a --- /dev/null +++ b/docs/code-bridge/archive/MSG-2026-04-13-v110-architecture.md @@ -0,0 +1,236 @@ +# Chronicler Dispatch — v1.1.0 Full Architecture Plan (Gemini Consultation Complete) + +**Date:** April 13, 2026 +**From:** Chronicler #84 — The Meridian +**To:** Code + +--- + +## Context + +Gemini consultation complete. Michael has reviewed and approved the plan. +Full consultation: `firefrost-operations-manual/docs/consultations/gemini-modpackchecker-ux-overhaul-2026-04-12.md` + +No time pressure — Michael wants to get this right. These are v1.1.0 priorities in order. + +--- + +## Priority 1: File ID Comparison (Foundation) + +**The problem:** We're comparing messy display name strings ("ATM10-6.5" vs "ATM10-6.6"). Unreliable and ugly. + +**Gemini's fix:** Use sequential File IDs from CurseForge/Modrinth. `latest_file_id > current_file_id` = update available. Clean, reliable, platform-agnostic. + +**Schema migration needed:** +```sql +ALTER TABLE modpackchecker_servers +ADD COLUMN current_file_id VARCHAR(64) NULL, +ADD COLUMN latest_file_id VARCHAR(64) NULL; +``` + +**API changes:** +- `ModpackApiService::fetchLatestVersion()` should also return `file_id` +- CurseForge: use the file's `id` field +- Modrinth: use the version's `id` field +- FTB: use the version `id` +- Technic: use the build number + +**Version comparison logic:** +```php +// Prefer file ID comparison if available +if ($currentFileId && $latestFileId) { + $updateAvailable = $latestFileId !== $currentFileId; +} else { + // Fallback to string comparison + $updateAvailable = $currentVersion !== $latestVersion; +} +``` + +--- + +## Priority 2: Date-Time Seeding Heuristic + +**The problem:** First run seeds `current_version = latest_version`, which is wrong for servers that have been running old versions. + +**Gemini's fix:** On first detection, fetch the platform's file history. Find the release closest to (but not after) `modpack_installations.created_at`. That's the assumed current version. + +```php +private function seedCurrentVersion(string $platform, string $modpackId, ?string $installDate): array +{ + if (!$installDate) { + // No install date — fall back to latest + return $this->apiService->fetchLatestVersion($platform, $modpackId); + } + + $allFiles = $this->apiService->fetchFileHistory($platform, $modpackId); + + $assumedCurrent = collect($allFiles) + ->filter(fn($f) => $f['releaseDate'] <= $installDate) + ->sortByDesc('releaseDate') + ->first(); + + return $assumedCurrent ?? $this->apiService->fetchLatestVersion($platform, $modpackId); +} +``` + +**New method needed:** `ModpackApiService::fetchFileHistory(platform, modpackId)` +- CurseForge: `GET /v1/mods/{modId}/files` — returns all files with dates +- Modrinth: `GET /project/{id}/version` — returns all versions with dates +- Returns: array of `['id', 'version', 'displayName', 'releaseDate']` + +**Also:** If `manifest.json` exists and has a `version` field — use that directly as `current_version` instead of any heuristic. We confirmed this works on 5 servers (Mythcraft, Create Plus, Beyond Depth, Beyond Ascension, Homestead). Manifest version is truth — no guessing needed. + +--- + +## Priority 3: Zero-Click Widget with Recalibrate + +**The problem:** Widget requires clicking, shows only latest version string, no comparison, no context. + +**Gemini's redesign:** + +### New GET endpoint needed: +`GET /api/client/extensions/modpackchecker/servers/{server}/status` +Returns cached DB data (NO external API calls): +```json +{ + "configured": true, + "platform": "curseforge", + "modpack_name": "MYTHCRAFT 5", + "current_version": "Update 5", + "latest_version": "Update 5", + "current_file_id": "6148845", + "latest_file_id": "6148845", + "update_available": false, + "last_checked": "2026-04-13T04:00:00Z", + "detection_method": "installer" +} +``` + +### New GET endpoint for Recalibrate dropdown: +`GET /api/client/extensions/modpackchecker/servers/{server}/releases` +Returns last 10 releases from platform (DOES make external API call): +```json +{ + "releases": [ + {"file_id": "6148845", "display_name": "MYTHCRAFT 5 | Update 5", "release_date": "2026-03-27"}, + {"file_id": "6089021", "display_name": "MYTHCRAFT 5 | Update 4.1", "release_date": "2026-03-11"}, + ... + ] +} +``` + +### New POST endpoint for Recalibrate save: +`POST /api/client/extensions/modpackchecker/servers/{server}/calibrate` +Body: `{ "file_id": "6089021", "version": "Update 4.1" }` +Sets `current_version`, `current_file_id`, `is_user_overridden = true` + +### Widget TSX redesign: +```tsx +const ModpackVersionCard: React.FC = () => { + const uuid = ServerContext.useStoreState(state => state.server.data?.uuid); + const [data, setData] = useState(null); + const [showCalibrate, setShowCalibrate] = useState(false); + const [releases, setReleases] = useState([]); + + // Zero-click: load on mount from cache + useEffect(() => { + if (!uuid) return; + http.get(`/api/client/extensions/modpackchecker/servers/${uuid}/status`) + .then(res => setData(res.data)) + .catch(() => {}); + }, [uuid]); + + const openCalibrate = () => { + http.get(`/api/client/extensions/modpackchecker/servers/${uuid}/releases`) + .then(res => setReleases(res.data.releases)); + setShowCalibrate(true); + }; + + // Render: platform icon | current → latest | Calibrate button + // If update_available: orange background + // If unconfigured: gray with "Not configured" + // If showCalibrate: dropdown showing last 10 releases to click +}; +``` + +--- + +## Priority 4: is_ignored Flag + +**The problem:** Vanilla, FoundryVTT, Hytale servers pollute the DB and show unconfigured widgets. + +**Note from Michael:** Nest ID filtering does NOT work — his eggs span multiple nests. + +**Solution:** +```sql +ALTER TABLE modpackchecker_servers ADD COLUMN is_ignored BOOLEAN DEFAULT FALSE; +``` + +- Widget shows "Hide (Not a Modpack)" button for unconfigured servers +- Clicking sets `is_ignored = true`, widget unmounts +- Cron skips servers where `is_ignored = true` +- Admin panel shows ignored servers in a separate list with "Restore" option + +--- + +## Priority 5: BCC Log Parsing (Optional Signal) + +**Research findings from live testing:** + +- `latest.log` IS readable via `DaemonFileRepository::getContent('logs/latest.log')` +- `BetterCompatibilityChecker` mod prints: `Loaded BetterCompatibilityChecker - Modpack: {name} | Version: {version}` +- Mythcraft 5 has BCC but it's unconfigured (`CHANGE_ME`) +- Most packs (ATM10 etc.) don't have BCC at all +- Log only has startup lines if server recently restarted + +**Recommended approach:** Add as optional detection step 4 in the cron (after modpack_installations, egg vars, file detection): +```php +// Step 4: BCC log parsing +private function detectFromLogs(Server $server): ?array +{ + try { + $log = $this->fileRepository->getContent('logs/latest.log'); + if (preg_match('/Loaded BetterCompatibilityChecker - Modpack: (.+?) \| Version: (.+)/', $log, $m)) { + if ($m[1] !== 'CHANGE_ME' && $m[2] !== 'CHANGE_ME') { + return ['name' => trim($m[1]), 'version' => trim($m[2])]; + } + } + } catch (\Exception $e) {} + return null; +} +``` + +Document in BuiltByBit: "Servers with BetterCompatibilityChecker configured will have the most accurate version detection. Version updates on server restart." + +--- + +## Also: Manifest Version Audit Results + +Ran Wings filesystem audit on all 22 servers for `manifest.json`: + +| Status | Servers | +|--------|---------| +| ✅ Has manifest with version | Mythcraft 5 (Update 5), Create Plus (0.9.0), Beyond Depth (Ver12.3.2), Beyond Ascension (Ver2.4.1), Homestead (1.2.9.4) | +| ⚠️ Manifest but no version field | Society, All of Create NC, Otherworld, Submerged 2 | +| ❌ No manifest | 13 servers (installed via modpack installer) | + +When `detectCurseForge()` finds a valid manifest, it should also extract `manifest['version']` as `installed_version` and use it as `current_version`. This is truth — no heuristic needed for these 5 servers. + +--- + +## Summary: What Needs to Be Built + +| Priority | Task | Complexity | +|----------|------|------------| +| 1 | File ID fields in DB + comparison logic | Medium | +| 2 | fetchFileHistory() + date-time seeding | Medium | +| 2b | manifest['version'] as current_version | Small | +| 3 | New status/releases/calibrate endpoints | Medium | +| 3b | Widget TSX redesign (zero-click + Recalibrate) | Large | +| 4 | is_ignored flag + Hide button | Small | +| 5 | BCC log parsing in cron | Small | + +Take them in order. File ID comparison first since it's foundational. + +*— Chronicler #84, The Meridian* +**Fire + Frost + Foundation = Where Love Builds Legacy** 💙🔥❄️ 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 81ab7e7..72939f5 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 @@ -58,18 +58,23 @@ class CheckModpackUpdates extends Command if ($existing && $existing->is_user_overridden) { if ($existing->platform && $existing->modpack_id) { - $this->checkVersion($server, $existing->platform, $existing->modpack_id, 'manual'); + $this->checkVersion($server, $existing->platform, $existing->modpack_id, 'manual', null); } return; } + // Skip ignored servers + if ($existing && $existing->is_ignored) { + return; + } + // Step 1: modpack_installations table (fastest, most reliable) $installation = DB::table('modpack_installations') ->where('server_id', $server->id) ->first(); if ($installation && !empty($installation->provider) && !empty($installation->modpack_id)) { - $this->checkVersion($server, $installation->provider, (string) $installation->modpack_id, 'installer'); + $this->checkVersion($server, $installation->provider, (string) $installation->modpack_id, 'installer', null); return; } @@ -88,14 +93,14 @@ class CheckModpackUpdates extends Command } if (!empty($platform) && !empty($modpackId)) { - $this->checkVersion($server, $platform, $modpackId, 'egg'); + $this->checkVersion($server, $platform, $modpackId, 'egg', null); return; } // Step 3: File-based detection via DaemonFileRepository (last resort) $detected = $this->detectFromFiles($server); if ($detected) { - $this->checkVersion($server, $detected['platform'], $detected['modpack_id'], 'file'); + $this->checkVersion($server, $detected['platform'], $detected['modpack_id'], 'file', $detected['installed_version'] ?? null); return; } @@ -152,6 +157,7 @@ class CheckModpackUpdates extends Command 'platform' => 'curseforge', 'modpack_id' => (string) $projectId, 'name' => $data['name'] ?? null, + 'installed_version' => $data['version'] ?? null, ]; } } @@ -163,6 +169,7 @@ class CheckModpackUpdates extends Command 'platform' => 'curseforge', 'modpack_id' => (string) $data['projectID'], 'name' => $data['name'] ?? null, + 'installed_version' => $data['version'] ?? null, ]; } } catch (\Exception $e) { @@ -217,28 +224,42 @@ class CheckModpackUpdates extends Command return null; } - private function checkVersion(Server $server, string $platform, string $modpackId, string $method): void + private function checkVersion(Server $server, string $platform, string $modpackId, string $method, ?string $installedVersion): void { try { $latestData = $this->apiService->fetchLatestVersion($platform, $modpackId); $latestVersion = $latestData['version'] ?? 'Unknown'; + $latestFileId = $latestData['file_id'] ?? null; - // Get current_version: egg variable > existing DB record > seed with latest - $currentVersion = $this->getVariable($server, 'MODPACK_CURRENT_VERSION'); + // Get current_version: manifest > egg variable > existing DB > seed with latest + $currentVersion = $installedVersion; + $currentFileId = null; + + if (empty($currentVersion)) { + $currentVersion = $this->getVariable($server, 'MODPACK_CURRENT_VERSION'); + } + + $existing = DB::table('modpackchecker_servers') + ->where('server_uuid', $server->uuid) + ->first(); if (empty($currentVersion)) { - $existing = DB::table('modpackchecker_servers') - ->where('server_uuid', $server->uuid) - ->first(); $currentVersion = $existing->current_version ?? null; } + $currentFileId = $existing->current_file_id ?? null; - // First time seeing this server — seed current_version with latest + // First time — seed with latest if (empty($currentVersion)) { $currentVersion = $latestVersion; + $currentFileId = $latestFileId; } - $updateAvailable = $currentVersion !== $latestVersion; + // Compare: prefer file ID, fall back to string + if ($currentFileId && $latestFileId) { + $updateAvailable = $latestFileId !== $currentFileId; + } else { + $updateAvailable = $currentVersion !== $latestVersion; + } $this->updateDatabase($server, [ 'platform' => $platform, @@ -246,6 +267,8 @@ class CheckModpackUpdates extends Command 'modpack_name' => $latestData['name'], 'current_version' => $currentVersion, 'latest_version' => $latestVersion, + 'current_file_id' => $currentFileId, + 'latest_file_id' => $latestFileId, 'status' => $updateAvailable ? 'update_available' : 'up_to_date', 'detection_method' => $method, 'error_message' => null, 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 45f9b96..f5b45b9 100644 --- a/services/modpack-version-checker/blueprint-extension/app/Services/ModpackApiService.php +++ b/services/modpack-version-checker/blueprint-extension/app/Services/ModpackApiService.php @@ -92,9 +92,12 @@ class ModpackApiService $versions = $versionResponse->json(); + $latestVer = $versions[0] ?? null; + return [ 'name' => $project['title'] ?? 'Unknown', - 'version' => !empty($versions) ? ($versions[0]['version_number'] ?? 'Unknown') : 'Unknown', + 'version' => $latestVer['version_number'] ?? 'Unknown', + 'file_id' => $latestVer ? ($latestVer['id'] ?? null) : null, ]; } @@ -128,10 +131,12 @@ class ModpackApiService } $data = $response->json()['data'] ?? []; + $latestFile = $data['latestFiles'][0] ?? null; return [ 'name' => $data['name'] ?? 'Unknown', - 'version' => !empty($data['latestFiles']) ? ($data['latestFiles'][0]['displayName'] ?? 'Unknown') : 'Unknown', + 'version' => $latestFile['displayName'] ?? 'Unknown', + 'file_id' => $latestFile ? (string) $latestFile['id'] : null, ]; } @@ -154,9 +159,12 @@ class ModpackApiService $data = $response->json(); + $latestVer = $data['versions'][0] ?? null; + return [ 'name' => $data['name'] ?? 'Unknown', - 'version' => !empty($data['versions']) ? ($data['versions'][0]['name'] ?? 'Unknown') : 'Unknown', + 'version' => $latestVer['name'] ?? 'Unknown', + 'file_id' => $latestVer ? (string) ($latestVer['id'] ?? null) : null, ]; } @@ -197,6 +205,7 @@ class ModpackApiService return [ 'name' => $data['displayName'] ?? $data['name'] ?? 'Unknown', 'version' => $data['version'] ?? 'Unknown', + 'file_id' => $data['version'] ?? null, ]; } } diff --git a/services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_13_000000_add_file_id_and_ignored.php b/services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_13_000000_add_file_id_and_ignored.php new file mode 100644 index 0000000..7112ef8 --- /dev/null +++ b/services/modpack-version-checker/blueprint-extension/database/migrations/2026_04_13_000000_add_file_id_and_ignored.php @@ -0,0 +1,24 @@ +string('current_file_id', 64)->nullable()->after('latest_version'); + $table->string('latest_file_id', 64)->nullable()->after('current_file_id'); + $table->boolean('is_ignored')->default(false)->after('is_user_overridden'); + }); + } + + public function down(): void + { + Schema::table('modpackchecker_servers', function (Blueprint $table) { + $table->dropColumn(['current_file_id', 'latest_file_id', 'is_ignored']); + }); + } +};