refactor(modpackchecker): Batch 3+4 fixes - frontend, admin, docs

BATCH 3 - Frontend & UI:

wrapper.tsx (Console Widget):
- FIXED: API URL from .../ext/modpackchecker/check to .../check
- Added 429 rate limit handling with user-friendly message

UpdateBadge.tsx (Dashboard Badge):
- Added 60-second TTL to global cache (was infinite)
- Prevents stale data during client-side navigation

admin/view.blade.php:
- Disabled Discord webhook field (PRO TIER badge)
- Disabled Check Interval field (PRO TIER badge)
- Added support callout linking to Discord

BATCH 4 - Documentation:

README.md:
- Fixed architecture diagram (server_uuid, status string)
- Added app/Services/ModpackApiService.php to file structure
- Fixed API endpoint URLs throughout
- Updated installation for BuiltByBit (.blueprint package)
- Updated 'Adding New Platform' instructions for Service pattern
- Added Support section with Discord link
- Changed license to explicit commercial terms

NEW: CHANGELOG.md
- Version history for future updates
- Documents v1.0.0 features

Reviewed by: Gemini AI (Architecture Consultant)
Signed-off-by: Claude (Chronicler #63) <claude@firefrostgaming.com>
This commit is contained in:
Claude (Chronicler #63)
2026-04-06 11:47:20 +00:00
parent 8e37120289
commit 5a607c8c8b
5 changed files with 167 additions and 393 deletions

View File

@@ -0,0 +1,32 @@
# Changelog
All notable changes to ModpackChecker will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2026-04-06
### Added
- Initial release
- Dashboard badge showing update status (🟠 update available / 🟢 up to date)
- Console widget with "Check for Updates" button
- Support for 4 modpack platforms:
- CurseForge (requires API key)
- Modrinth (no key required)
- FTB via modpacks.ch (no key required)
- Technic (no key required, dynamic build detection)
- Admin panel for CurseForge API key configuration
- Cron command for automated background checks
- Rate limiting: 2 manual checks per minute per server
- 60-second TTL cache for dashboard badges
- Foreign key cascade delete for data integrity
### Architecture
- Centralized `ModpackApiService` for all platform API calls
- Cached Technic launcher build number (12-hour TTL)
- Database table `modpackchecker_servers` for status caching
---
*Fire + Frost + Foundation = Where Love Builds Legacy* 🔥❄️💙

View File

@@ -1,8 +1,8 @@
# ModpackChecker — Pterodactyl Blueprint Extension # ModpackChecker — Pterodactyl Blueprint Extension
**Version:** 1.0.0 **Version:** 1.0.0
**Author:** Firefrost Gaming **Author:** Firefrost Gaming / Frostystyle
**License:** Proprietary (Commercial product for BuiltByBit) **License:** Commercial License - Unauthorized redistribution, resale, or sharing of this source code is strictly prohibited.
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. 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.
@@ -19,7 +19,7 @@ A Pterodactyl Panel extension that checks modpack versions across CurseForge, Mo
7. [Development](#development) 7. [Development](#development)
8. [API Reference](#api-reference) 8. [API Reference](#api-reference)
9. [Troubleshooting](#troubleshooting) 9. [Troubleshooting](#troubleshooting)
10. [Design Decisions](#design-decisions) 10. [Support](#support)
--- ---
@@ -30,17 +30,18 @@ A Pterodactyl Panel extension that checks modpack versions across CurseForge, Mo
- **🟠 Orange (Fire #FF6B35):** Update available - **🟠 Orange (Fire #FF6B35):** Update available
- **🟢 Teal (Frost #4ECDC4):** Up to date - **🟢 Teal (Frost #4ECDC4):** Up to date
- Hover for version details tooltip - Hover for version details tooltip
- Single API call per page load (cached globally) - Single API call per page load (cached with 60s TTL)
### Console Widget ### Console Widget
- "Check for Updates" button on each server's console page - "Check for Updates" button on each server's console page
- Real-time version check against platform API - Real-time version check against platform API
- Shows modpack name, current version, and latest version - Rate limited: 2 checks per minute per server
- Shows modpack name and latest version
### Admin Panel ### Admin Panel
- Configure CurseForge API key - Configure CurseForge API key
- View extension status - View supported platforms
- (Future: Rate limit settings, notification preferences) - PRO features: Discord notifications, custom check intervals
### Supported Platforms ### Supported Platforms
| Platform | ID Type | API Key Required | Status | | Platform | ID Type | API Key Required | Status |
@@ -50,8 +51,6 @@ A Pterodactyl Panel extension that checks modpack versions across CurseForge, Mo
| FTB | Numeric modpack ID | ❌ No | ✅ Working | | FTB | Numeric modpack ID | ❌ No | ✅ Working |
| Technic | URL slug | ❌ No | ✅ Working | | Technic | URL slug | ❌ No | ✅ Working |
> **Note:** Technic requires a dynamic build number parameter. The extension automatically fetches the current launcher build from Technic's API to avoid 401 errors.
--- ---
## Architecture Overview ## Architecture Overview
@@ -59,17 +58,11 @@ A Pterodactyl Panel extension that checks modpack versions across CurseForge, Mo
``` ```
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ MODPACK VERSION CHECKER │ │ MODPACK VERSION CHECKER │
│ Architecture Diagram │
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────┐ ┌─────────────────────────────────────┐
│ CRON JOB (runs every 4-6 hrs) │ │ CRON JOB (runs every 4-6 hrs) │
│ php artisan modpackchecker:check │ │ php artisan modpackchecker:check │
│ │
│ • Finds servers with MODPACK_* │
│ • Calls platform APIs one by one │
│ • 2-second delay between calls │
│ • Stores results in database │
└──────────────────┬──────────────────┘ └──────────────────┬──────────────────┘
@@ -77,10 +70,10 @@ A Pterodactyl Panel extension that checks modpack versions across CurseForge, Mo
│ DATABASE CACHE │ │ DATABASE CACHE │
│ modpackchecker_servers table │ │ modpackchecker_servers table │
│ │ │ │
│ • server_id, server_uuid │ • server_uuid
│ • platform, modpack_id │ │ • platform, modpack_id │
│ • current_version, latest_version │ │ • current_version, latest_version │
│ • update_available (boolean) │ • status (string)
│ • last_checked timestamp │ │ • last_checked timestamp │
└──────────────────┬──────────────────┘ └──────────────────┬──────────────────┘
@@ -92,116 +85,76 @@ A Pterodactyl Panel extension that checks modpack versions across CurseForge, Mo
│ (UpdateBadge.tsx) │ │ (wrapper.tsx) │ │ (UpdateBadge.tsx) │ │ (wrapper.tsx) │
│ │ │ │ │ │ │ │
│ • Reads from cache ONLY │ │ • Manual "Check" button │ │ • Reads from cache ONLY │ │ • Manual "Check" button │
│ • Never calls external │ │ • LIVE API call │ │ • 60-second TTL │ │ • LIVE API call │
│ • One API call per page │ │ • Single server only │ • Shows 🟠 or 🟢 dot │ │ • Rate limited (2/min)
│ • 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 ## File Structure
``` ```
blueprint-extension/ blueprint-extension/
├── README.md # This file ├── README.md
├── conf.yml # Blueprint configuration ├── CHANGELOG.md
├── build.sh # Injection script (runs during blueprint -build) ├── conf.yml
├── icon.png # Extension icon (128x128, Gemini-designed) ├── build.sh
├── icon.png
├── app/ # Merges into Pterodactyl's app/ via requests.app ├── app/
│ ├── Console/ │ ├── Console/Commands/
│ │ └── Commands/ │ │ └── CheckModpackUpdates.php
│ └── CheckModpackUpdates.php # Laravel cron command ├── Http/Controllers/
│ └── Http/ │ └── ModpackAPIController.php
└── Controllers/ └── Services/
└── ModpackAPIController.php # API endpoints (manualCheck, getStatus) │ └── ModpackApiService.php
├── admin/ ├── admin/
│ ├── controller.php # Admin panel logic │ ├── controller.php
│ └── view.blade.php # Admin panel UI │ └── view.blade.php
├── database/ ├── database/migrations/
│ └── migrations/ │ └── 2026_04_06_000000_create_modpackchecker_servers_table.php
│ └── 2026_04_06_000000_create_modpackchecker_servers_table.php
├── routes/ ├── routes/
│ └── client.php # API route definitions │ └── client.php
└── views/ └── views/
├── server/ ├── server/wrapper.tsx
│ └── wrapper.tsx # Console "Check for Updates" widget └── dashboard/UpdateBadge.tsx
└── dashboard/
└── UpdateBadge.tsx # Dashboard status dot component
``` ```
### Why the `app/` folder structure?
Blueprint's `requests.app` field merges the contents of your `app/` folder directly into Pterodactyl's `app/` directory. This means:
1. **PSR-4 Autoloading:** Your classes are automatically found by Laravel's autoloader
2. **Correct Namespaces:** Use `Pterodactyl\Http\Controllers` (not custom Blueprint namespaces)
3. **Case Sensitivity:** Linux requires exact folder casing — `Controllers/` not `controllers/`
This architecture was validated through painful debugging and Gemini AI consultation.
--- ---
## Installation ## Installation
### Prerequisites ### Standard Installation (BuiltByBit)
- Pterodactyl Panel v1.11+
- Blueprint Framework (beta-2026-01 or newer)
- PHP 8.1+
- Node.js 18+
### Steps 1. Upload the downloaded `modpackchecker.blueprint` file to your Pterodactyl panel's root directory (usually `/var/www/pterodactyl`).
1. **Copy extension to Blueprint directory:** 2. Run the Blueprint installation command:
```bash ```bash
cp -r blueprint-extension /var/www/pterodactyl/.blueprint/extensions/modpackchecker blueprint -install modpackchecker
chown -R www-data:www-data /var/www/pterodactyl/.blueprint/extensions/modpackchecker
``` ```
2. **Build the extension:** 3. The framework will automatically inject the frontend components and rebuild the panel assets.
```bash
cd /var/www/pterodactyl
blueprint -build
```
3. **Compile frontend assets:** 4. Set up the cron job for automated checks:
```bash ```bash
export NODE_OPTIONS=--openssl-legacy-provider # Add to crontab
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 0 */6 * * * www-data cd /var/www/pterodactyl && php artisan modpackchecker:check >> /dev/null 2>&1
``` ```
### Developer/Manual Installation
If installing from raw source:
```bash
cp -r blueprint-extension /var/www/pterodactyl/.blueprint/extensions/modpackchecker
chown -R www-data:www-data /var/www/pterodactyl/.blueprint/extensions/modpackchecker
blueprint -build
```
--- ---
## Configuration ## Configuration
@@ -218,9 +171,9 @@ For modpack detection, set these variables in your server's egg:
### CurseForge API Key ### CurseForge API Key
CurseForge requires an API key. To configure: CurseForge requires an API key:
1. Apply for API access at https://docs.curseforge.com/ 1. Apply for API access at https://console.curseforge.com/
2. Go to **Admin Panel → Extensions → ModpackChecker** 2. Go to **Admin Panel → Extensions → ModpackChecker**
3. Enter your API key and save 3. Enter your API key and save
@@ -229,7 +182,7 @@ CurseForge requires an API key. To configure:
## Usage ## Usage
### Dashboard Badge ### Dashboard Badge
No action needed — badges appear automatically for servers that: Badges appear automatically for servers that:
- Have `MODPACK_PLATFORM` egg variable set - Have `MODPACK_PLATFORM` egg variable set
- Have been checked by the cron job at least once - Have been checked by the cron job at least once
@@ -241,55 +194,41 @@ No action needed — badges appear automatically for servers that:
### Cron Command ### Cron Command
Run manually for testing: Run manually for testing:
```bash ```bash
cd /var/www/pterodactyl
php artisan modpackchecker:check 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 ## Development
### Local Development ### Adding a New Platform
1. Enable Blueprint developer mode in admin panel
2. Make changes in `.blueprint/dev/` or `.blueprint/extensions/modpackchecker/` 1. Open `app/Services/ModpackApiService.php`
3. Run `blueprint -build` after changes 2. Add your new platform check method (e.g., `private function checkNewPlatform(string $id): array`)
4. Run `yarn build:production` for frontend changes 3. Add the platform key to the `match()` statement inside the `fetchLatestVersion()` method
4. The Controller and Cron Command will automatically inherit the new logic
5. Update this README to reflect the newly supported platform
### Testing API Endpoints ### Testing API Endpoints
```bash ```bash
# Manual check (requires auth token) # Manual check (requires auth token)
curl -X POST "https://panel.example.com/api/client/servers/{uuid}/ext/modpackchecker/check" \ curl -X POST "https://panel.example.com/api/client/extensions/modpackchecker/servers/{uuid}/check" \
-H "Authorization: Bearer {token}" -H "Authorization: Bearer {token}"
# Get all statuses (requires auth token) # Get all statuses
curl "https://panel.example.com/api/client/extensions/modpackchecker/status" \ curl "https://panel.example.com/api/client/extensions/modpackchecker/status" \
-H "Authorization: Bearer {token}" -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 ## API Reference
### POST /api/client/servers/{server}/ext/modpackchecker/check ### POST `/api/client/extensions/modpackchecker/servers/{server}/check`
Manual version check for a specific server. Makes live API call. Manual version check for a specific server. Triggers a live API call to the modpack platform.
**Rate Limit:** 2 requests per minute per server
**Response:** **Response:**
```json ```json
@@ -303,7 +242,7 @@ Manual version check for a specific server. Makes live API call.
} }
``` ```
### GET /api/client/extensions/modpackchecker/status ### GET `/api/client/extensions/modpackchecker/status`
Get cached status for all servers accessible to the authenticated user. Get cached status for all servers accessible to the authenticated user.
@@ -315,12 +254,6 @@ Get cached status for all servers accessible to the authenticated user.
"modpack_name": "All The Mods 9", "modpack_name": "All The Mods 9",
"current_version": "0.2.51", "current_version": "0.2.51",
"latest_version": "0.2.60" "latest_version": "0.2.60"
},
"e5f6g7h8-...": {
"update_available": false,
"modpack_name": "Adrenaserver",
"current_version": "1.7.0",
"latest_version": "1.7.0"
} }
} }
``` ```
@@ -344,36 +277,24 @@ Get cached status for all servers accessible to the authenticated user.
2. Verify controller namespace: `Pterodactyl\Http\Controllers` 2. Verify controller namespace: `Pterodactyl\Http\Controllers`
3. Restart PHP-FPM: `systemctl restart php8.3-fpm` 3. Restart PHP-FPM: `systemctl restart php8.3-fpm`
### Build.sh not running ### "Rate limit reached" message
1. Ensure file is executable: `chmod +x build.sh` The manual check is limited to 2 requests per minute per server. Wait 60 seconds and try again.
2. Check Blueprint version supports build scripts
3. Run manually from panel root: `bash .blueprint/extensions/modpackchecker/build.sh`
--- ---
## Design Decisions ## Support
### Why cache instead of live checks? **Need help?** Join our Discord for support:
Rate limits. CurseForge allows ~1000 requests/day for personal keys. A busy panel could exhaust that in hours without caching. - **Discord:** [discord.firefrostgaming.com](https://discord.firefrostgaming.com)
- **Email:** dev@firefrostgaming.com
### Why 2-second sleep in cron? - **Website:** [firefrostgaming.com](https://firefrostgaming.com)
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 ## Credits
**Developed by:** Firefrost Gaming / Frostystyle **Developed by:** Firefrost Gaming / Frostystyle
**Contact:** dev@firefrostgaming.com **Architecture Review:** Gemini AI
**Website:** https://firefrostgaming.com
**Part of Firefrost Gaming** **Part of Firefrost Gaming**
*Fire + Frost + Foundation = Where Love Builds Legacy* 🔥❄️💙 *Fire + Frost + Foundation = Where Love Builds Legacy* 🔥❄️💙

View File

@@ -92,32 +92,26 @@
</div> </div>
</div> </div>
<!-- Check Interval --> <!-- Check Interval (PRO TIER) -->
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<div class="box box-info"> <div class="box box-info">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title"> <h3 class="box-title">
<i class="fa fa-clock-o"></i> Check Interval <i class="fa fa-clock-o"></i> Check Interval
<span class="label label-warning" style="margin-left: 10px;">PRO TIER</span>
</h3> </h3>
<span class="label label-warning" style="margin-left: 10px;">Professional</span>
</div> </div>
<div class="box-body"> <div class="box-body">
<div class="form-group"> <div class="form-group">
<label class="control-label">Automatic Check Frequency</label> <label class="control-label">Automatic Check Frequency</label>
<select class="form-control" name="check_interval" id="check_interval"> <select class="form-control" name="check_interval" id="check_interval" disabled>
<option value="daily" @if($check_interval == 'daily') selected @endif> <option value="daily" selected>Daily (24 Hours)</option>
Daily (Recommended) <option value="12h">Every 12 Hours</option>
</option> <option value="6h">Every 6 Hours</option>
<option value="12h" @if($check_interval == '12h') selected @endif>
Every 12 Hours
</option>
<option value="6h" @if($check_interval == '6h') selected @endif>
Every 6 Hours
</option>
</select> </select>
<p class="text-muted small" style="margin-top: 8px;"> <p class="text-muted small" style="margin-top: 8px;">
How often to automatically check for modpack updates. Standard tier is locked to daily cron checks.
More frequent checks use more API quota. Upgrade to Professional for more frequent automated checks.
</p> </p>
</div> </div>
</div> </div>
@@ -126,14 +120,14 @@
</div> </div>
<div class="row"> <div class="row">
<!-- Discord Webhook --> <!-- Discord Webhook (PRO TIER) -->
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<div class="box box-success"> <div class="box box-success">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title"> <h3 class="box-title">
<i class="fa fa-bell"></i> Discord Notifications <i class="fa fa-bell"></i> Discord Notifications
<span class="label label-warning" style="margin-left: 10px;">PRO TIER</span>
</h3> </h3>
<span class="label label-warning" style="margin-left: 10px;">Professional</span>
</div> </div>
<div class="box-body"> <div class="box-body">
<div class="form-group"> <div class="form-group">
@@ -145,10 +139,10 @@
value="{{ $discord_webhook_url }}" value="{{ $discord_webhook_url }}"
placeholder="https://discord.com/api/webhooks/..." placeholder="https://discord.com/api/webhooks/..."
class="form-control" class="form-control"
disabled
/> />
<p class="text-muted small" style="margin-top: 8px;"> <p class="text-muted small" style="margin-top: 8px;">
Receive alerts when modpack updates are available. Upgrade to Professional to receive automated update alerts in your Discord server.
Create a webhook in your Discord server settings.
</p> </p>
</div> </div>
</div> </div>
@@ -205,4 +199,16 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Support -->
<div class="row">
<div class="col-xs-12">
<div class="callout callout-warning">
<h4><i class="fa fa-life-ring"></i> Need Help?</h4>
<p style="margin-bottom: 0;">
Join our Discord for support: <a href="https://discord.firefrostgaming.com" target="_blank">discord.firefrostgaming.com</a>
</p>
</div>
</div>
</div>
</form> </form>

View File

@@ -11,307 +11,113 @@
* - 🟠 Fire (#FF6B35): Update available for this modpack * - 🟠 Fire (#FF6B35): Update available for this modpack
* - No dot: Server has no modpack configured or not yet checked * - No dot: Server has no modpack configured or not yet checked
* *
* Colors match Firefrost Gaming brand palette. * CACHING:
* * Uses a global cache with 60-second TTL to prevent excessive API calls
* CRITICAL ARCHITECTURE DECISION (Gemini-approved): * while ensuring reasonably fresh data during navigation.
* 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 * @package ModpackChecker Blueprint Extension
* @author Firefrost Gaming / Frostystyle <dev@firefrostgaming.com> * @author Firefrost Gaming / Frostystyle <dev@firefrostgaming.com>
* @version 1.0.0 * @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';
// =============================================================================
// TYPE DEFINITIONS
// =============================================================================
/**
* 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 with TTL support
// 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;
let cacheTimestamp: number = 0;
/** Promise for in-flight fetch. Prevents duplicate requests. */
let fetchPromise: Promise<StatusCache> | null = null; let fetchPromise: Promise<StatusCache> | null = null;
const CACHE_TTL_MS = 60000; // 60 seconds
/** /**
* Fetch all server statuses from the backend. * Fetch all server statuses with 60-second TTL caching.
*
* 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> => {
// FAST PATH: Return cached data if available const now = Date.now();
if (globalCache !== null) {
// Return cached data if it exists AND is less than 60 seconds old
if (globalCache !== null && (now - cacheTimestamp < CACHE_TTL_MS)) {
return globalCache; return globalCache;
} }
// DEDUP PATH: If a fetch is already in progress, wait for it // 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;
} }
// FETCH PATH: Start a new API request // Start new fetch
// 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 || {};
cacheTimestamp = Date.now();
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]); // Re-run if serverUuid changes (unlikely in practice) }, [serverUuid]);
// ========================================================================= // Don't render while loading or if no status data
// RENDER CONDITIONS if (loading || !status || !status.modpack_name) {
// =========================================================================
// 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;
} }
// 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) {
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%', // Perfect circle borderRadius: '50%',
marginLeft: '8px', // Space from server name marginLeft: '8px',
// Color based on update status
backgroundColor: status.update_available ? '#FF6B35' : '#4ECDC4', 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)' // Fire glow ? '0 0 4px rgba(255, 107, 53, 0.5)'
: '0 0 4px rgba(78, 205, 196, 0.5)', // Frost glow : '0 0 4px rgba(78, 205, 196, 0.5)',
}; };
/**
* 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} // Native browser tooltip title={tooltipText}
aria-label={tooltipText} // Accessibility for screen readers aria-label={tooltipText}
/> />
); );
}; };

View File

@@ -24,14 +24,23 @@ const ModpackVersionCard: React.FC = () => {
setStatus('loading'); setStatus('loading');
try { try {
const response = await http.post(`/api/client/extensions/modpackchecker/servers/${uuid}/ext/modpackchecker/check`); // Updated to match Batch 1 route optimization
const response = await http.post(`/api/client/extensions/modpackchecker/servers/${uuid}/check`);
setData(response.data); setData(response.data);
setStatus(response.data.success ? 'success' : 'error'); setStatus(response.data.success ? 'success' : 'error');
} catch (error: any) { } catch (error: any) {
setData({ // Handle 429 Rate Limit responses with user-friendly message
success: false, if (error.response?.status === 429) {
error: error.response?.data?.message || 'Failed to check for updates', setData({
}); success: false,
error: 'Rate limit reached. Please wait 60 seconds.',
});
} else {
setData({
success: false,
error: error.response?.data?.message || 'Failed to check for updates',
});
}
setStatus('error'); setStatus('error');
} }
}; };