docs(modpackchecker): Comprehensive developer documentation

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>
This commit is contained in:
Claude (Chronicler #63)
2026-04-06 09:05:48 +00:00
parent 0cbea6d993
commit e36b20d06e
4 changed files with 993 additions and 35 deletions

View File

@@ -1,5 +1,62 @@
<?php
/**
* =============================================================================
* MODPACK VERSION CHECKER - API CONTROLLER
* =============================================================================
*
* Part of the ModpackChecker Blueprint extension for Pterodactyl Panel.
*
* PURPOSE:
* Provides API endpoints for checking modpack versions across multiple platforms
* (CurseForge, Modrinth, FTB, Technic). Supports both on-demand manual checks
* from the server console and cached status retrieval for the dashboard badge.
*
* ARCHITECTURE OVERVIEW:
* This controller serves two distinct use cases:
*
* 1. MANUAL CHECK (manualCheck method)
* - Called from: Server console "Check for Updates" button
* - Behavior: Makes LIVE API calls to modpack platforms
* - Use case: User wants current info for a specific server
* - Rate limit consideration: One server at a time, user-initiated
*
* 2. DASHBOARD STATUS (getStatus method)
* - Called from: Dashboard badge component (UpdateBadge.tsx)
* - Behavior: Reads from LOCAL DATABASE CACHE only - NO external API calls
* - Use case: Show update indicators for all servers on dashboard
* - Why cached? Dashboard loads could mean 10+ servers × multiple users =
* rate limit hell. Cron job populates cache instead.
*
* CRITICAL DESIGN DECISION (Gemini-approved):
* The dashboard badge MUST be "dumb" - it only reads cached data. If we let
* the dashboard trigger live API calls, we'd hit rate limits within minutes
* on any panel with more than a handful of servers. The CheckModpackUpdates
* cron command handles the external API calls on a schedule with rate limiting.
*
* PLATFORM SUPPORT:
* - CurseForge: Requires API key (configured in admin panel)
* - Modrinth: No key required, uses User-Agent identification
* - FTB (Feed The Beast): Public API via modpacks.ch
* - Technic: Public API, uses slug instead of numeric ID
*
* DEPENDENCIES:
* - Blueprint Framework (BlueprintAdminLibrary for settings storage)
* - Pterodactyl's DaemonFileRepository (for file-based modpack detection)
* - Database table: modpackchecker_servers (created by migration)
*
* ROUTES (defined in routes/client.php):
* - POST /api/client/servers/{server}/ext/modpackchecker/check -> manualCheck()
* - GET /api/client/extensions/modpackchecker/status -> getStatus()
*
* @package Pterodactyl\BlueprintFramework\Extensions\modpackchecker
* @author Firefrost Gaming (Chroniclers #52, #62, #63)
* @version 1.0.0
* @see CheckModpackUpdates.php (cron command that populates the cache)
* @see UpdateBadge.tsx (React component that consumes getStatus)
* =============================================================================
*/
namespace Pterodactyl\BlueprintFramework\Extensions\modpackchecker\Controllers;
use Pterodactyl\Http\Controllers\Controller;
@@ -11,6 +68,13 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\DB;
/**
* Handles all modpack version checking API requests.
*
* Two public endpoints:
* - manualCheck(): Live check for single server (console button)
* - getStatus(): Cached status for all servers (dashboard badge)
*/
class ModpackAPIController extends Controller
{
public function __construct(
@@ -19,7 +83,41 @@ class ModpackAPIController extends Controller
) {}
/**
* Manual version check triggered from React frontend
* Manual version check triggered from the server console UI.
*
* This endpoint makes LIVE API calls to external modpack platforms.
* It's designed for on-demand, user-initiated checks of a single server.
*
* DETECTION PRIORITY:
* 1. Egg variables (MODPACK_PLATFORM + MODPACK_ID) - most reliable
* 2. Platform-specific variables (CURSEFORGE_ID, MODRINTH_PROJECT_ID, etc.)
* 3. File fingerprinting (manifest.json, modrinth.index.json)
*
* WHY THIS ORDER?
* Egg variables are explicitly set by the server owner, so they're most
* trustworthy. File detection is a fallback for servers that were set up
* before this extension was installed.
*
* @param Request $request The incoming HTTP request (includes auth)
* @param Server $server The server to check (injected by route model binding)
* @return JsonResponse Contains: success, platform, modpack_id, modpack_name,
* latest_version, status, and error (if applicable)
*
* @example Success response:
* {
* "success": true,
* "platform": "modrinth",
* "modpack_id": "adrenaserver",
* "modpack_name": "Adrenaserver",
* "latest_version": "1.7.0+1.21.1.fabric",
* "status": "checked"
* }
*
* @example Error response (no modpack detected):
* {
* "success": false,
* "message": "Could not detect modpack. Set MODPACK_PLATFORM and MODPACK_ID in startup variables."
* }
*/
public function manualCheck(Request $request, Server $server): JsonResponse
{
@@ -82,7 +180,17 @@ class ModpackAPIController extends Controller
}
/**
* Get an egg variable value for a server
* Retrieve an egg variable value for a specific server.
*
* Egg variables are the startup parameters defined in Pterodactyl eggs.
* For modpack detection, we look for variables like:
* - MODPACK_PLATFORM: "curseforge", "modrinth", "ftb", "technic"
* - MODPACK_ID: The platform-specific identifier
* - MODPACK_CURRENT_VERSION: What version is currently installed
*
* @param Server $server The server to query
* @param string $name The environment variable name (e.g., "MODPACK_PLATFORM")
* @return string|null The variable's value, or null if not set
*/
private function getEggVariable(Server $server, string $name): ?string
{
@@ -93,7 +201,25 @@ class ModpackAPIController extends Controller
}
/**
* Attempt to detect modpack from files
* Attempt to detect modpack platform and ID by reading server files.
*
* This is a FALLBACK method when egg variables aren't set. It works by
* looking for platform-specific manifest files that modpack installers create:
*
* - CurseForge: manifest.json with manifestType="minecraftModpack"
* - Modrinth: modrinth.index.json with formatVersion field
*
* LIMITATIONS:
* - Requires the server to be running (Wings must be accessible)
* - Some modpack installations don't include these files
* - Modrinth index doesn't always contain the project ID
*
* WHY NOT FTB/TECHNIC?
* FTB and Technic launchers don't leave standardized manifest files.
* Servers using these platforms MUST set egg variables manually.
*
* @param Server $server The server to scan for manifest files
* @return array Contains: platform, modpack_id, name, version (all nullable)
*/
private function detectFromFiles(Server $server): array
{
@@ -133,7 +259,20 @@ class ModpackAPIController extends Controller
}
/**
* Read a file from the server via Wings
* Read a file from the game server via the Wings daemon.
*
* Uses Pterodactyl's DaemonFileRepository to communicate with Wings,
* which runs on the game server node. This allows us to read files
* from the server's filesystem without direct SSH access.
*
* FAILURE MODES:
* - Server offline: Wings can't access stopped containers
* - File doesn't exist: Returns null (caught exception)
* - Wings unreachable: Network/auth issues return null
*
* @param Server $server The server whose files we're reading
* @param string $path Relative path from server root (e.g., "manifest.json")
* @return string|null File contents, or null if unreadable
*/
private function readServerFile(Server $server, string $path): ?string
{
@@ -146,7 +285,22 @@ class ModpackAPIController extends Controller
}
/**
* Check CurseForge API for latest version
* Query CurseForge API for latest modpack version.
*
* REQUIRES: API key configured in admin panel.
* CurseForge's API is not public - you must apply for access:
* https://docs.curseforge.com/#getting-started
*
* API ENDPOINT: GET https://api.curseforge.com/v1/mods/{modId}
*
* RATE LIMITS: CurseForge allows ~1000 requests/day for personal keys.
* The cron job handles rate limiting via sleep() between checks.
*
* @param string $modpackId CurseForge project ID (numeric string)
* @return array Contains: name, version
* @throws \Exception If API key missing or request fails
*
* @see https://docs.curseforge.com/#get-mod
*/
private function checkCurseForge(string $modpackId): array
{
@@ -175,7 +329,25 @@ class ModpackAPIController extends Controller
}
/**
* Check Modrinth API for latest version
* Query Modrinth API for latest modpack version.
*
* NO API KEY REQUIRED - Modrinth's API is public.
* However, they require a User-Agent header for identification.
*
* API ENDPOINTS:
* - GET https://api.modrinth.com/v2/project/{id}/version (get versions)
* - GET https://api.modrinth.com/v2/project/{id} (get project name)
*
* RATE LIMITS: 300 requests/minute (generous, but cron still throttles)
*
* NOTE: We make TWO API calls here - one for versions, one for project
* name. This could be optimized to a single call if performance matters.
*
* @param string $projectId Modrinth project ID or slug
* @return array Contains: name, version
* @throws \Exception If request fails
*
* @see https://docs.modrinth.com/api/operations/getproject/
*/
private function checkModrinth(string $projectId): array
{
@@ -206,7 +378,22 @@ class ModpackAPIController extends Controller
}
/**
* Check FTB (modpacks.ch) API for latest version
* Query Feed The Beast (FTB) API for latest modpack version.
*
* NO API KEY REQUIRED - modpacks.ch is a public API.
*
* API ENDPOINT: GET https://api.modpacks.ch/public/modpack/{id}
*
* NOTE: FTB modpack IDs are numeric but often displayed differently
* in the launcher. Users may need to find the ID from the modpack URL.
*
* The versions array is sorted newest-first, so [0] is always latest.
*
* @param string $modpackId FTB modpack ID (numeric string)
* @return array Contains: name, version
* @throws \Exception If request fails
*
* @see https://api.modpacks.ch/
*/
private function checkFTB(string $modpackId): array
{
@@ -227,7 +414,23 @@ class ModpackAPIController extends Controller
}
/**
* Check Technic API for latest version
* Query Technic Platform API for latest modpack version.
*
* NO API KEY REQUIRED - Technic's API is public.
*
* IMPORTANT: Technic uses SLUGS, not numeric IDs!
* The slug is the URL-friendly name from the modpack page.
* Example: "tekkit" for https://www.technicpack.net/modpack/tekkit
*
* API ENDPOINT: GET https://api.technicpack.net/modpack/{slug}?build=1
*
* The ?build=1 parameter includes build metadata in the response.
*
* @param string $slug Technic modpack slug (URL-friendly name)
* @return array Contains: name (displayName preferred), version
* @throws \Exception If request fails
*
* @see https://www.technicpack.net/api
*/
private function checkTechnic(string $slug): array
{
@@ -248,8 +451,48 @@ class ModpackAPIController extends Controller
}
/**
* Get cached update status for all servers (dashboard badge view)
* Called once on page load, returns status for all user's servers
* Get cached update status for all of a user's servers.
*
* THIS IS THE DASHBOARD BADGE ENDPOINT.
*
* CRITICAL: This method ONLY reads from the local database cache.
* It NEVER makes external API calls. This is by design.
*
* WHY CACHED-ONLY?
* Imagine a panel with 50 servers across 20 users. If each dashboard
* load triggered 50 live API calls, you'd hit rate limits in minutes.
* Instead, the CheckModpackUpdates cron job runs hourly/daily and
* populates the modpackchecker_servers table. This endpoint just
* reads that cache.
*
* The React component (UpdateBadge.tsx) calls this ONCE on page load
* and caches the result client-side to avoid even repeated DB queries.
*
* RESPONSE FORMAT:
* Keyed by server UUID for easy lookup in the React component.
* Only includes servers that have been checked by the cron job.
*
* @param Request $request The incoming HTTP request (includes auth user)
* @return JsonResponse Keyed by server_uuid, contains update status
*
* @example Response:
* {
* "a1b2c3d4-...": {
* "update_available": true,
* "modpack_name": "All The Mods 9",
* "current_version": "0.2.51",
* "latest_version": "0.2.60"
* },
* "e5f6g7h8-...": {
* "update_available": false,
* "modpack_name": "Adrenaserver",
* "current_version": "1.7.0",
* "latest_version": "1.7.0"
* }
* }
*
* @see CheckModpackUpdates.php (cron command that populates this cache)
* @see UpdateBadge.tsx (React component that consumes this endpoint)
*/
public function getStatus(Request $request): JsonResponse
{