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
**Version:** 1.0.0
**Author:** Firefrost Gaming
**License:** Proprietary (Commercial product for BuiltByBit)
**Author:** Firefrost Gaming / Frostystyle
**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.
@@ -19,7 +19,7 @@ A Pterodactyl Panel extension that checks modpack versions across CurseForge, Mo
7. [Development](#development)
8. [API Reference](#api-reference)
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
- **🟢 Teal (Frost #4ECDC4):** Up to date
- 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
- "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
- Rate limited: 2 checks per minute per server
- Shows modpack name and latest version
### Admin Panel
- Configure CurseForge API key
- View extension status
- (Future: Rate limit settings, notification preferences)
- View supported platforms
- PRO features: Discord notifications, custom check intervals
### Supported Platforms
| 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 |
| 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
@@ -59,17 +58,11 @@ A Pterodactyl Panel extension that checks modpack versions across CurseForge, Mo
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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 │
└──────────────────┬──────────────────┘
@@ -77,10 +70,10 @@ A Pterodactyl Panel extension that checks modpack versions across CurseForge, Mo
│ DATABASE CACHE │
│ modpackchecker_servers table │
│ │
│ • server_id, server_uuid
│ • server_uuid
│ • platform, modpack_id │
│ • current_version, latest_version │
│ • update_available (boolean)
│ • status (string)
│ • last_checked timestamp │
└──────────────────┬──────────────────┘
@@ -92,116 +85,76 @@ A Pterodactyl Panel extension that checks modpack versions across CurseForge, Mo
│ (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 │
│ • 60-second TTL │ │ • LIVE API call │
│ • Shows 🟠 or 🟢 dot │ │ • Rate limited (2/min)
└───────────────────────────┘ └───────────────────────────┘
```
### 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)
├── icon.png # Extension icon (128x128, Gemini-designed)
├── README.md
├── CHANGELOG.md
├── conf.yml
├── build.sh
├── icon.png
├── app/ # Merges into Pterodactyl's app/ via requests.app
│ ├── Console/
│ │ └── Commands/
│ └── CheckModpackUpdates.php # Laravel cron command
│ └── Http/
└── Controllers/
└── ModpackAPIController.php # API endpoints (manualCheck, getStatus)
├── app/
│ ├── Console/Commands/
│ │ └── CheckModpackUpdates.php
├── Http/Controllers/
│ └── ModpackAPIController.php
└── Services/
│ └── ModpackApiService.php
├── admin/
│ ├── controller.php # Admin panel logic
│ └── view.blade.php # Admin panel UI
│ ├── controller.php
│ └── view.blade.php
├── database/
│ └── migrations/
│ └── 2026_04_06_000000_create_modpackchecker_servers_table.php
├── database/migrations/
│ └── 2026_04_06_000000_create_modpackchecker_servers_table.php
├── routes/
│ └── client.php # API route definitions
│ └── client.php
└── views/
├── server/
│ └── wrapper.tsx # Console "Check for Updates" widget
└── dashboard/
└── UpdateBadge.tsx # Dashboard status dot component
├── server/wrapper.tsx
└── dashboard/UpdateBadge.tsx
```
### 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
### Prerequisites
- Pterodactyl Panel v1.11+
- Blueprint Framework (beta-2026-01 or newer)
- PHP 8.1+
- Node.js 18+
### Standard Installation (BuiltByBit)
### 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
cp -r blueprint-extension /var/www/pterodactyl/.blueprint/extensions/modpackchecker
chown -R www-data:www-data /var/www/pterodactyl/.blueprint/extensions/modpackchecker
blueprint -install modpackchecker
```
2. **Build the extension:**
```bash
cd /var/www/pterodactyl
blueprint -build
```
3. The framework will automatically inject the frontend components and rebuild the panel assets.
3. **Compile frontend assets:**
4. Set up the cron job for automated checks:
```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
# Add to crontab
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
@@ -218,9 +171,9 @@ For modpack detection, set these variables in your server's egg:
### 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**
3. Enter your API key and save
@@ -229,7 +182,7 @@ CurseForge requires an API key. To configure:
## Usage
### Dashboard Badge
No action needed — badges appear automatically for servers that:
Badges appear automatically for servers that:
- Have `MODPACK_PLATFORM` egg variable set
- 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
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
### Adding a New Platform
1. Open `app/Services/ModpackApiService.php`
2. Add your new platform check method (e.g., `private function checkNewPlatform(string $id): array`)
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
```bash
# 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}"
# Get all statuses (requires auth token)
# Get all statuses
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
### 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:**
```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.
@@ -315,12 +254,6 @@ Get cached status for all servers accessible to the authenticated user.
"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"
}
}
```
@@ -344,36 +277,24 @@ Get cached status for all servers accessible to the authenticated user.
2. Verify controller namespace: `Pterodactyl\Http\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`
### "Rate limit reached" message
The manual check is limited to 2 requests per minute per server. Wait 60 seconds and try again.
---
## Design Decisions
## Support
### 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
**Need help?** Join our Discord for support:
- **Discord:** [discord.firefrostgaming.com](https://discord.firefrostgaming.com)
- **Email:** dev@firefrostgaming.com
- **Website:** [firefrostgaming.com](https://firefrostgaming.com)
---
## Credits
**Developed by:** Firefrost Gaming / Frostystyle
**Contact:** dev@firefrostgaming.com
**Website:** https://firefrostgaming.com
**Architecture Review:** Gemini AI
**Part of Firefrost Gaming**
*Fire + Frost + Foundation = Where Love Builds Legacy* 🔥❄️💙

View File

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

View File

@@ -11,307 +11,113 @@
* - 🟠 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
* CACHING:
* Uses a global cache with 60-second TTL to prevent excessive API calls
* while ensuring reasonably fresh data during navigation.
*
* @package ModpackChecker Blueprint Extension
* @author Firefrost Gaming / Frostystyle <dev@firefrostgaming.com>
* @version 1.0.0
* @see CheckModpackUpdates.php (cron that populates the cache)
* @see ModpackAPIController::getStatus() (backend endpoint)
* =============================================================================
*/
import React, { useEffect, useState } from 'react';
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 {
/** True if latest_version differs from current_version */
update_available: boolean;
/** Human-readable modpack name (e.g., "All The Mods 9") */
modpack_name?: string;
/** Version currently installed on the server */
current_version?: string;
/** Latest version available from the platform */
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 {
[serverUuid: string]: ServerStatus;
}
// =============================================================================
// 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. */
// Global cache with TTL support
let globalCache: StatusCache | null = null;
/** Promise for in-flight fetch. Prevents duplicate requests. */
let cacheTimestamp: number = 0;
let fetchPromise: Promise<StatusCache> | null = null;
const CACHE_TTL_MS = 60000; // 60 seconds
/**
* 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)
* Fetch all server statuses with 60-second TTL caching.
*/
const fetchAllStatuses = async (): Promise<StatusCache> => {
// FAST PATH: Return cached data if available
if (globalCache !== null) {
const now = Date.now();
// Return cached data if it exists AND is less than 60 seconds old
if (globalCache !== null && (now - cacheTimestamp < CACHE_TTL_MS)) {
return globalCache;
}
// DEDUP PATH: If a fetch is already in progress, wait for it
// instead of starting another request
// If a fetch is already in progress, wait for it
if (fetchPromise !== null) {
return fetchPromise;
}
// FETCH PATH: Start a new API request
// This is the only code path that actually makes an HTTP call
// Start new fetch
fetchPromise = http.get('/api/client/extensions/modpackchecker/status')
.then((response) => {
// Store the response data in the global cache
globalCache = response.data || {};
cacheTimestamp = Date.now();
return globalCache;
})
.catch((error) => {
// Log the error for debugging
console.error('ModpackChecker: Failed to fetch status', error);
// Cache empty object to prevent retry spam
// Users can refresh the page to try again
globalCache = {};
return globalCache;
})
.finally(() => {
// Clear the promise reference
// This allows future retries if cache is manually cleared
fetchPromise = null;
});
return fetchPromise;
};
// =============================================================================
// COMPONENT
// =============================================================================
/**
* Props for the UpdateBadge component.
*/
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;
}
/**
* 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 }) => {
// =========================================================================
// STATE
// =========================================================================
/** This specific server's status (extracted from global cache) */
const [status, setStatus] = useState<ServerStatus | null>(null);
/** Loading state - true until we've checked the cache */
const [loading, setLoading] = useState(true);
// =========================================================================
// DATA FETCHING
// =========================================================================
useEffect(() => {
// Fetch from global cache (makes API call only on first badge mount)
fetchAllStatuses()
.then((cache) => {
// Extract this server's status from the cache
// Will be null/undefined if server not in cache
setStatus(cache[serverUuid] || null);
setLoading(false);
});
}, [serverUuid]); // Re-run if serverUuid changes (unlikely in practice)
}, [serverUuid]);
// =========================================================================
// 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) {
// Don't render while loading or if no status data
if (loading || !status || !status.modpack_name) {
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 = {
// Layout
display: 'inline-block',
width: '8px',
height: '8px',
borderRadius: '50%', // Perfect circle
marginLeft: '8px', // Space from server name
// Color based on update status
borderRadius: '50%',
marginLeft: '8px',
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
? '0 0 4px rgba(255, 107, 53, 0.5)' // Fire glow
: '0 0 4px rgba(78, 205, 196, 0.5)', // Frost glow
? '0 0 4px rgba(255, 107, 53, 0.5)'
: '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
? `Update available: ${status.latest_version}`
: `Up to date: ${status.latest_version}`;
// =========================================================================
// RENDER
// =========================================================================
return (
<span
style={dotStyle}
title={tooltipText} // Native browser tooltip
aria-label={tooltipText} // Accessibility for screen readers
title={tooltipText}
aria-label={tooltipText}
/>
);
};

View File

@@ -24,14 +24,23 @@ const ModpackVersionCard: React.FC = () => {
setStatus('loading');
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);
setStatus(response.data.success ? 'success' : 'error');
} catch (error: any) {
setData({
success: false,
error: error.response?.data?.message || 'Failed to check for updates',
});
// Handle 429 Rate Limit responses with user-friendly message
if (error.response?.status === 429) {
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');
}
};