Files
firefrost-operations-manual/docs/tasks/modpack-version-checker/BLUEPRINT-IMPLEMENTATION-GUIDE.md
Claude (Chronicler #62) 2e82ec43df docs: add verified Blueprint implementation guide for Task #26
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>
2026-04-05 22:32:37 +00:00

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:

  1. Egg Variables (most reliable) — Check MODPACK_PLATFORM and MODPACK_ID
  2. File Fingerprinting (fallback) — Read manifest files via DaemonFileRepository
  3. Manual Override (last resort) — User sets in admin UI

Egg Variable Names:

  • CURSEFORGE_ID
  • MODRINTH_PROJECT_ID
  • FTB_MODPACK_ID
  • TECHNIC_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 setup
  • CHANGELOG.md — Version history
  • LICENSE — Commercial license

15. TODO Before Production

  • Create dev@firefrostgaming.com in 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 💙🔥❄️