# 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)

```yaml
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)

```php
// 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)

```php
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
<?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
<?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
<?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`

```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
```json
{
  "data": {
    "id": 123456,
    "name": "All the Mods 9",
    "latestFiles": [
      {
        "id": 9876543,
        "displayName": "All the Mods 9 - 0.2.51",
        "releaseType": 1
      }
    ]
  }
}
```

### Modrinth
```json
[
  {
    "id": "IIxjBpdz",
    "project_id": "P7dR8mSH",
    "name": "Fabulously Optimized 5.12.0",
    "version_number": "5.12.0",
    "version_type": "release"
  }
]
```

### Technic
```json
{
  "name": "tekkitmain",
  "version": "1.2.9k",
  "minecraft": "1.6.4"
}
```

### FTB (modpacks.ch)
```json
{
  "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)

```php
// 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

```bash
# 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** 💙🔥❄️
