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:
@@ -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* 🔥❄️💙
|
||||
@@ -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* 🔥❄️💙
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
// Don't render while loading or if no status data
|
||||
if (loading || !status || !status.modpack_name) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user