Files
firefrost-operations-manual/docs/tasks/arbiter-2x/IMPLEMENTATION-GUIDE.md
Claude (Chronicler #35) c47d22fc41 docs: Add Arbiter 2.x task directory and Gemini consultation records
WHAT WAS DONE:
- Created docs/tasks/arbiter-2x/ with README and implementation guide
- Created docs/reference/gemini-consultations/ for AI partner records
- Documented complete Arbiter 2.x architecture and implementation plan

FILES ADDED:
- docs/tasks/arbiter-2x/README.md (overview, phases, gotchas)
- docs/tasks/arbiter-2x/IMPLEMENTATION-GUIDE.md (complete technical spec)
- docs/reference/gemini-consultations/2026-03-31-arbiter-whitelist-architecture.md
- docs/reference/gemini-consultations/2026-03-31-arbiter-implementation-details.md

GEMINI CONSULTATIONS:
Preserved complete Gemini AI architectural consultation from March 31, 2026.
Includes:
- Initial architecture consultation (unified app vs microservices)
- Database schema design (PostgreSQL with indexes)
- Minecraft account linking flow (Discord /link command)
- Pterodactyl API integration (File Management API)
- Complete code examples (Mojang validation, file write, cron sync)

IMPLEMENTATION GUIDE INCLUDES:
- 5-phase implementation plan with checklists
- PostgreSQL schema with indexes for 500-user scale
- Production-ready code snippets (pg pool, Mojang API, Panel API)
- Critical gotchas (Content-Type, UUID dashes, HTTP 412)
- Hourly cron reconciliation logic
- Error handling and rate limiting strategies

WHY:
Task #90 is Tier 1 soft launch blocker. This documentation provides
complete blueprint for implementing subscription-driven whitelist
system. All architectural decisions validated by Gemini AI.

NEXT STEPS:
- Phase 1: PostgreSQL database setup
- Phase 2: Core functions (Mojang, Panel API)
- Phase 3: Discord /link command
- Phase 4: Sync system (event-driven + cron)
- Phase 5: Admin panel and testing

Signed-off-by: The Golden Chronicler <claude@firefrostgaming.com>
2026-03-31 22:47:49 +00:00

8.5 KiB

Gemini's Arbiter 2.x Implementation Guide

Complete Technical Specification

Date: 2026-03-31 Source: Gemini AI Architectural Consultation Status: READY TO IMPLEMENT


1. Pterodactyl File Management API

Writing whitelist.json

Endpoint: POST /api/client/servers/{server_identifier}/files/write?file=whitelist.json

Headers:

  • Authorization: Bearer <Client_API_Key>
  • Accept: application/json
  • Content-Type: text/plainCRITICAL: Must be text/plain, not application/json!

Body: Raw stringified JSON array (not wrapped in JSON object)

const fileContent = JSON.stringify(whitelistArray, null, 2);
// Send as raw text body

Triggering Reload Command

Endpoint: POST /api/client/servers/{server_identifier}/command

Headers:

  • Authorization: Bearer <Client_API_Key>
  • Content-Type: application/json
  • Accept: application/json

Body:

{"command": "whitelist reload"}

Error Handling - CRITICAL!

HTTP 412 (Server Offline) is EXPECTED and SAFE:

  • File write succeeds even if server offline
  • Command fails with 412 = server will use new file on next boot
  • DO NOT treat this as an error!

Rate Limiting:

  • Do NOT use Promise.all() for bulk syncs
  • Process servers sequentially or in small batches
  • Prevents overwhelming Panel API

2. Auto-Discovery - Application API

Use Application API, not Client API for discovery!

Endpoint: GET /api/application/servers?include=allocations,node,nest

Headers: Authorization: Bearer <Application_API_Key>

Filtering Logic:

  1. Check attributes.nest.attributes.name === "Minecraft"
  2. Check attributes.node.attributes.name for physical location (TX1/NC1)
  3. Extract identifier (8-char string for Client API calls)

Exclude servers:

  • Filter out by nest type (FoundryVTT is different nest)
  • OR check server name against EXCLUDED_SERVERS list

3. Whitelist JSON Format - EXACT SPECIFICATION

CRITICAL: Minecraft requires UUIDs WITH DASHES since 1.8+

Correct format:

[
  {
    "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5",
    "name": "Notch"
  },
  {
    "uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "name": "Steve"
  }
]

Why UUIDs matter:

  • Username-only format forces server to query Mojang on boot
  • Slows startup
  • Can fail if Mojang API rate-limits server IP
  • UUIDs are permanent, usernames can change

GOTCHA: Mojang API returns UUIDs WITHOUT dashes - must format before saving!


4. Database Schema Refinements

Add Indexes for 500-User Scale

CREATE INDEX idx_users_discord_id ON users(discord_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);

New Table: Sync Tracking

CREATE TABLE server_sync_log (
    server_identifier VARCHAR(50) PRIMARY KEY,
    last_successful_sync TIMESTAMP,
    last_error TEXT,
    is_online BOOLEAN DEFAULT true
);

Purpose: Debug "Why isn't Steve whitelisted on Dallas node?"


5. Code Examples - Production Ready

A. PostgreSQL Connection (using pg pool)

// database.js
const { Pool } = require('pg');

const pool = new Pool({
  user: process.env.DB_USER,
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  password: process.env.DB_PASSWORD,
  port: process.env.DB_PORT,
  max: 20, // Max connections for 500 user scale
  idleTimeoutMillis: 30000
});

module.exports = {
  query: (text, params) => pool.query(text, params),
};

B. Mojang API Validation + UUID Formatting

// mojang.js

function formatUUID(uuidStr) {
  // Converts "069a79f444e94726a5befca90e38aaf5" 
  // to "069a79f4-44e9-4726-a5be-fca90e38aaf5"
  return `${uuidStr.slice(0, 8)}-${uuidStr.slice(8, 12)}-${uuidStr.slice(12, 16)}-${uuidStr.slice(16, 20)}-${uuidStr.slice(20)}`;
}

async function validateMinecraftUser(username) {
  try {
    const res = await fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`);
    
    // 204 or 404 = user not found
    if (res.status === 204 || res.status === 404) return null;
    
    if (!res.ok) throw new Error(`Mojang API error: ${res.status}`);
    
    const data = await res.json();
    return {
      name: data.name, // Correct capitalization from Mojang
      uuid: formatUUID(data.id) // ADD DASHES!
    };
  } catch (error) {
    console.error("Failed to validate Minecraft user:", error);
    return null;
  }
}

C. Pterodactyl File Write

// panel.js

async function writeWhitelistFile(serverIdentifier, whitelistArray) {
  const fileContent = JSON.stringify(whitelistArray, null, 2);
  const endpoint = `${process.env.PANEL_URL}/api/client/servers/${serverIdentifier}/files/write?file=whitelist.json`;

  const res = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.PANEL_CLIENT_KEY}`,
      'Accept': 'application/json',
      'Content-Type': 'text/plain' // MUST be text/plain!
    },
    body: fileContent
  });

  if (!res.ok) throw new Error(`Failed to write file: ${res.statusText}`);
  return true;
}

