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>
189 lines
7.4 KiB
HTML
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>
|