MAJOR SESSION ACCOMPLISHMENTS (Chronicler #62, April 5, 2026): Task #26 — ModpackChecker Extension: - Full Dev Panel deployed (64.50.188.128) - Pterodactyl 1.12.2 + Wings Local Node + Blueprint beta-2026-01 - Extension scaffolded with real structure verified - 4-platform support from Day 1 (CurseForge, Modrinth, Technic, FTB) - FTB now accessible via modpacks.ch API (game changer) - Pricing locked: $14.99 Standard / $24.99 Professional - Gemini consultation: 20+ gaps filled with concrete details Implementation Guide includes: - Real Blueprint file structure (verified from scaffold) - All 4 API endpoints with response schemas - Database schema (modpack_settings, server_modpack_data) - Admin controller boilerplate (Blade) - Client API controller boilerplate (PHP) - React component boilerplate (TSX) - Detection strategy (Egg Variables → File Fingerprinting → Manual) - Error handling patterns - Cron job chunking for rate limit protection Other session work: - Complete task audit (#2-94) - BACKLOG.md full rewrite (30 active tasks) - Fabric egg fix (eclipse-temurin:21-jdk-jammy) - Gemini consultation procedure updated with template TODO before Phase 2: - Create dev@firefrostgaming.com in Mailcow - Begin admin settings UI (Blade) - Implement Modrinth API first (easiest) Signed-off-by: Claude (Chronicler #62) <claude@firefrostgaming.com>
17 KiB
17 KiB
ModpackChecker Blueprint Extension — Implementation Guide
Document ID: FFG-TASK-026-IMPL
Created: April 5, 2026
Created By: Chronicler #62 with Gemini consultation
Status: VERIFIED — Based on real Blueprint scaffolding
Dev Panel: 64.50.188.128
Overview
This guide contains the verified, real-world implementation details for the ModpackChecker Blueprint extension. Unlike theoretical documentation, this is based on:
- Actual Blueprint beta-2026-01 scaffolding output
- Real Pterodactyl 1.12.2 installation
- Gemini architectural consultation (April 5, 2026)
1. Product Specification
| Attribute | Value |
|---|---|
| Name | ModpackChecker |
| Identifier | modpackchecker |
| Target | Pterodactyl 1.12.x |
| Blueprint | beta-2026-01 |
| Pricing | $14.99 Standard / $24.99 Professional |
| Distribution | BuiltByBit (two separate listings) |
Platform Support (Day 1)
| Platform | API Endpoint | Auth |
|---|---|---|
| CurseForge | https://api.curseforge.com/v1/mods/{modId} |
BYOK (x-api-key) |
| Modrinth | https://api.modrinth.com/v2/project/{id}/version |
None |
| Technic | https://api.technicpack.net/modpack/{slug}?build=1 |
None |
| FTB | https://api.modpacks.ch/public/modpack/{id} |
None |
Tier Features
| Feature | Standard | Professional |
|---|---|---|
| 4-Platform Support | ✅ | ✅ |
| Manual Checks | ✅ | ✅ |
| Egg Variable Caching | ✅ | ✅ |
| Automated Cron Checks | ❌ | ✅ |
| Discord Webhook Alerts | ❌ | ✅ |
| Global Dashboard | ❌ | ✅ |
2. Dev Environment
Dev Panel Credentials:
| Item | Value |
|---|---|
| URL | http://64.50.188.128 |
| Admin User | frostystyle |
| Admin Password | FFG-Dev-2026! |
| DB Name | panel |
| DB User | pterodactyl |
| DB Password | FFG-Dev-Panel-2026! |
| Node | Dev-Local (127.0.0.1) |
| Wings Port | 8080 |
| SFTP Port | 2022 |
| Allocations | 127.0.0.1:25500-25510 |
3. Blueprint Extension Structure
Location: /var/www/pterodactyl/.blueprint/dev/
modpackchecker/
├── conf.yml # Extension manifest
├── admin/
│ ├── controller.php # Admin settings controller (Blade)
│ └── view.blade.php # Admin settings UI
├── controllers/
│ └── ModpackAPIController.php # Client API controller
├── routes/
│ ├── admin.php # Admin routes
│ └── client.php # Client API routes
├── views/
│ └── server/
│ └── wrapper.tsx # Per-server React component
├── database/
│ └── migrations/
│ └── 2026_04_05_000000_create_modpack_settings.php
└── .dist/ # Build output (auto-generated)
4. conf.yml (Extension Manifest)
info:
name: "ModpackChecker"
identifier: "modpackchecker"
description: "4-platform modpack version checker for Pterodactyl - supports CurseForge, Modrinth, Technic, and FTB"
flags: ""
version: "1.0.0"
target: "beta-2026-01"
author: "Firefrost Gaming <dev@firefrostgaming.com>"
icon: ""
website: "https://firefrostgaming.com"
admin:
view: "admin/view.blade.php"
controller: "admin/controller.php"
css: ""
wrapper: ""
dashboard:
css: ""
wrapper: "views/server/wrapper.tsx"
components: ""
data:
directory: "modpackchecker"
public: ""
console: ""
requests:
views: "views"
app: ""
routers:
application: ""
client: "routes/client.php"
web: ""
database:
migrations: "database/migrations"
5. Database Schema
modpack_settings (Global Config)
// database/migrations/2026_04_05_000000_create_modpack_settings.php
Schema::create('modpack_settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->text('value')->nullable();
$table->timestamps();
});
// Default records:
// - curseforge_api_key (BYOK)
// - discord_webhook_url (Professional)
// - check_interval (daily/12h/6h)
// - tier (standard/professional)
server_modpack_data (Per-Server)
Schema::create('server_modpack_data', function (Blueprint $table) {
$table->id();
$table->string('server_uuid')->unique();
$table->string('platform')->nullable(); // curseforge, modrinth, technic, ftb
$table->string('modpack_id')->nullable();
$table->string('modpack_name')->nullable();
$table->string('current_version')->nullable();
$table->string('latest_version')->nullable();
$table->enum('status', ['up_to_date', 'update_available', 'error', 'unknown'])->default('unknown');
$table->timestamp('last_checked')->nullable();
$table->text('error_message')->nullable();
$table->timestamps();
});
6. Backend Controllers
Admin Controller (Blade)
Location: admin/controller.php
<?php
namespace Pterodactyl\Http\Controllers\Admin\Extensions\modpackchecker;
use Illuminate\View\View;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary;
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
use Illuminate\Http\RedirectResponse;
class modpackcheckerExtensionController extends Controller
{
public function __construct(
private ViewFactory $view,
private BlueprintExtensionLibrary $blueprint,
) {}
public function index(): View
{
// Database access via Blueprint helper
$curseforge_key = $this->blueprint->dbGet('modpackchecker', 'curseforge_api_key');
$webhook_url = $this->blueprint->dbGet('modpackchecker', 'discord_webhook_url');
$check_interval = $this->blueprint->dbGet('modpackchecker', 'check_interval') ?: 'daily';
return $this->view->make(
'admin.extensions.modpackchecker.index', [
'curseforge_key' => $curseforge_key,
'webhook_url' => $webhook_url,
'check_interval' => $check_interval,
'root' => '/admin/extensions/modpackchecker',
'blueprint' => $this->blueprint,
]
);
}
public function update(modpackcheckerSettingsFormRequest $request): RedirectResponse
{
$this->blueprint->dbSet('modpackchecker', 'curseforge_api_key', $request->input('curseforge_api_key'));
$this->blueprint->dbSet('modpackchecker', 'discord_webhook_url', $request->input('discord_webhook_url'));
$this->blueprint->dbSet('modpackchecker', 'check_interval', $request->input('check_interval'));
return redirect()->route('admin.extensions.modpackchecker.index');
}
}
class modpackcheckerSettingsFormRequest extends AdminFormRequest
{
public function rules(): array
{
return [
'curseforge_api_key' => 'nullable|string|max:500',
'discord_webhook_url' => 'nullable|url|max:500',
'check_interval' => 'required|in:daily,12h,6h',
];
}
}
Client API Controller
Location: controllers/ModpackAPIController.php
<?php
namespace Pterodactyl\BlueprintFramework\Extensions\modpackchecker\controllers;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Models\Server;
use Pterodactyl\Repositories\Wings\DaemonFileRepository;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class ModpackAPIController extends Controller
{
public function __construct(
private DaemonFileRepository $fileRepository
) {}
/**
* Manual version check triggered from React frontend
*/
public function manualCheck(Request $request, Server $server): JsonResponse
{
// 1. Try Egg Variables first (most reliable)
$platform = $this->getEggVariable($server, 'MODPACK_PLATFORM');
$modpackId = $this->getEggVariable($server, 'MODPACK_ID');
// 2. Fallback to file fingerprinting if not set
if (empty($platform) || empty($modpackId)) {
$detected = $this->detectFromFiles($server);
$platform = $detected['platform'] ?? null;
$modpackId = $detected['id'] ?? null;
}
// 3. If still nothing, return unknown
if (empty($platform) || empty($modpackId)) {
return response()->json([
'success' => false,
'status' => 'unknown',
'message' => 'Could not detect modpack. Please set platform and ID manually.',
]);
}
// 4. Query the appropriate API
$versionData = $this->checkVersion($platform, $modpackId);
return response()->json([
'success' => true,
'server_uuid' => $server->uuid,
'platform' => $platform,
'modpack_id' => $modpackId,
'current_version' => $versionData['current'] ?? 'Unknown',
'latest_version' => $versionData['latest'] ?? 'Unknown',
'status' => $versionData['status'] ?? 'unknown',
]);
}
private function getEggVariable(Server $server, string $name): ?string
{
$variable = $server->variables()->where('env_variable', $name)->first();
return $variable?->server_value;
}
private function detectFromFiles(Server $server): array
{
try {
// Try CurseForge manifest.json
$content = $this->fileRepository->setServer($server)->getContent('/manifest.json');
$json = json_decode($content, true);
if (isset($json['projectID'])) {
return ['platform' => 'curseforge', 'id' => $json['projectID']];
}
} catch (\Exception $e) {
// File doesn't exist, try next
}
try {
// Try Modrinth modrinth.index.json
$content = $this->fileRepository->setServer($server)->getContent('/modrinth.index.json');
$json = json_decode($content, true);
if (isset($json['dependencies'])) {
// Modrinth detection logic
return ['platform' => 'modrinth', 'id' => null]; // Needs manual ID
}
} catch (\Exception $e) {
// File doesn't exist
}
return [];
}
private function checkVersion(string $platform, string $modpackId): array
{
// Dispatch to appropriate provider
return match($platform) {
'curseforge' => $this->checkCurseForge($modpackId),
'modrinth' => $this->checkModrinth($modpackId),
'technic' => $this->checkTechnic($modpackId),
'ftb' => $this->checkFTB($modpackId),
default => ['status' => 'error', 'message' => 'Unknown platform'],
};
}
private function checkCurseForge(string $modpackId): array
{
// Implementation with BYOK API key
// Header: x-api-key: {key}
// Endpoint: https://api.curseforge.com/v1/mods/{modId}
return ['status' => 'unknown']; // TODO
}
private function checkModrinth(string $modpackId): array
{
// Endpoint: https://api.modrinth.com/v2/project/{id}/version
return ['status' => 'unknown']; // TODO
}
private function checkTechnic(string $modpackId): array
{
// Endpoint: https://api.technicpack.net/modpack/{slug}?build=1
return ['status' => 'unknown']; // TODO
}
private function checkFTB(string $modpackId): array
{
// Endpoint: https://api.modpacks.ch/public/modpack/{id}
return ['status' => 'unknown']; // TODO
}
}
7. Client Routes
Location: routes/client.php
<?php
use Illuminate\Support\Facades\Route;
use Pterodactyl\BlueprintFramework\Extensions\modpackchecker\controllers\ModpackAPIController;
Route::post('/servers/{server}/ext/modpackchecker/check', [ModpackAPIController::class, 'manualCheck']);
8. React Component (Per-Server View)
Location: views/server/wrapper.tsx
import React, { useState } from 'react';
import { ServerContext } from '@/state/server';
import http from '@/api/http';
const ModpackVersionCard: React.FC = () => {
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [data, setData] = useState<any>(null);
const checkForUpdates = async () => {
if (!uuid) return;
setStatus('loading');
try {
const response = await http.post(`/api/client/servers/${uuid}/ext/modpackchecker/check`);
setData(response.data);
setStatus('success');
} catch (error) {
setStatus('error');
}
};
return (
<div className="bg-gray-700 rounded p-4 mb-4">
<h3 className="text-lg font-semibold mb-2">Modpack Version</h3>
{status === 'idle' && (
<button
onClick={checkForUpdates}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
>
Check for Updates
</button>
)}
{status === 'loading' && (
<p className="text-gray-400">Checking...</p>
)}
{status === 'success' && data && (
<div>
<p><strong>Platform:</strong> {data.platform}</p>
<p><strong>Current:</strong> {data.current_version}</p>
<p><strong>Latest:</strong> {data.latest_version}</p>
<p className={data.status === 'update_available' ? 'text-yellow-400' : 'text-green-400'}>
{data.status === 'update_available' ? '⚠️ Update Available' : '✅ Up to Date'}
</p>
</div>
)}
{status === 'error' && (
<p className="text-red-400">Error checking version</p>
)}
</div>
);
};
export default ModpackVersionCard;
9. API Response Schemas
CurseForge
{
"data": {
"id": 123456,
"name": "All the Mods 9",
"latestFiles": [
{
"id": 9876543,
"displayName": "All the Mods 9 - 0.2.51",
"releaseType": 1
}
]
}
}
Modrinth
[
{
"id": "IIxjBpdz",
"project_id": "P7dR8mSH",
"name": "Fabulously Optimized 5.12.0",
"version_number": "5.12.0",
"version_type": "release"
}
]
Technic
{
"name": "tekkitmain",
"version": "1.2.9k",
"minecraft": "1.6.4"
}
FTB (modpacks.ch)
{
"name": "FTB Omnia",
"versions": [
{
"id": 45,
"name": "1.0.1",
"type": "Release",
"updated": 1585968036,
"targets": [
{"name": "minecraft", "version": "1.15.2"},
{"name": "forge", "version": "31.1.35"}
]
}
]
}
10. Detection Strategy
Priority Order:
- Egg Variables (most reliable) — Check
MODPACK_PLATFORMandMODPACK_ID - File Fingerprinting (fallback) — Read manifest files via DaemonFileRepository
- Manual Override (last resort) — User sets in admin UI
Egg Variable Names:
CURSEFORGE_IDMODRINTH_PROJECT_IDFTB_MODPACK_IDTECHNIC_SLUG
11. Error Handling
| Error | HTTP Code | User Message |
|---|---|---|
| Pack delisted | 404 | "Pack not found or delisted from platform." |
| Invalid BYOK | 401/403 | "Invalid CurseForge API Key. Check Extension Settings." |
| No versions | 200 (empty) | "No release versions found." |
| Timeout | 500 | Show last known version with ⚠️ warning |
12. Cron Job (Professional Only)
// In Laravel scheduler
Server::chunk(20, function ($servers) {
foreach ($servers as $server) {
// Check version via Egg Variables only (NOT file reads)
}
sleep(2); // Rate limit protection
});
Frequency options: Daily (default), Every 12 Hours, Every 6 Hours
13. Build & Deploy Commands
# Build extension
cd /var/www/pterodactyl
blueprint -build
# Export for distribution
blueprint -export
# Install on customer panel
blueprint -install modpackchecker
php artisan migrate
14. Documentation Files (for BuiltByBit)
README.md— Installation, BYOK guide, webhook setupCHANGELOG.md— Version historyLICENSE— Commercial license
15. TODO Before Production
- Create
dev@firefrostgaming.comin Mailcow - Implement all 4 API providers
- Build admin settings UI (Blade)
- Build per-server component (React)
- Implement Discord webhook
- Implement cron scheduler
- Build global dashboard (Professional)
- Test on Dev Panel
- Create BuiltByBit listings
- Write user documentation
Fire + Frost + Foundation = Where Love Builds Legacy 💙🔥❄️