Files
firefrost-services/services/arbiter/src/views/admin.html
Claude (The Golden Chronicler #50) 04e9b407d5 feat: Migrate Arbiter and Modpack Version Checker to monorepo
WHAT WAS DONE:
- Migrated Arbiter (discord-oauth-arbiter) code to services/arbiter/
- Migrated Modpack Version Checker code to services/modpack-version-checker/
- Created .env.example for Arbiter with all required environment variables
- Moved systemd service file to services/arbiter/deploy/
- Organized directory structure per Gemini monorepo recommendations

WHY:
- Consolidate all service code in one repository
- Prepare for Gemini code review (Panel v1.12 compatibility check)
- Enable service-prefixed Git tagging (arbiter-v2.1.0, modpack-v1.0.0)
- Support npm workspaces for shared dependencies

SERVICES MIGRATED:
1. Arbiter (Discord OAuth bot) - Originally written by Gemini + Claude
   - Full source code from ops-manual docs/implementation/
   - Created comprehensive .env.example
   - Ready for Panel v1.12 compatibility verification

2. Modpack Version Checker (Python CLI tool)
   - Full source code from ops-manual docs/tasks/
   - Written for Panel v1.11, needs Gemini review for v1.12
   - Never had code review before

STILL TODO:
- Whitelist Manager - Pull from Billing VPS (38.68.14.188)
  - Currently deployed and running
  - Needs Panel v1.12 API compatibility fix (Task #86)
  - Requires SSH access to pull code

NEXT STEPS:
- Gemini code review for Panel v1.12 API compatibility
- Create package.json for each service
- Test npm workspaces integration
- Deploy after verification

FILES:
- services/arbiter/ (25 new files, full application)
- services/modpack-version-checker/ (21 new files, full application)

Signed-off-by: The Golden Chronicler <claude@firefrostgaming.com>
2026-03-31 21:52:42 +00:00

189 lines
7.4 KiB
HTML

<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin Panel - Firefrost Gaming</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
</head>
<body>
<main class="container">
<nav>
<ul><li><strong>🔥❄️ Firefrost Admin Panel</strong></li></ul>
<ul><li><a href="/admin/logout">Logout</a></li></ul>
</nav>
<!-- Search User Section -->
<section>
<h2>Search User</h2>
<form id="searchForm">
<input type="email" id="searchEmail" placeholder="Enter subscriber email from Ghost CMS" required>
<button type="submit">Search</button>
</form>
<div id="searchResults"></div>
</section>
<!-- Manual Role Assignment Section -->
<section>
<h2>Manual Role Assignment</h2>
<form id="assignForm">
<input type="text" id="targetId" placeholder="Discord User ID (snowflake)" required>
<select id="action" required>
<option value="" disabled selected>Select Action...</option>
<option value="add">Add Role</option>
<option value="remove_all">Remove All Subscription Roles</option>
</select>
<select id="tier" required>
<!-- Populated dynamically by JavaScript -->
</select>
<input type="text" id="reason" placeholder="Reason (e.g., 'Support ticket #123', 'Refund')" required>
<button type="submit">Execute & Log</button>
</form>
</section>
<!-- Audit Log Section -->
<section>
<h2>Recent Actions (Audit Log)</h2>
<figure>
<table role="grid">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">Admin ID</th>
<th scope="col">Target User</th>
<th scope="col">Action</th>
<th scope="col">Reason</th>
</tr>
</thead>
<tbody id="auditLogs">
<tr><td colspan="5">Loading...</td></tr>
</tbody>
</table>
</figure>
</section>
</main>
<script>
// --- 1. Load Tier Dropdown ---
async function loadTiers() {
try {
const response = await fetch('/admin/api/tiers');
const tiers = await response.json();
const tierSelect = document.getElementById('tier');
tierSelect.innerHTML = Object.keys(tiers).map(tierKey =>
`<option value="${tierKey}">${tierKey.replace(/_/g, ' ').toUpperCase()}</option>`
).join('');
} catch (error) {
console.error('Failed to load tiers:', error);
}
}
// --- 2. Search Functionality ---
document.getElementById('searchForm').addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('searchEmail').value;
const resultsDiv = document.getElementById('searchResults');
resultsDiv.innerHTML = '<em>Searching...</em>';
try {
const response = await fetch(`/admin/api/search?email=${encodeURIComponent(email)}`);
if (!response.ok) throw new Error('User not found in CMS.');
const user = await response.json();
// Assuming Ghost CMS custom field is named 'discord_id'
const discordId = user.labels?.find(l => l.name === 'discord_id')?.value ||
user.custom_fields?.find(f => f.name === 'discord_id')?.value ||
'Not Linked';
resultsDiv.innerHTML = `
<article>
<p><strong>Name:</strong> ${user.name || 'Unknown'}</p>
<p><strong>Email:</strong> ${user.email}</p>
<p><strong>Discord ID:</strong> ${discordId}</p>
</article>
`;
// Auto-fill assignment form if Discord ID exists
if (discordId !== 'Not Linked') {
document.getElementById('targetId').value = discordId;
}
} catch (error) {
resultsDiv.innerHTML = `<p style="color: #ff6b6b;">${error.message}</p>`;
}
});
// --- 3. Role Assignment Functionality ---
document.getElementById('assignForm').addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = e.target.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = 'Processing...';
const payload = {
targetDiscordId: document.getElementById('targetId').value,
action: document.getElementById('action').value,
tier: document.getElementById('tier').value,
reason: document.getElementById('reason').value
};
try {
const response = await fetch('/admin/api/assign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if (!response.ok) throw new Error(result.error || 'Assignment failed');
alert('✅ Success: ' + result.message);
e.target.reset();
loadAuditLogs(); // Refresh logs
} catch (error) {
alert('❌ Error: ' + error.message);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Execute & Log';
}
});
// --- 4. Audit Log Display ---
async function loadAuditLogs() {
const logContainer = document.getElementById('auditLogs');
try {
const response = await fetch('/admin/api/audit-log');
const logs = await response.json();
if (logs.length === 0) {
logContainer.innerHTML = '<tr><td colspan="5">No audit logs yet.</td></tr>';
return;
}
logContainer.innerHTML = logs.map(log => `
<tr>
<td>${new Date(log.timestamp).toLocaleString()}</td>
<td>${log.admin_id}</td>
<td>${log.target_user}</td>
<td>${log.action}</td>
<td>${log.reason}</td>
</tr>
`).join('');
} catch (error) {
logContainer.innerHTML = '<tr><td colspan="5">Failed to load logs.</td></tr>';
}
}
// Initialize on page load
loadTiers();
loadAuditLogs();
</script>
</body>
</html>