Files
firefrost-operations-manual/deployments/whitelist-manager/templates/index.html
Claude dd2b57bce6 feat: Whitelist Manager v1.0 - Complete deployment package
- Flask web application for managing Minecraft whitelists
- Manages all 11 game servers (TX1 + NC1)
- TailwindCSS Fire & Frost themed UI
- Single player and bulk operations
- HTTP Basic Auth with password hashing
- Nginx reverse proxy + SSL configuration
- systemd service for auto-start
- Complete deployment automation

Components:
- app.py: Main Flask application with Pterodactyl API integration
- templates/index.html: Responsive web dashboard
- requirements.txt: Python dependencies
- .env: Configuration with API keys and credentials
- deploy.sh: Automated deployment script
- DEPLOYMENT.md: Step-by-step manual deployment guide
- nginx.conf: Reverse proxy configuration
- whitelist-manager.service: systemd service

Target: Ghost VPS (64.50.188.14)
Domain: whitelist.firefrostgaming.com
Login: mkrause612 / Butter2018!!

Transforms 15-minute manual task into 30-second web operation.
Zero-error whitelist management with full visibility.

Ready for deployment - FFG-STD-001 compliant
2026-02-17 03:36:48 +00:00

318 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Whitelist Manager - Firefrost Gaming</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
fire: '#FF4500',
frost: '#00CED1',
}
}
}
}
</script>
</head>
<body class="bg-gray-900 text-gray-100">
<div class="container mx-auto px-4 py-8">
<!-- Header -->
<header class="mb-8">
<h1 class="text-4xl font-bold mb-2">
<span class="text-fire">🔥</span>
Whitelist Manager
<span class="text-frost">❄️</span>
</h1>
<p class="text-gray-400">Firefrost Gaming - Server Whitelist Management</p>
</header>
<!-- Player Management Section -->
<div class="bg-gray-800 rounded-lg p-6 mb-8">
<h2 class="text-2xl font-bold mb-4">Player Management</h2>
<!-- Single Player Operations -->
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Player Username</label>
<input type="text" id="playerName"
class="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 focus:outline-none focus:border-frost"
placeholder="Enter Minecraft username">
</div>
<!-- Bulk Operations -->
<div class="mb-6">
<label class="block text-sm font-medium mb-2">Bulk Operations (one username per line)</label>
<textarea id="bulkPlayers" rows="4"
class="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 focus:outline-none focus:border-frost"
placeholder="username1&#10;username2&#10;username3"></textarea>
</div>
<!-- Action Buttons -->
<div class="flex gap-4 mb-6">
<button onclick="addPlayer()"
class="bg-green-600 hover:bg-green-700 px-6 py-2 rounded font-medium transition">
Add to Whitelist
</button>
<button onclick="removePlayer()"
class="bg-red-600 hover:bg-red-700 px-6 py-2 rounded font-medium transition">
Remove from Whitelist
</button>
<button onclick="bulkAdd()"
class="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded font-medium transition">
Bulk Add
</button>
<button onclick="bulkRemove()"
class="bg-orange-600 hover:bg-orange-700 px-6 py-2 rounded font-medium transition">
Bulk Remove
</button>
</div>
<!-- Server Selection -->
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Select Servers</label>
<div class="flex gap-4 mb-4">
<button onclick="selectAllServers()"
class="text-frost hover:text-frost-light underline">
Select All
</button>
<button onclick="selectNone()"
class="text-frost hover:text-frost-light underline">
Select None
</button>
<button onclick="selectTX()"
class="text-fire hover:text-fire-light underline">
TX1 Only
</button>
<button onclick="selectNC()"
class="text-frost hover:text-frost-light underline">
NC1 Only
</button>
</div>
<div id="serverList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<!-- Server checkboxes will be populated here -->
</div>
</div>
</div>
<!-- Server Status Section -->
<div class="bg-gray-800 rounded-lg p-6">
<h2 class="text-2xl font-bold mb-4">Server Status</h2>
<div id="serverStatus" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Server status cards will be populated here -->
</div>
</div>
<!-- Results Section -->
<div id="results" class="mt-8 hidden">
<div class="bg-gray-800 rounded-lg p-6">
<h2 class="text-2xl font-bold mb-4">Results</h2>
<div id="resultsContent"></div>
</div>
</div>
</div>
<script>
const servers = {{ servers | tojson }};
// Initialize the page
function init() {
populateServerList();
populateServerStatus();
}
function populateServerList() {
const serverList = document.getElementById('serverList');
servers.forEach(server => {
const node = server.name.includes('TX') || ['Reclamation', 'Stoneblock 4', 'Society: Sunlit Valley', 'Vanilla 1.21.11', 'All The Mons'].includes(server.name) ? 'TX1' : 'NC1';
const nodeColor = node === 'TX1' ? 'text-fire' : 'text-frost';
const div = document.createElement('div');
div.className = 'flex items-center gap-2';
div.innerHTML = `
<input type="checkbox" id="server-${server.uuid}" value="${server.uuid}"
class="w-4 h-4 rounded border-gray-600 bg-gray-700 checked:bg-frost">
<label for="server-${server.uuid}" class="cursor-pointer">
<span class="${nodeColor} font-medium">[${node}]</span> ${server.name}
</label>
`;
serverList.appendChild(div);
});
}
function populateServerStatus() {
const statusDiv = document.getElementById('serverStatus');
servers.forEach(server => {
const statusColor = server.status === 'running' ? 'text-green-500' :
server.status === 'offline' ? 'text-red-500' : 'text-yellow-500';
const statusText = server.status || 'unknown';
const card = document.createElement('div');
card.className = 'bg-gray-700 rounded p-4';
card.innerHTML = `
<h3 class="font-bold mb-2">${server.name}</h3>
<p class="${statusColor}">● ${statusText.toUpperCase()}</p>
`;
statusDiv.appendChild(card);
});
}
function getSelectedServers() {
const checkboxes = document.querySelectorAll('#serverList input[type="checkbox"]:checked');
return Array.from(checkboxes).map(cb => cb.value);
}
function selectAllServers() {
document.querySelectorAll('#serverList input[type="checkbox"]').forEach(cb => cb.checked = true);
}
function selectNone() {
document.querySelectorAll('#serverList input[type="checkbox"]').forEach(cb => cb.checked = false);
}
function selectTX() {
const txServers = ['1eb33479-a6bc-4e8f-b64d-d1e4bfa0a8b4', 'a0efbfe8-4b97-4a90-869d-ffe6d3072bd5',
'9310d0a6-62a6-4fe6-82c4-eb483dc68876', '3bed1bda-f648-4630-801a-fe9f2e3d3f27',
'668a5220-7e72-4379-9165-bdbb84bc9806'];
document.querySelectorAll('#serverList input[type="checkbox"]').forEach(cb => {
cb.checked = txServers.includes(cb.value);
});
}
function selectNC() {
const ncServers = ['124f9060-58a7-457a-b2cf-b4024fce2951', 'a14201d2-83b2-44e6-ae48-e6c4cbc56f24',
'82e63949-8fbf-4a44-b32a-53324e8492bf', '2f85d4ef-aa49-4dd6-b448-beb3fca1db12',
'09a95f38-9f8c-404a-9557-3a7c44258223'];
document.querySelectorAll('#serverList input[type="checkbox"]').forEach(cb => {
cb.checked = ncServers.includes(cb.value);
});
}
async function addPlayer() {
const player = document.getElementById('playerName').value.trim();
if (!player) {
showResults([{success: false, message: 'Please enter a player name'}]);
return;
}
const selectedServers = getSelectedServers();
if (selectedServers.length === 0) {
showResults([{success: false, message: 'Please select at least one server'}]);
return;
}
const response = await fetch('/api/whitelist/add', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({player, servers: selectedServers})
});
const data = await response.json();
showResults(data.results || [data]);
}
async function removePlayer() {
const player = document.getElementById('playerName').value.trim();
if (!player) {
showResults([{success: false, message: 'Please enter a player name'}]);
return;
}
const selectedServers = getSelectedServers();
if (selectedServers.length === 0) {
showResults([{success: false, message: 'Please select at least one server'}]);
return;
}
const response = await fetch('/api/whitelist/remove', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({player, servers: selectedServers})
});
const data = await response.json();
showResults(data.results || [data]);
}
async function bulkAdd() {
const playersText = document.getElementById('bulkPlayers').value.trim();
if (!playersText) {
showResults([{success: false, message: 'Please enter player names'}]);
return;
}
const players = playersText.split('\n').map(p => p.trim()).filter(p => p);
const selectedServers = getSelectedServers();
if (selectedServers.length === 0) {
showResults([{success: false, message: 'Please select at least one server'}]);
return;
}
const response = await fetch('/api/whitelist/bulk', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({operation: 'add', players, servers: selectedServers})
});
const data = await response.json();
showResults(data.results || [data]);
}
async function bulkRemove() {
const playersText = document.getElementById('bulkPlayers').value.trim();
if (!playersText) {
showResults([{success: false, message: 'Please enter player names'}]);
return;
}
const players = playersText.split('\n').map(p => p.trim()).filter(p => p);
const selectedServers = getSelectedServers();
if (selectedServers.length === 0) {
showResults([{success: false, message: 'Please select at least one server'}]);
return;
}
const response = await fetch('/api/whitelist/bulk', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({operation: 'remove', players, servers: selectedServers})
});
const data = await response.json();
showResults(data.results || [data]);
}
function showResults(results) {
const resultsDiv = document.getElementById('results');
const resultsContent = document.getElementById('resultsContent');
resultsContent.innerHTML = '';
results.forEach(result => {
const color = result.success ? 'text-green-500' : 'text-red-500';
const icon = result.success ? '✓' : '✗';
const div = document.createElement('div');
div.className = `p-3 mb-2 rounded ${result.success ? 'bg-green-900/20' : 'bg-red-900/20'}`;
div.innerHTML = `
<span class="${color} font-bold">${icon}</span>
${result.player ? `<strong>${result.player}</strong> on ` : ''}
<strong>${result.server || 'Server'}</strong>:
${result.message}
`;
resultsContent.appendChild(div);
});
resultsDiv.classList.remove('hidden');
setTimeout(() => resultsDiv.scrollIntoView({behavior: 'smooth'}), 100);
}
// Initialize on page load
init();
</script>
</body>
</html>