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

330 lines
8.5 KiB
Markdown

# 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