v1.1.0 Priorities 2-5: date seeding, new endpoints, BCC detection
Priority 2 — Date-time seeding:
- fetchFileHistory() for CurseForge, Modrinth, FTB
- seedCurrentVersion() matches release closest to server install date
- Falls back to latest if no history or no install date
Priority 3 — New endpoints:
- GET /servers/{server}/status — zero-click cached status
- GET /servers/{server}/releases — recalibrate dropdown (10 releases)
- POST /servers/{server}/calibrate — save user's version selection
- POST /servers/{server}/ignore — toggle is_ignored flag
Priority 5 — BCC log parsing:
- detectFromBccLog() reads logs/latest.log for BetterCompatibilityChecker
- Extracts modpack name + version from BCC output line
- Skips CHANGE_ME values
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9991240eab
commit
b84958c0ee
15
docs/code-bridge/archive/MSG-2026-04-13-keep-going-p2.md
Normal file
15
docs/code-bridge/archive/MSG-2026-04-13-keep-going-p2.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Chronicler Dispatch — Keep Going with Priority 2
|
||||
|
||||
**Date:** April 13, 2026
|
||||
**From:** Chronicler #84 — The Meridian
|
||||
**To:** Code
|
||||
|
||||
---
|
||||
|
||||
Keep going. Deploy and test can wait — consolidate when more pieces are ready.
|
||||
|
||||
Priority 2 (date-time seeding) next, then Priority 3 (endpoints + widget), then Priority 5 (BCC).
|
||||
|
||||
One consolidated deploy when they're all done.
|
||||
|
||||
*— Chronicler #84*
|
||||
@@ -134,6 +134,10 @@ class CheckModpackUpdates extends Command
|
||||
$ftb = $this->detectFtb($server);
|
||||
if ($ftb) return $ftb;
|
||||
|
||||
// BCC: logs/latest.log
|
||||
$bcc = $this->detectFromBccLog($server);
|
||||
if ($bcc) return $bcc;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -248,10 +252,11 @@ class CheckModpackUpdates extends Command
|
||||
}
|
||||
$currentFileId = $existing->current_file_id ?? null;
|
||||
|
||||
// First time — seed with latest
|
||||
// First time — try date-time seeding, fall back to latest
|
||||
if (empty($currentVersion)) {
|
||||
$currentVersion = $latestVersion;
|
||||
$currentFileId = $latestFileId;
|
||||
$seeded = $this->seedCurrentVersion($server, $platform, $modpackId, $latestVersion, $latestFileId);
|
||||
$currentVersion = $seeded['version'];
|
||||
$currentFileId = $seeded['file_id'];
|
||||
}
|
||||
|
||||
// Compare: prefer file ID, fall back to string
|
||||
@@ -291,6 +296,69 @@ class CheckModpackUpdates extends Command
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BCC log parsing — detect modpack from BetterCompatibilityChecker output.
|
||||
*/
|
||||
private function detectFromBccLog(Server $server): ?array
|
||||
{
|
||||
try {
|
||||
$log = $this->fileRepository->getContent('logs/latest.log');
|
||||
if (preg_match('/Loaded BetterCompatibilityChecker - Modpack: (.+?) \| Version: (.+)/', $log, $m)) {
|
||||
$name = trim($m[1]);
|
||||
$version = trim($m[2]);
|
||||
if ($name !== 'CHANGE_ME' && $version !== 'CHANGE_ME') {
|
||||
$this->info(" Detected via BCC log: {$name} v{$version}");
|
||||
return [
|
||||
'platform' => 'unknown',
|
||||
'modpack_id' => strtolower(str_replace(' ', '-', $name)),
|
||||
'name' => $name,
|
||||
'installed_version' => $version,
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->line(" [debug] BCC log: " . $e->getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed current version using date-time heuristic.
|
||||
* Finds the release closest to (but not after) the server's created_at date.
|
||||
*/
|
||||
private function seedCurrentVersion(Server $server, string $platform, string $modpackId, string $fallbackVersion, ?string $fallbackFileId): array
|
||||
{
|
||||
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,
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->line(" [seed] Heuristic failed: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return ['version' => $fallbackVersion, 'file_id' => $fallbackFileId];
|
||||
}
|
||||
|
||||
private function getVariable(Server $server, string $name): ?string
|
||||
{
|
||||
$variable = $server->variables()
|
||||
|
||||
@@ -287,4 +287,144 @@ class ModpackAPIController extends Controller
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached status for a single server (zero-click widget).
|
||||
* Reads from DB only — no external API calls.
|
||||
*/
|
||||
public function serverStatus(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$cached = DB::table('modpackchecker_servers')
|
||||
->where('server_uuid', $server->uuid)
|
||||
->first();
|
||||
|
||||
if (!$cached || $cached->is_ignored ?? false) {
|
||||
return response()->json([
|
||||
'configured' => false,
|
||||
'is_ignored' => $cached->is_ignored ?? false,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'configured' => !empty($cached->platform) && $cached->status !== 'unconfigured',
|
||||
'platform' => $cached->platform,
|
||||
'modpack_name' => $cached->modpack_name,
|
||||
'current_version' => $cached->current_version,
|
||||
'latest_version' => $cached->latest_version,
|
||||
'current_file_id' => $cached->current_file_id ?? null,
|
||||
'latest_file_id' => $cached->latest_file_id ?? null,
|
||||
'update_available' => $cached->status === 'update_available',
|
||||
'last_checked' => $cached->last_checked,
|
||||
'detection_method' => $cached->detection_method ?? 'unknown',
|
||||
'is_ignored' => $cached->is_ignored ?? false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get release history for recalibrate dropdown.
|
||||
* Makes external API call — rate limited.
|
||||
*/
|
||||
public function releases(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$limitKey = 'modpack_releases_' . $server->uuid;
|
||||
if (RateLimiter::tooManyAttempts($limitKey, 2)) {
|
||||
$seconds = RateLimiter::availableIn($limitKey);
|
||||
return response()->json(['error' => "Too many requests. Wait {$seconds}s."], 429);
|
||||
}
|
||||
RateLimiter::hit($limitKey, 60);
|
||||
|
||||
$cached = DB::table('modpackchecker_servers')
|
||||
->where('server_uuid', $server->uuid)
|
||||
->first();
|
||||
|
||||
if (!$cached || empty($cached->platform) || empty($cached->modpack_id)) {
|
||||
return response()->json(['releases' => []]);
|
||||
}
|
||||
|
||||
$releases = $this->apiService->fetchFileHistory($cached->platform, $cached->modpack_id, 10);
|
||||
|
||||
return response()->json(['releases' => $releases]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalibrate current version (user selects from release list).
|
||||
* Sets is_user_overridden = true.
|
||||
*/
|
||||
public function calibrate(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$fileId = $request->input('file_id');
|
||||
$version = $request->input('version');
|
||||
|
||||
if (empty($version)) {
|
||||
return response()->json(['error' => 'version is required'], 400);
|
||||
}
|
||||
|
||||
$updated = DB::table('modpackchecker_servers')
|
||||
->where('server_uuid', $server->uuid)
|
||||
->update([
|
||||
'current_version' => $version,
|
||||
'current_file_id' => $fileId,
|
||||
'is_user_overridden' => true,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
if (!$updated) {
|
||||
return response()->json(['error' => 'Server not found in modpack cache'], 404);
|
||||
}
|
||||
|
||||
// Re-evaluate update status
|
||||
$cached = DB::table('modpackchecker_servers')
|
||||
->where('server_uuid', $server->uuid)
|
||||
->first();
|
||||
|
||||
$updateAvailable = false;
|
||||
if ($cached->latest_file_id && $fileId) {
|
||||
$updateAvailable = $cached->latest_file_id !== $fileId;
|
||||
} else {
|
||||
$updateAvailable = $cached->latest_version !== $version;
|
||||
}
|
||||
|
||||
DB::table('modpackchecker_servers')
|
||||
->where('server_uuid', $server->uuid)
|
||||
->update([
|
||||
'status' => $updateAvailable ? 'update_available' : 'up_to_date',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'current_version' => $version,
|
||||
'update_available' => $updateAvailable,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle is_ignored flag for a server.
|
||||
*/
|
||||
public function toggleIgnore(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$existing = DB::table('modpackchecker_servers')
|
||||
->where('server_uuid', $server->uuid)
|
||||
->first();
|
||||
|
||||
if (!$existing) {
|
||||
// Create a record to track the ignore
|
||||
DB::table('modpackchecker_servers')->insert([
|
||||
'server_uuid' => $server->uuid,
|
||||
'is_ignored' => true,
|
||||
'status' => 'ignored',
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
return response()->json(['is_ignored' => true]);
|
||||
}
|
||||
|
||||
$newState = !($existing->is_ignored ?? false);
|
||||
DB::table('modpackchecker_servers')
|
||||
->where('server_uuid', $server->uuid)
|
||||
->update([
|
||||
'is_ignored' => $newState,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json(['is_ignored' => $newState]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,81 @@ class ModpackApiService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch file/version history for date-time seeding.
|
||||
* Returns array of ['file_id', 'version', 'display_name', 'release_date'].
|
||||
*/
|
||||
public function fetchFileHistory(string $platform, string $modpackId, int $limit = 20): array
|
||||
{
|
||||
return match($platform) {
|
||||
'curseforge' => $this->fetchCurseForgeHistory($modpackId, $limit),
|
||||
'modrinth' => $this->fetchModrinthHistory($modpackId, $limit),
|
||||
'ftb' => $this->fetchFtbHistory($modpackId, $limit),
|
||||
default => []
|
||||
};
|
||||
}
|
||||
|
||||
private function fetchCurseForgeHistory(string $modpackId, int $limit): array
|
||||
{
|
||||
$apiKey = trim($this->blueprint->dbGet('modpackchecker', 'curseforge_api_key') ?? '');
|
||||
if (empty($apiKey)) return [];
|
||||
|
||||
$response = Http::timeout(15)->withHeaders([
|
||||
'x-api-key' => $apiKey,
|
||||
'Accept' => 'application/json',
|
||||
])->get("https://api.curseforge.com/v1/mods/{$modpackId}/files", [
|
||||
'pageSize' => $limit,
|
||||
]);
|
||||
|
||||
if (!$response->successful()) return [];
|
||||
|
||||
return collect($response->json()['data'] ?? [])
|
||||
->map(fn($f) => [
|
||||
'file_id' => (string) $f['id'],
|
||||
'version' => $f['displayName'] ?? 'Unknown',
|
||||
'display_name' => $f['displayName'] ?? 'Unknown',
|
||||
'release_date' => $f['fileDate'] ?? null,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function fetchModrinthHistory(string $projectId, int $limit): array
|
||||
{
|
||||
$response = Http::timeout(15)
|
||||
->withHeaders(['User-Agent' => 'FirefrostGaming/ModpackChecker/1.0'])
|
||||
->get("https://api.modrinth.com/v2/project/{$projectId}/version");
|
||||
|
||||
if (!$response->successful()) return [];
|
||||
|
||||
return collect($response->json() ?? [])
|
||||
->take($limit)
|
||||
->map(fn($v) => [
|
||||
'file_id' => $v['id'] ?? null,
|
||||
'version' => $v['version_number'] ?? 'Unknown',
|
||||
'display_name' => $v['name'] ?? $v['version_number'] ?? 'Unknown',
|
||||
'release_date' => $v['date_published'] ?? null,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function fetchFtbHistory(string $modpackId, int $limit): array
|
||||
{
|
||||
$response = Http::timeout(15)
|
||||
->get("https://api.modpacks.ch/public/modpack/{$modpackId}");
|
||||
|
||||
if (!$response->successful()) return [];
|
||||
|
||||
return collect($response->json()['versions'] ?? [])
|
||||
->take($limit)
|
||||
->map(fn($v) => [
|
||||
'file_id' => (string) ($v['id'] ?? ''),
|
||||
'version' => $v['name'] ?? 'Unknown',
|
||||
'display_name' => $v['name'] ?? 'Unknown',
|
||||
'release_date' => isset($v['updated']) ? date('c', $v['updated']) : null,
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Query Technic Platform API for latest modpack version.
|
||||
*
|
||||
|
||||
@@ -13,8 +13,20 @@ use Pterodactyl\Http\Controllers\ModpackAPIController;
|
||||
|
|
||||
*/
|
||||
|
||||
// Resulting URL: /api/client/extensions/modpackchecker/servers/{server}/check
|
||||
// Manual check (POST — makes live API call)
|
||||
Route::post('/servers/{server}/check', [ModpackAPIController::class, 'manualCheck']);
|
||||
|
||||
// Resulting URL: /api/client/extensions/modpackchecker/status
|
||||
// Dashboard badge status (GET — reads from DB cache only)
|
||||
Route::get('/status', [ModpackAPIController::class, 'getStatus']);
|
||||
|
||||
// Server status from cache (GET — no external API calls)
|
||||
Route::get('/servers/{server}/status', [ModpackAPIController::class, 'serverStatus']);
|
||||
|
||||
// Release history for recalibrate dropdown (GET — makes external API call)
|
||||
Route::get('/servers/{server}/releases', [ModpackAPIController::class, 'releases']);
|
||||
|
||||
// Recalibrate current version (POST — saves user selection)
|
||||
Route::post('/servers/{server}/calibrate', [ModpackAPIController::class, 'calibrate']);
|
||||
|
||||
// Ignore/restore server (POST — toggles is_ignored flag)
|
||||
Route::post('/servers/{server}/ignore', [ModpackAPIController::class, 'toggleIgnore']);
|
||||
|
||||
Reference in New Issue
Block a user