# 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/plain` ← **CRITICAL: Must be text/plain, not application/json!**

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

```javascript
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:**
```json
{"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:**
```json
[
  {
    "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

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

### New Table: Sync Tracking

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

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

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

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

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

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