D. Pterodactyl Reload Command (Safe Fail)

// panel.js (continued)

async function reloadWhitelistCommand(serverIdentifier) {
  const endpoint = `${process.env.PANEL_URL}/api/client/servers/${serverIdentifier}/command`;

  const res = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.PANEL_CLIENT_KEY}`,
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    body: JSON.stringify({ command: "whitelist reload" })
  });

  // 412 = server offline, this is FINE!
  if (res.status === 412) {
    console.log(`[${serverIdentifier}] is offline. File saved for next boot.`);
    return true; 
  }

  if (!res.ok) throw new Error(`Command failed: ${res.statusText}`);
  return true;
}

E. Hourly Cron Reconciliation

// sync.js
const cron = require('node-cron');
const db = require('./database');
const panel = require('./panel');

// Runs at minute 0 past every hour (1:00, 2:00, 3:00, etc.)
cron.schedule('0 * * * *', async () => {
  console.log("Starting hourly whitelist reconciliation...");
  
  // 1. Fetch Master Whitelist from Database
  const { rows: players } = await db.query(
    `SELECT minecraft_username as name, minecraft_uuid as uuid 
     FROM users 
     JOIN subscriptions ON users.discord_id = subscriptions.discord_id 
     WHERE subscriptions.status = 'active'`
  );

  // 2. Fetch Active Servers via Auto-Discovery
  const servers = await panel.getServers(); 

  // 3. Sync Sequentially (prevents rate limiting)
  for (const server of servers) {
    try {
      await panel.writeWhitelistFile(server.identifier, players);
      await panel.reloadWhitelistCommand(server.identifier);
      
      // Log successful sync
      await db.query(
        "UPDATE server_sync_log SET last_successful_sync = NOW() WHERE server_identifier = $1",
        [server.identifier]
      );
    } catch (err) {
      console.error(`Sync failed for ${server.identifier}:`, err);
      
      // Log failed sync
      await db.query(
        "UPDATE server_sync_log SET last_error = $1 WHERE server_identifier = $2",
        [err.message, server.identifier]
      );
    }
  }
  
  console.log("Hourly reconciliation complete.");
});

Key Gotchas & Warnings

  1. Content-Type MUST be text/plain for file write endpoint (not application/json)
  2. UUIDs MUST include dashes in whitelist.json (Mojang returns without dashes)
  3. HTTP 412 is not an error - server offline, file saved for next boot
  4. Sequential processing prevents rate limiting (don't use Promise.all)
  5. Application API for discovery, Client API for file operations
  6. Mojang API can rate-limit - validate once during /link, store permanently

Implementation Order

Phase 1: Database Migration

  1. Set up PostgreSQL
  2. Create tables with indexes
  3. Migrate existing Arbiter data

Phase 2: Core Functions

  1. Database connection pool
  2. Mojang validation
  3. Pterodactyl file write/command functions

Phase 3: Discord Integration

  1. /link slash command
  2. Auto-DM new subscribers
  3. Role assignment webhook handler

Phase 4: Sync System

  1. Event-driven sync (on /link, on subscribe)
  2. Hourly cron reconciliation
  3. Sync logging for debugging

Phase 5: Admin Panel

  1. View sync status
  2. Manual sync trigger
  3. View linked accounts

Fire + Frost + Foundation = Where Love Builds Legacy 💙🔥❄️

Implementation Status: READY TO BUILD Next Step: Create Task #90 - Arbiter 2.x Implementation