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:
@@ -1,5 +1,62 @@
|
|||||||
<?php
|
<?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;
|
namespace Pterodactyl\BlueprintFramework\Extensions\modpackchecker\Controllers;
|
||||||
|
|
||||||
use Pterodactyl\Http\Controllers\Controller;
|
use Pterodactyl\Http\Controllers\Controller;
|
||||||
@@ -11,6 +68,13 @@ use Illuminate\Http\JsonResponse;
|
|||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\DB;
|
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
|
class ModpackAPIController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
private function checkTechnic(string $slug): array
|
||||||
{
|
{
|
||||||
@@ -248,8 +451,48 @@ class ModpackAPIController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cached update status for all servers (dashboard badge view)
|
* Get cached update status for all of a user's servers.
|
||||||
* Called once on page load, returns status for all 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
|
public function getStatus(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
365
services/modpack-version-checker/blueprint-extension/README.md
Normal file
365
services/modpack-version-checker/blueprint-extension/README.md
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
# ModpackChecker — Pterodactyl Blueprint Extension
|
||||||
|
|
||||||
|
**Version:** 1.0.0
|
||||||
|
**Author:** Firefrost Gaming
|
||||||
|
**License:** Proprietary (Commercial product for BuiltByBit)
|
||||||
|
|
||||||
|
A Pterodactyl Panel extension that checks modpack versions across CurseForge, Modrinth, FTB, and Technic platforms. Shows update status on the dashboard and provides manual version checks from the server console.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Features](#features)
|
||||||
|
2. [Architecture Overview](#architecture-overview)
|
||||||
|
3. [File Structure](#file-structure)
|
||||||
|
4. [Installation](#installation)
|
||||||
|
5. [Configuration](#configuration)
|
||||||
|
6. [Usage](#usage)
|
||||||
|
7. [Development](#development)
|
||||||
|
8. [API Reference](#api-reference)
|
||||||
|
9. [Troubleshooting](#troubleshooting)
|
||||||
|
10. [Design Decisions](#design-decisions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Dashboard Badge
|
||||||
|
- Shows a colored dot next to each server name on the dashboard
|
||||||
|
- **🟠 Orange (Fire #FF6B35):** Update available
|
||||||
|
- **🟢 Teal (Frost #4ECDC4):** Up to date
|
||||||
|
- Hover for version details tooltip
|
||||||
|
- Single API call per page load (cached globally)
|
||||||
|
|
||||||
|
### Console Widget
|
||||||
|
- "Check for Updates" button on each server's console page
|
||||||
|
- Real-time version check against platform API
|
||||||
|
- Shows modpack name, current version, and latest version
|
||||||
|
|
||||||
|
### Admin Panel
|
||||||
|
- Configure CurseForge API key
|
||||||
|
- View extension status
|
||||||
|
- (Future: Rate limit settings, notification preferences)
|
||||||
|
|
||||||
|
### Supported Platforms
|
||||||
|
| Platform | ID Type | API Key Required |
|
||||||
|
|----------|---------|------------------|
|
||||||
|
| CurseForge | Numeric project ID | ✅ Yes |
|
||||||
|
| Modrinth | Project ID or slug | ❌ No |
|
||||||
|
| FTB | Numeric modpack ID | ❌ No |
|
||||||
|
| Technic | URL slug | ❌ No |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ MODPACK VERSION CHECKER │
|
||||||
|
│ Architecture Diagram │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ CRON JOB (runs every 4-6 hrs) │
|
||||||
|
│ php artisan modpackchecker:check │
|
||||||
|
│ │
|
||||||
|
│ • Finds servers with MODPACK_* │
|
||||||
|
│ • Calls platform APIs one by one │
|
||||||
|
│ • 2-second delay between calls │
|
||||||
|
│ • Stores results in database │
|
||||||
|
└──────────────────┬──────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ DATABASE CACHE │
|
||||||
|
│ modpackchecker_servers table │
|
||||||
|
│ │
|
||||||
|
│ • server_id, server_uuid │
|
||||||
|
│ • platform, modpack_id │
|
||||||
|
│ • current_version, latest_version │
|
||||||
|
│ • update_available (boolean) │
|
||||||
|
│ • last_checked timestamp │
|
||||||
|
└──────────────────┬──────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────────┴──────────────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌───────────────────────────┐ ┌───────────────────────────┐
|
||||||
|
│ DASHBOARD BADGE │ │ CONSOLE WIDGET │
|
||||||
|
│ (UpdateBadge.tsx) │ │ (wrapper.tsx) │
|
||||||
|
│ │ │ │
|
||||||
|
│ • Reads from cache ONLY │ │ • Manual "Check" button │
|
||||||
|
│ • Never calls external │ │ • LIVE API call │
|
||||||
|
│ • One API call per page │ │ • Single server only │
|
||||||
|
│ • Shows 🟠 or 🟢 dot │ │ • Shows full details │
|
||||||
|
└───────────────────────────┘ └───────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why This Architecture?
|
||||||
|
|
||||||
|
**The Problem:** A panel with 50 servers and 20 active users could generate thousands of API calls per day if each dashboard view triggered live checks.
|
||||||
|
|
||||||
|
**The Solution:**
|
||||||
|
1. Cron job handles external API calls with rate limiting
|
||||||
|
2. Dashboard reads from local cache only
|
||||||
|
3. Console provides on-demand checks for specific servers
|
||||||
|
|
||||||
|
This was validated by Gemini AI during architectural review.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
blueprint-extension/
|
||||||
|
├── README.md # This file
|
||||||
|
├── conf.yml # Blueprint configuration
|
||||||
|
├── build.sh # Injection script (runs during blueprint -build)
|
||||||
|
│
|
||||||
|
├── Controllers/
|
||||||
|
│ └── ModpackAPIController.php # API endpoints (manualCheck, getStatus)
|
||||||
|
│
|
||||||
|
├── admin/
|
||||||
|
│ ├── controller.php # Admin panel logic
|
||||||
|
│ └── view.blade.php # Admin panel UI
|
||||||
|
│
|
||||||
|
├── console/
|
||||||
|
│ └── CheckModpackUpdates.php # Laravel cron command
|
||||||
|
│
|
||||||
|
├── database/
|
||||||
|
│ └── migrations/
|
||||||
|
│ └── 2024_XX_XX_create_modpackchecker_servers_table.php
|
||||||
|
│
|
||||||
|
├── routes/
|
||||||
|
│ └── client.php # API route definitions
|
||||||
|
│
|
||||||
|
└── views/
|
||||||
|
├── server/
|
||||||
|
│ └── wrapper.tsx # Console "Check for Updates" widget
|
||||||
|
└── dashboard/
|
||||||
|
└── UpdateBadge.tsx # Dashboard status dot component
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Pterodactyl Panel v1.11+
|
||||||
|
- Blueprint Framework (beta-2026-01 or newer)
|
||||||
|
- PHP 8.1+
|
||||||
|
- Node.js 18+
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. **Copy extension to Blueprint directory:**
|
||||||
|
```bash
|
||||||
|
cp -r blueprint-extension /var/www/pterodactyl/.blueprint/extensions/modpackchecker
|
||||||
|
chown -R www-data:www-data /var/www/pterodactyl/.blueprint/extensions/modpackchecker
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build the extension:**
|
||||||
|
```bash
|
||||||
|
cd /var/www/pterodactyl
|
||||||
|
blueprint -build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Compile frontend assets:**
|
||||||
|
```bash
|
||||||
|
export NODE_OPTIONS=--openssl-legacy-provider
|
||||||
|
yarn build:production
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run database migration:**
|
||||||
|
```bash
|
||||||
|
php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Restart PHP-FPM:**
|
||||||
|
```bash
|
||||||
|
systemctl restart php8.3-fpm
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Set up cron job:**
|
||||||
|
```bash
|
||||||
|
# Add to /etc/crontab or crontab -e
|
||||||
|
0 */6 * * * www-data cd /var/www/pterodactyl && php artisan modpackchecker:check >> /dev/null 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Server Egg Variables
|
||||||
|
|
||||||
|
For modpack detection, set these variables in your server's egg:
|
||||||
|
|
||||||
|
| Variable | Description | Example |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `MODPACK_PLATFORM` | Platform name | `modrinth`, `curseforge`, `ftb`, `technic` |
|
||||||
|
| `MODPACK_ID` | Platform-specific ID | `adrenaserver` (Modrinth slug) |
|
||||||
|
| `MODPACK_CURRENT_VERSION` | Installed version | `1.7.0` |
|
||||||
|
|
||||||
|
### CurseForge API Key
|
||||||
|
|
||||||
|
CurseForge requires an API key. To configure:
|
||||||
|
|
||||||
|
1. Apply for API access at https://docs.curseforge.com/
|
||||||
|
2. Go to **Admin Panel → Extensions → ModpackChecker**
|
||||||
|
3. Enter your API key and save
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Dashboard Badge
|
||||||
|
No action needed — badges appear automatically for servers that:
|
||||||
|
- Have `MODPACK_PLATFORM` egg variable set
|
||||||
|
- Have been checked by the cron job at least once
|
||||||
|
|
||||||
|
### Manual Check
|
||||||
|
1. Go to any server's console page
|
||||||
|
2. Click "Check for Updates" button
|
||||||
|
3. View results showing modpack name and version status
|
||||||
|
|
||||||
|
### Cron Command
|
||||||
|
Run manually for testing:
|
||||||
|
```bash
|
||||||
|
cd /var/www/pterodactyl
|
||||||
|
php artisan modpackchecker:check
|
||||||
|
```
|
||||||
|
|
||||||
|
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: Vanilla+ (e5f6g7h8-...)
|
||||||
|
🟢 Up to date: Vanilla+ - 1.2.0
|
||||||
|
Modpack update check complete!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
1. Enable Blueprint developer mode in admin panel
|
||||||
|
2. Make changes in `.blueprint/dev/` or `.blueprint/extensions/modpackchecker/`
|
||||||
|
3. Run `blueprint -build` after changes
|
||||||
|
4. Run `yarn build:production` for frontend changes
|
||||||
|
|
||||||
|
### Testing API Endpoints
|
||||||
|
```bash
|
||||||
|
# Manual check (requires auth token)
|
||||||
|
curl -X POST "https://panel.example.com/api/client/servers/{uuid}/ext/modpackchecker/check" \
|
||||||
|
-H "Authorization: Bearer {token}"
|
||||||
|
|
||||||
|
# Get all statuses (requires auth token)
|
||||||
|
curl "https://panel.example.com/api/client/extensions/modpackchecker/status" \
|
||||||
|
-H "Authorization: Bearer {token}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a New Platform
|
||||||
|
1. Add check method to `ModpackAPIController.php` (e.g., `checkNewPlatform()`)
|
||||||
|
2. Add to the `match()` statement in `checkVersion()`
|
||||||
|
3. Add same method to `CheckModpackUpdates.php`
|
||||||
|
4. Update this README
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### POST /api/client/servers/{server}/ext/modpackchecker/check
|
||||||
|
|
||||||
|
Manual version check for a specific server. Makes live API call.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"platform": "modrinth",
|
||||||
|
"modpack_id": "adrenaserver",
|
||||||
|
"modpack_name": "Adrenaserver",
|
||||||
|
"latest_version": "1.7.0+1.21.1.fabric",
|
||||||
|
"status": "checked"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/client/extensions/modpackchecker/status
|
||||||
|
|
||||||
|
Get cached status for all servers accessible to the authenticated user.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Badge not showing
|
||||||
|
1. Check server has `MODPACK_PLATFORM` variable set
|
||||||
|
2. Run cron command manually: `php artisan modpackchecker:check`
|
||||||
|
3. Check `modpackchecker_servers` table for entries
|
||||||
|
|
||||||
|
### "CurseForge API key not configured"
|
||||||
|
1. Go to Admin → Extensions → ModpackChecker
|
||||||
|
2. Enter your CurseForge API key
|
||||||
|
3. Key must have mod read permissions
|
||||||
|
|
||||||
|
### 500 errors on check
|
||||||
|
1. Check PHP error log: `tail -f /var/log/php8.3-fpm.log`
|
||||||
|
2. Verify controller namespace: `Pterodactyl\BlueprintFramework\Extensions\modpackchecker\Controllers`
|
||||||
|
3. Restart PHP-FPM: `systemctl restart php8.3-fpm`
|
||||||
|
|
||||||
|
### Build.sh not running
|
||||||
|
1. Ensure file is executable: `chmod +x build.sh`
|
||||||
|
2. Check Blueprint version supports build scripts
|
||||||
|
3. Run manually from panel root: `bash .blueprint/extensions/modpackchecker/build.sh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
### Why cache instead of live checks?
|
||||||
|
Rate limits. CurseForge allows ~1000 requests/day for personal keys. A busy panel could exhaust that in hours without caching.
|
||||||
|
|
||||||
|
### Why 2-second sleep in cron?
|
||||||
|
Prevents burst traffic to APIs. 50 servers × 2 seconds = ~2 minute runtime, which is acceptable for a background job.
|
||||||
|
|
||||||
|
### Why inline styles in React?
|
||||||
|
The component is injected into Pterodactyl's build. Adding CSS classes would require modifying their build pipeline. Inline styles are self-contained.
|
||||||
|
|
||||||
|
### Why separate console widget and dashboard badge?
|
||||||
|
Different use cases:
|
||||||
|
- Dashboard: Quick overview, needs to be fast → cached
|
||||||
|
- Console: User wants current info → live API call is acceptable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
**Development Team:**
|
||||||
|
- Architecture design: Gemini AI
|
||||||
|
- Implementation: Chroniclers #52, #62, #63 (Claude instances)
|
||||||
|
- Project Lead: Michael "Frostystyle" Krause
|
||||||
|
|
||||||
|
**Part of Firefrost Gaming**
|
||||||
|
*Fire + Frost + Foundation = Where Love Builds Legacy* 🔥❄️💙
|
||||||
@@ -1,5 +1,67 @@
|
|||||||
<?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;
|
namespace Pterodactyl\Console\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
@@ -7,11 +69,37 @@ use Illuminate\Support\Facades\DB;
|
|||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Pterodactyl\Models\Server;
|
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
|
class CheckModpackUpdates extends Command
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The console command signature.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
protected $signature = 'modpackchecker:check';
|
protected $signature = 'modpackchecker:check';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description (shown in `php artisan list`).
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
protected $description = 'Check all servers for modpack updates';
|
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
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$this->info('Starting modpack update check...');
|
$this->info('Starting modpack update check...');
|
||||||
@@ -33,6 +121,21 @@ class CheckModpackUpdates extends Command
|
|||||||
return 0;
|
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
|
private function checkServer(Server $server): void
|
||||||
{
|
{
|
||||||
$this->line("Checking: {$server->name} ({$server->uuid})");
|
$this->line("Checking: {$server->name} ({$server->uuid})");
|
||||||
@@ -89,6 +192,19 @@ class CheckModpackUpdates extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
private function getVariable(Server $server, string $name): ?string
|
||||||
{
|
{
|
||||||
$variable = $server->variables()
|
$variable = $server->variables()
|
||||||
@@ -97,6 +213,15 @@ class CheckModpackUpdates extends Command
|
|||||||
return $variable?->server_value;
|
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
|
private function fetchLatestVersion(string $platform, string $modpackId): ?array
|
||||||
{
|
{
|
||||||
return match($platform) {
|
return match($platform) {
|
||||||
@@ -174,6 +299,22 @@ class CheckModpackUpdates extends Command
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
private function updateDatabase(Server $server, array $data): void
|
||||||
{
|
{
|
||||||
DB::table('modpackchecker_servers')->updateOrInsert(
|
DB::table('modpackchecker_servers')->updateOrInsert(
|
||||||
|
|||||||
@@ -1,108 +1,317 @@
|
|||||||
|
/**
|
||||||
|
* =============================================================================
|
||||||
|
* MODPACK VERSION CHECKER - DASHBOARD BADGE COMPONENT
|
||||||
|
* =============================================================================
|
||||||
|
*
|
||||||
|
* React component that displays a colored indicator dot next to server names
|
||||||
|
* on the Pterodactyl dashboard, showing modpack update status at a glance.
|
||||||
|
*
|
||||||
|
* VISUAL DESIGN:
|
||||||
|
* - 🟢 Frost (#4ECDC4): Server's modpack is up to date
|
||||||
|
* - 🟠 Fire (#FF6B35): Update available for this modpack
|
||||||
|
* - No dot: Server has no modpack configured or not yet checked
|
||||||
|
*
|
||||||
|
* Colors match Firefrost Gaming brand palette.
|
||||||
|
*
|
||||||
|
* CRITICAL ARCHITECTURE DECISION (Gemini-approved):
|
||||||
|
* This component is intentionally "dumb" - it ONLY reads from a local cache.
|
||||||
|
* It NEVER makes external API calls to modpack platforms.
|
||||||
|
*
|
||||||
|
* WHY?
|
||||||
|
* Imagine a dashboard with 20 servers. If each server row triggered a live
|
||||||
|
* API call to CurseForge/Modrinth, you'd make 20+ requests on every page load.
|
||||||
|
* Multiply by multiple users refreshing throughout the day, and you'd hit
|
||||||
|
* rate limits within hours.
|
||||||
|
*
|
||||||
|
* Instead, this component:
|
||||||
|
* 1. Makes ONE API call to our backend (/api/client/extensions/modpackchecker/status)
|
||||||
|
* 2. Backend returns cached data from modpackchecker_servers table
|
||||||
|
* 3. Results are cached globally in JS memory for the session
|
||||||
|
* 4. Each badge instance reads from this shared cache
|
||||||
|
*
|
||||||
|
* The actual modpack checking is done by a cron job (CheckModpackUpdates.php)
|
||||||
|
* that runs on a schedule with proper rate limiting.
|
||||||
|
*
|
||||||
|
* INJECTION:
|
||||||
|
* This component is injected into ServerRow.tsx by build.sh during
|
||||||
|
* `blueprint -build`. It receives the server UUID as a prop.
|
||||||
|
*
|
||||||
|
* DEPENDENCIES:
|
||||||
|
* - @/api/http: Pterodactyl's axios wrapper (handles auth automatically)
|
||||||
|
* - Backend endpoint: GET /api/client/extensions/modpackchecker/status
|
||||||
|
*
|
||||||
|
* @package ModpackChecker Blueprint Extension
|
||||||
|
* @author Firefrost Gaming (Chroniclers #62, #63)
|
||||||
|
* @version 1.0.0
|
||||||
|
* @see CheckModpackUpdates.php (cron that populates the cache)
|
||||||
|
* @see ModpackAPIController::getStatus() (backend endpoint)
|
||||||
|
* =============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
|
|
||||||
/**
|
// =============================================================================
|
||||||
* UpdateBadge - Shows a colored dot next to server name on dashboard
|
// TYPE DEFINITIONS
|
||||||
*
|
// =============================================================================
|
||||||
* Architecture (per Gemini/Pyrrhus):
|
|
||||||
* - NEVER calls external APIs directly from dashboard
|
|
||||||
* - Reads from local database cache populated by cron job
|
|
||||||
* - Single API call on page load, cached globally
|
|
||||||
* - Shows 🟢 (up to date) or 🟠 (update available)
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status data for a single server, as returned from the backend.
|
||||||
|
*
|
||||||
|
* This mirrors the structure returned by ModpackAPIController::getStatus().
|
||||||
|
* All fields except update_available are optional because servers might
|
||||||
|
* have partial data (e.g., error during last check).
|
||||||
|
*/
|
||||||
interface ServerStatus {
|
interface ServerStatus {
|
||||||
|
/** True if latest_version differs from current_version */
|
||||||
update_available: boolean;
|
update_available: boolean;
|
||||||
|
/** Human-readable modpack name (e.g., "All The Mods 9") */
|
||||||
modpack_name?: string;
|
modpack_name?: string;
|
||||||
|
/** Version currently installed on the server */
|
||||||
current_version?: string;
|
current_version?: string;
|
||||||
|
/** Latest version available from the platform */
|
||||||
latest_version?: string;
|
latest_version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full cache structure - keyed by server UUID.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* {
|
||||||
|
* "a1b2c3d4-e5f6-7890-...": { update_available: true, modpack_name: "ATM9", ... },
|
||||||
|
* "b2c3d4e5-f6g7-8901-...": { update_available: false, modpack_name: "Vanilla+", ... }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
interface StatusCache {
|
interface StatusCache {
|
||||||
[serverUuid: string]: ServerStatus;
|
[serverUuid: string]: ServerStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global cache - shared across all UpdateBadge instances
|
// =============================================================================
|
||||||
// Prevents multiple API calls when rendering server list
|
// GLOBAL CACHE
|
||||||
|
// =============================================================================
|
||||||
|
//
|
||||||
|
// These module-level variables are shared across ALL instances of UpdateBadge.
|
||||||
|
// This is intentional - we want exactly ONE API call for the entire dashboard,
|
||||||
|
// not one per server row.
|
||||||
|
//
|
||||||
|
// The pattern here is a simple "fetch-once" cache:
|
||||||
|
// - globalCache: Stores the data once fetched
|
||||||
|
// - fetchPromise: Prevents duplicate in-flight requests
|
||||||
|
//
|
||||||
|
// LIFECYCLE:
|
||||||
|
// 1. First UpdateBadge mounts → fetchAllStatuses() called → API request starts
|
||||||
|
// 2. Second UpdateBadge mounts → fetchAllStatuses() returns same promise
|
||||||
|
// 3. API response arrives → globalCache populated, all badges update
|
||||||
|
// 4. Any future calls → return globalCache immediately (no API call)
|
||||||
|
//
|
||||||
|
// CACHE INVALIDATION:
|
||||||
|
// Currently, cache persists until page refresh. For real-time updates,
|
||||||
|
// you could add a timeout or expose a refresh function.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/** Cached status data. Null until first fetch completes. */
|
||||||
let globalCache: StatusCache | null = null;
|
let globalCache: StatusCache | null = null;
|
||||||
|
|
||||||
|
/** Promise for in-flight fetch. Prevents duplicate requests. */
|
||||||
let fetchPromise: Promise<StatusCache> | null = null;
|
let fetchPromise: Promise<StatusCache> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all server statuses from the backend.
|
||||||
|
*
|
||||||
|
* This function implements a "fetch-once" pattern:
|
||||||
|
* - First call: Makes the API request, stores result in globalCache
|
||||||
|
* - Subsequent calls: Returns cached data immediately
|
||||||
|
* - Concurrent calls: Wait for the same promise (no duplicate requests)
|
||||||
|
*
|
||||||
|
* ENDPOINT: GET /api/client/extensions/modpackchecker/status
|
||||||
|
*
|
||||||
|
* The backend (ModpackAPIController::getStatus) returns only servers
|
||||||
|
* that the authenticated user has access to, so there's no data leakage.
|
||||||
|
*
|
||||||
|
* ERROR HANDLING:
|
||||||
|
* On failure, we cache an empty object rather than null. This prevents
|
||||||
|
* retry spam - if the API is down, we don't hammer it on every badge mount.
|
||||||
|
* Users can refresh the page to retry.
|
||||||
|
*
|
||||||
|
* @returns Promise resolving to the status cache (keyed by server UUID)
|
||||||
|
*/
|
||||||
const fetchAllStatuses = async (): Promise<StatusCache> => {
|
const fetchAllStatuses = async (): Promise<StatusCache> => {
|
||||||
// Return cached data if available
|
// FAST PATH: Return cached data if available
|
||||||
if (globalCache !== null) {
|
if (globalCache !== null) {
|
||||||
return globalCache;
|
return globalCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If already fetching, wait for that promise
|
// DEDUP PATH: If a fetch is already in progress, wait for it
|
||||||
|
// instead of starting another request
|
||||||
if (fetchPromise !== null) {
|
if (fetchPromise !== null) {
|
||||||
return fetchPromise;
|
return fetchPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start new fetch
|
// FETCH PATH: Start a new API request
|
||||||
|
// This is the only code path that actually makes an HTTP call
|
||||||
fetchPromise = http.get('/api/client/extensions/modpackchecker/status')
|
fetchPromise = http.get('/api/client/extensions/modpackchecker/status')
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
// Store the response data in the global cache
|
||||||
globalCache = response.data || {};
|
globalCache = response.data || {};
|
||||||
return globalCache;
|
return globalCache;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
// Log the error for debugging
|
||||||
console.error('ModpackChecker: Failed to fetch status', error);
|
console.error('ModpackChecker: Failed to fetch status', error);
|
||||||
|
// Cache empty object to prevent retry spam
|
||||||
|
// Users can refresh the page to try again
|
||||||
globalCache = {};
|
globalCache = {};
|
||||||
return globalCache;
|
return globalCache;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
// Clear the promise reference
|
||||||
|
// This allows future retries if cache is manually cleared
|
||||||
fetchPromise = null;
|
fetchPromise = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
return fetchPromise;
|
return fetchPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// COMPONENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the UpdateBadge component.
|
||||||
|
*/
|
||||||
interface UpdateBadgeProps {
|
interface UpdateBadgeProps {
|
||||||
|
/**
|
||||||
|
* The UUID of the server to show status for.
|
||||||
|
* This is passed from ServerRow.tsx where the component is injected.
|
||||||
|
* Example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||||
|
*/
|
||||||
serverUuid: string;
|
serverUuid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard badge showing modpack update status.
|
||||||
|
*
|
||||||
|
* Renders a small colored dot next to the server name:
|
||||||
|
* - Orange (#FF6B35) = Update available (Fire brand color)
|
||||||
|
* - Teal (#4ECDC4) = Up to date (Frost brand color)
|
||||||
|
* - Nothing = No modpack configured or not yet checked by cron
|
||||||
|
*
|
||||||
|
* Includes a native browser tooltip on hover showing version details.
|
||||||
|
*
|
||||||
|
* USAGE (injected by build.sh, not manually added):
|
||||||
|
* ```tsx
|
||||||
|
* <p>{server.name}<UpdateBadge serverUuid={server.uuid} /></p>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ACCESSIBILITY:
|
||||||
|
* - Uses aria-label for screen readers
|
||||||
|
* - Native title attribute provides tooltip for sighted users
|
||||||
|
* - Color is not the only indicator (tooltip shows text status)
|
||||||
|
*/
|
||||||
const UpdateBadge: React.FC<UpdateBadgeProps> = ({ serverUuid }) => {
|
const UpdateBadge: React.FC<UpdateBadgeProps> = ({ serverUuid }) => {
|
||||||
|
// =========================================================================
|
||||||
|
// STATE
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/** This specific server's status (extracted from global cache) */
|
||||||
const [status, setStatus] = useState<ServerStatus | null>(null);
|
const [status, setStatus] = useState<ServerStatus | null>(null);
|
||||||
|
|
||||||
|
/** Loading state - true until we've checked the cache */
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// DATA FETCHING
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Fetch from global cache (makes API call only on first badge mount)
|
||||||
fetchAllStatuses()
|
fetchAllStatuses()
|
||||||
.then((cache) => {
|
.then((cache) => {
|
||||||
|
// Extract this server's status from the cache
|
||||||
|
// Will be null/undefined if server not in cache
|
||||||
setStatus(cache[serverUuid] || null);
|
setStatus(cache[serverUuid] || null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}, [serverUuid]);
|
}, [serverUuid]); // Re-run if serverUuid changes (unlikely in practice)
|
||||||
|
|
||||||
// Don't render anything while loading or if no status
|
// =========================================================================
|
||||||
if (loading || !status) {
|
// RENDER CONDITIONS
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Don't render anything while waiting for cache
|
||||||
|
// This prevents flicker - badges appear all at once when data arrives
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't render if no status data exists for this server
|
||||||
|
// This happens for servers that:
|
||||||
|
// - Don't have MODPACK_PLATFORM configured
|
||||||
|
// - Haven't been checked by the cron job yet
|
||||||
|
// - Had an error during their last check
|
||||||
|
if (!status) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show badge if we have modpack data
|
// Don't render if we have a status entry but no modpack name
|
||||||
|
// This can happen if the check errored but created a partial record
|
||||||
if (!status.modpack_name) {
|
if (!status.modpack_name) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// STYLING
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline styles for the dot indicator.
|
||||||
|
*
|
||||||
|
* Using inline styles rather than CSS classes because:
|
||||||
|
* 1. This component is injected into Pterodactyl's build
|
||||||
|
* 2. We can't easily add to their CSS pipeline
|
||||||
|
* 3. Inline styles are self-contained and reliable
|
||||||
|
*
|
||||||
|
* BRAND COLORS (Firefrost Gaming):
|
||||||
|
* - Fire: #FF6B35 (used for "update available" - action needed)
|
||||||
|
* - Frost: #4ECDC4 (used for "up to date" - all good)
|
||||||
|
*/
|
||||||
const dotStyle: React.CSSProperties = {
|
const dotStyle: React.CSSProperties = {
|
||||||
|
// Layout
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
width: '8px',
|
width: '8px',
|
||||||
height: '8px',
|
height: '8px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%', // Perfect circle
|
||||||
marginLeft: '8px',
|
marginLeft: '8px', // Space from server name
|
||||||
backgroundColor: status.update_available ? '#FF6B35' : '#4ECDC4', // Fire : Frost
|
|
||||||
|
// Color based on update status
|
||||||
|
backgroundColor: status.update_available ? '#FF6B35' : '#4ECDC4',
|
||||||
|
|
||||||
|
// Subtle glow effect for visual polish
|
||||||
|
// Uses rgba version of the same color at 50% opacity
|
||||||
boxShadow: status.update_available
|
boxShadow: status.update_available
|
||||||
? '0 0 4px rgba(255, 107, 53, 0.5)'
|
? '0 0 4px rgba(255, 107, 53, 0.5)' // Fire glow
|
||||||
: '0 0 4px rgba(78, 205, 196, 0.5)',
|
: '0 0 4px rgba(78, 205, 196, 0.5)', // Frost glow
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tooltip text shown on hover.
|
||||||
|
*
|
||||||
|
* Uses native browser tooltip (title attribute) for simplicity.
|
||||||
|
* A fancier tooltip library could be added later if needed.
|
||||||
|
*/
|
||||||
const tooltipText = status.update_available
|
const tooltipText = status.update_available
|
||||||
? `Update available: ${status.latest_version}`
|
? `Update available: ${status.latest_version}`
|
||||||
: `Up to date: ${status.latest_version}`;
|
: `Up to date: ${status.latest_version}`;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// RENDER
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
style={dotStyle}
|
style={dotStyle}
|
||||||
title={tooltipText}
|
title={tooltipText} // Native browser tooltip
|
||||||
aria-label={tooltipText}
|
aria-label={tooltipText} // Accessibility for screen readers
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user