Added professional-grade documentation throughout the codebase so any developer can pick up this project and understand it immediately. PHILOSOPHY: 'Hand someone the repo and say: here's what we built, here's WHY we built it this way, here's where it's going. Make it better.' — Michael NEW FILES: - blueprint-extension/README.md - Complete developer onboarding guide (400+ lines) - Architecture diagram showing cron → cache → badge flow - Installation steps, configuration, usage - API reference with example responses - Troubleshooting guide - Design decisions with rationale ENHANCED DOCUMENTATION: ModpackAPIController.php: - 60-line file header explaining purpose, architecture, critical decisions - Detailed docblocks on every method - Explains WHY dashboard reads cache-only (rate limits) - Documents all four platform APIs with links - Example request/response for each endpoint CheckModpackUpdates.php: - 50-line file header with usage examples - Recommended cron schedule - Example console output - Documents rate limiting strategy - Explains relationship to dashboard badges UpdateBadge.tsx: - 50-line file header explaining the 'dumb badge' architecture - Detailed comments on global cache pattern - Documents the fetch-once deduplication strategy - Explains render conditions and why each exists - Brand color documentation (Fire/Frost) - Accessibility notes (aria-label) WHAT A NEW DEVELOPER NOW KNOWS: 1. The 'why' behind every architectural decision 2. How the cron → cache → badge flow prevents rate limits 3. Which methods call external APIs vs read cache 4. How to add a new platform 5. How to troubleshoot common issues 6. The relationship between all components This codebase is now ready to hand to a contractor with the words: 'This was made great. Make it awesome.' Signed-off-by: Claude (Chronicler #63) <claude@firefrostgaming.com>
330 lines
12 KiB
PHP
330 lines
12 KiB
PHP
<?php
|
|
|
|
/**
|
|
* =============================================================================
|
|
* MODPACK VERSION CHECKER - CRON COMMAND
|
|
* =============================================================================
|
|
*
|
|
* Laravel Artisan command that checks all servers for modpack updates.
|
|
* This is the "brain" that populates the cache used by the dashboard badges.
|
|
*
|
|
* USAGE:
|
|
* php artisan modpackchecker:check
|
|
*
|
|
* RECOMMENDED CRON SCHEDULE:
|
|
* # Check for updates every 6 hours (adjust based on your server count)
|
|
* 0 */6 * * * cd /var/www/pterodactyl && php artisan modpackchecker:check >> /dev/null 2>&1
|
|
*
|
|
* HOW IT WORKS:
|
|
* 1. Finds all servers with MODPACK_PLATFORM egg variable set
|
|
* 2. Loops through each server, checking the appropriate API
|
|
* 3. Stores results in modpackchecker_servers database table
|
|
* 4. Dashboard badges read from this table (never calling APIs directly)
|
|
*
|
|
* RATE LIMITING:
|
|
* Each API call is followed by a 2-second sleep to avoid rate limits.
|
|
* For 50 servers, a full check takes ~2 minutes. Adjust sleep() if needed.
|
|
*
|
|
* PLATFORM SUPPORT:
|
|
* - CurseForge: Requires API key in settings (configured via admin panel)
|
|
* - Modrinth: Public API, no key needed (uses User-Agent)
|
|
* - FTB: Public API via modpacks.ch
|
|
* - Technic: Public API, uses slug instead of numeric ID
|
|
*
|
|
* DATABASE TABLE: modpackchecker_servers
|
|
* This table caches the latest check results. See migration file for schema.
|
|
* Uses updateOrInsert for upsert behavior - first run creates, subsequent update.
|
|
*
|
|
* EXAMPLE OUTPUT:
|
|
* Starting modpack update check...
|
|
* Found 12 servers with modpack configuration
|
|
* Checking: ATM9 Server (a1b2c3d4-...)
|
|
* 🟠 UPDATE AVAILABLE: All The Mods 9 - 0.2.60
|
|
* Checking: Creative Server (e5f6g7h8-...)
|
|
* Skipping - missing platform or modpack ID
|
|
* Checking: Vanilla Server (i9j0k1l2-...)
|
|
* 🟢 Up to date: Adrenaserver - 1.7.0
|
|
* Modpack update check complete!
|
|
*
|
|
* ERROR HANDLING:
|
|
* If an API call fails, the error is logged to the database and the command
|
|
* continues to the next server. One failure doesn't stop the whole batch.
|
|
*
|
|
* DEPENDENCIES:
|
|
* - Database table: modpackchecker_servers (from migration)
|
|
* - Settings: modpackchecker::curseforge_api_key (for CurseForge only)
|
|
*
|
|
* @package Pterodactyl\Console\Commands
|
|
* @author Firefrost Gaming (Chroniclers #62, #63)
|
|
* @version 1.0.0
|
|
* @see ModpackAPIController.php (provides getStatus endpoint for badges)
|
|
* @see UpdateBadge.tsx (React component that displays the results)
|
|
* =============================================================================
|
|
*/
|
|
|
|
namespace Pterodactyl\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Pterodactyl\Models\Server;
|
|
|
|
/**
|
|
* Artisan command to check all servers for modpack updates.
|
|
*
|
|
* Run manually with: php artisan modpackchecker:check
|
|
* Or schedule in app/Console/Kernel.php:
|
|
* $schedule->command('modpackchecker:check')->everyFourHours();
|
|
*/
|
|
class CheckModpackUpdates extends Command
|
|
{
|
|
/**
|
|
* The console command signature.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $signature = 'modpackchecker:check';
|
|
|
|
/**
|
|
* The console command description (shown in `php artisan list`).
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $description = 'Check all servers for modpack updates';
|
|
|
|
/**
|
|
* Execute the console command.
|
|
*
|
|
* Main entry point. Finds all servers with modpack configuration
|
|
* and checks each one against its platform's API.
|
|
*
|
|
* @return int Exit code (0 = success)
|
|
*/
|
|
public function handle(): int
|
|
{
|
|
$this->info('Starting modpack update check...');
|
|
|
|
// Get all servers that have modpack variables set
|
|
$servers = Server::whereHas('variables', function ($q) {
|
|
$q->where('env_variable', 'MODPACK_PLATFORM');
|
|
})->get();
|
|
|
|
$this->info("Found {$servers->count()} servers with modpack configuration");
|
|
|
|
foreach ($servers as $server) {
|
|
$this->checkServer($server);
|
|
// Rate limiting - sleep between checks
|
|
sleep(2);
|
|
}
|
|
|
|
$this->info('Modpack update check complete!');
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Check a single server for modpack updates.
|
|
*
|
|
* This method:
|
|
* 1. Retrieves modpack configuration from egg variables
|
|
* 2. Calls the appropriate platform API
|
|
* 3. Compares current vs latest version
|
|
* 4. Stores results in database cache
|
|
*
|
|
* If any step fails, the error is logged to the database
|
|
* and the method returns (doesn't throw).
|
|
*
|
|
* @param Server $server The server to check
|
|
* @return void
|
|
*/
|
|
private function checkServer(Server $server): void
|
|
{
|
|
$this->line("Checking: {$server->name} ({$server->uuid})");
|
|
|
|
try {
|
|
// Get platform and modpack ID from variables
|
|
$platform = $this->getVariable($server, 'MODPACK_PLATFORM');
|
|
$modpackId = $this->getVariable($server, 'MODPACK_ID');
|
|
|
|
if (!$platform || !$modpackId) {
|
|
$this->warn(" Skipping - missing platform or modpack ID");
|
|
return;
|
|
}
|
|
|
|
// Get latest version from API
|
|
$latestData = $this->fetchLatestVersion($platform, $modpackId);
|
|
|
|
if (!$latestData) {
|
|
$this->error(" Failed to fetch version data");
|
|
$this->updateDatabase($server, [
|
|
'platform' => $platform,
|
|
'modpack_id' => $modpackId,
|
|
'error_message' => 'Failed to fetch version data',
|
|
'update_available' => false,
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// Get current version (from variable or file - for now just use variable)
|
|
$currentVersion = $this->getVariable($server, 'MODPACK_CURRENT_VERSION');
|
|
|
|
// Determine if update is available
|
|
$updateAvailable = $currentVersion && $currentVersion !== $latestData['version'];
|
|
|
|
$this->updateDatabase($server, [
|
|
'platform' => $platform,
|
|
'modpack_id' => $modpackId,
|
|
'modpack_name' => $latestData['name'],
|
|
'current_version' => $currentVersion,
|
|
'latest_version' => $latestData['version'],
|
|
'update_available' => $updateAvailable,
|
|
'error_message' => null,
|
|
]);
|
|
|
|
$status = $updateAvailable ? '🟠 UPDATE AVAILABLE' : '🟢 Up to date';
|
|
$this->info(" {$status}: {$latestData['name']} - {$latestData['version']}");
|
|
|
|
} catch (\Exception $e) {
|
|
$this->error(" Error: {$e->getMessage()}");
|
|
$this->updateDatabase($server, [
|
|
'error_message' => $e->getMessage(),
|
|
'update_available' => false,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get an egg variable value from a server.
|
|
*
|
|
* Egg variables are the startup parameters configured in the server's
|
|
* egg. For modpack checking, we look for:
|
|
* - MODPACK_PLATFORM: Which API to query
|
|
* - MODPACK_ID: The modpack identifier for that platform
|
|
* - MODPACK_CURRENT_VERSION: What's currently installed (optional)
|
|
*
|
|
* @param Server $server The server to query
|
|
* @param string $name The variable name (e.g., "MODPACK_PLATFORM")
|
|
* @return string|null The variable value, or null if not set
|
|
*/
|
|
private function getVariable(Server $server, string $name): ?string
|
|
{
|
|
$variable = $server->variables()
|
|
->where('env_variable', $name)
|
|
->first();
|
|
return $variable?->server_value;
|
|
}
|
|
|
|
/**
|
|
* Route to the appropriate platform API based on platform name.
|
|
*
|
|
* This is just a dispatcher - actual API logic is in the check* methods.
|
|
*
|
|
* @param string $platform The platform name (modrinth, curseforge, ftb, technic)
|
|
* @param string $modpackId The platform-specific modpack identifier
|
|
* @return array|null Contains [name, version] or null on failure
|
|
*/
|
|
private function fetchLatestVersion(string $platform, string $modpackId): ?array
|
|
{
|
|
return match($platform) {
|
|
'modrinth' => $this->checkModrinth($modpackId),
|
|
'curseforge' => $this->checkCurseForge($modpackId),
|
|
'ftb' => $this->checkFTB($modpackId),
|
|
'technic' => $this->checkTechnic($modpackId),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function checkModrinth(string $projectId): ?array
|
|
{
|
|
$response = Http::withHeaders([
|
|
'User-Agent' => 'FirefrostGaming/ModpackChecker/1.0',
|
|
])->get("https://api.modrinth.com/v2/project/{$projectId}");
|
|
|
|
if (!$response->successful()) return null;
|
|
$project = $response->json();
|
|
|
|
$versionResponse = Http::withHeaders([
|
|
'User-Agent' => 'FirefrostGaming/ModpackChecker/1.0',
|
|
])->get("https://api.modrinth.com/v2/project/{$projectId}/version");
|
|
|
|
$versions = $versionResponse->json();
|
|
|
|
return [
|
|
'name' => $project['title'] ?? 'Unknown',
|
|
'version' => $versions[0]['version_number'] ?? 'Unknown',
|
|
];
|
|
}
|
|
|
|
private function checkCurseForge(string $modpackId): ?array
|
|
{
|
|
$apiKey = DB::table('settings')
|
|
->where('key', 'like', '%modpackchecker::curseforge_api_key%')
|
|
->value('value');
|
|
|
|
if (!$apiKey) return null;
|
|
|
|
$response = Http::withHeaders([
|
|
'x-api-key' => $apiKey,
|
|
])->get("https://api.curseforge.com/v1/mods/{$modpackId}");
|
|
|
|
if (!$response->successful()) return null;
|
|
$data = $response->json()['data'] ?? [];
|
|
|
|
return [
|
|
'name' => $data['name'] ?? 'Unknown',
|
|
'version' => $data['latestFiles'][0]['displayName'] ?? 'Unknown',
|
|
];
|
|
}
|
|
|
|
private function checkFTB(string $modpackId): ?array
|
|
{
|
|
$response = Http::get("https://api.modpacks.ch/public/modpack/{$modpackId}");
|
|
if (!$response->successful()) return null;
|
|
$data = $response->json();
|
|
|
|
return [
|
|
'name' => $data['name'] ?? 'Unknown',
|
|
'version' => $data['versions'][0]['name'] ?? 'Unknown',
|
|
];
|
|
}
|
|
|
|
private function checkTechnic(string $slug): ?array
|
|
{
|
|
$response = Http::get("https://api.technicpack.net/modpack/{$slug}?build=1");
|
|
if (!$response->successful()) return null;
|
|
$data = $response->json();
|
|
|
|
return [
|
|
'name' => $data['displayName'] ?? $data['name'] ?? 'Unknown',
|
|
'version' => $data['version'] ?? 'Unknown',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Store or update the modpack check results in the database.
|
|
*
|
|
* Uses updateOrInsert for upsert behavior:
|
|
* - First check: Creates a new row
|
|
* - Subsequent checks: Updates the existing row
|
|
*
|
|
* The server_id column is the unique key for matching.
|
|
*
|
|
* This cache table is read by ModpackAPIController::getStatus()
|
|
* which powers the dashboard badges.
|
|
*
|
|
* @param Server $server The server being checked
|
|
* @param array $data The data to store (platform, version info, errors, etc.)
|
|
* @return void
|
|
*/
|
|
private function updateDatabase(Server $server, array $data): void
|
|
{
|
|
DB::table('modpackchecker_servers')->updateOrInsert(
|
|
['server_id' => $server->id],
|
|
array_merge($data, [
|
|
'server_uuid' => $server->uuid,
|
|
'last_checked' => now(),
|
|
'updated_at' => now(),
|
|
])
|
|
);
|
|
}
|
|
}
|