Files
firefrost-services/services/arbiter-3.0/src/routes/admin.js
Claude 918fb99b87 Add Social Overview card to Trinity Console dashboard
- Shows cross-platform totals (posts, views, likes, comments)
- Breaks down by platform with icons
- Clickable link to full Social Analytics page

Chronicler #76
2026-04-10 22:23:21 +00:00

475 lines
16 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { getRoleMappings, saveRoleMappings } = require('../utils/roleMappings');
const { getFinancialMetrics } = require('../services/FinancialsService');
const isAdmin = (req, res, next) => {
if (req.isAuthenticated()) {
const admins = process.env.ADMIN_USERS.split(',');
if (admins.includes(req.user.id)) return next();
}
res.status(403).send('Forbidden: Admin access only.');
};
// TODO: Replace with full beautiful UI from live bot.js
router.get('/', isAdmin, async (req, res) => {
try {
const mappings = getRoleMappings();
// Fetch social stats across all platforms
const socialStats = await db.query(`
SELECT
platform,
COUNT(*) as post_count,
COALESCE(SUM(views), 0) as total_views,
COALESCE(SUM(likes), 0) as total_likes,
COALESCE(SUM(comments), 0) as total_comments
FROM social_posts
GROUP BY platform
`);
// Aggregate totals
const socialTotals = {
posts: 0,
views: 0,
likes: 0,
comments: 0,
platforms: {}
};
for (const row of socialStats.rows) {
socialTotals.posts += parseInt(row.post_count);
socialTotals.views += parseInt(row.total_views);
socialTotals.likes += parseInt(row.total_likes);
socialTotals.comments += parseInt(row.total_comments);
socialTotals.platforms[row.platform] = {
posts: parseInt(row.post_count),
views: parseInt(row.total_views),
likes: parseInt(row.total_likes)
};
}
res.render('admin/dashboard', {
title: 'Dashboard',
adminUser: req.user,
csrfToken: req.csrfToken(),
mappings: mappings,
currentPath: '/dashboard',
socialTotals
});
} catch (error) {
console.error('Admin dashboard error:', error);
res.status(500).send('Internal Server Error: ' + error.message);
}
});
// Server Matrix Module
router.get('/servers', isAdmin, async (req, res) => {
try {
res.render('admin/servers/index', {
title: 'Server Matrix',
adminUser: req.user,
csrfToken: req.csrfToken(),
currentPath: '/servers'
});
} catch (error) {
console.error('Server matrix error:', error);
res.status(500).send('Internal Server Error: ' + error.message);
}
});
// Player Management Module
router.get('/players', isAdmin, async (req, res) => {
try {
res.render('admin/players/index', {
title: 'Player Management',
adminUser: req.user,
csrfToken: req.csrfToken(),
currentPath: '/players'
});
} catch (error) {
console.error('Player management error:', error);
res.status(500).send('Internal Server Error: ' + error.message);
}
});
// Role Management Module
router.get('/roles', isAdmin, async (req, res) => {
try {
const mappings = getRoleMappings();
res.render('admin/roles/index', {
title: 'Role Management',
adminUser: req.user,
csrfToken: req.csrfToken(),
mappings: mappings,
currentPath: '/roles'
});
} catch (error) {
console.error('Role management error:', error);
res.status(500).send('Internal Server Error: ' + error.message);
}
});
// Grace Period Management Module
router.get('/grace', isAdmin, async (req, res) => {
try {
res.render('admin/grace/index', {
title: 'Grace Period Management',
adminUser: req.user,
csrfToken: req.csrfToken(),
currentPath: '/grace'
});
} catch (error) {
console.error('Grace period management error:', error);
res.status(500).send('Internal Server Error: ' + error.message);
}
});
// Audit Log Module
router.get('/audit', isAdmin, async (req, res) => {
try {
res.render('admin/audit/index', {
title: 'Audit Log',
adminUser: req.user,
csrfToken: req.csrfToken(),
currentPath: '/audit'
});
} catch (error) {
console.error('Audit log error:', error);
res.status(500).send('Internal Server Error: ' + error.message);
}
});
// Financials Module
router.get('/financials', isAdmin, async (req, res) => {
try {
// Fetch real financial data from PostgreSQL
const financialData = await getFinancialMetrics();
res.render('admin/financials/index', {
title: 'Financials',
adminUser: req.user,
csrfToken: req.csrfToken(),
currentPath: '/financials',
metrics: financialData.metrics,
paths: financialData.paths,
tierBreakdown: financialData.tierBreakdown
});
} catch (error) {
console.error('Financials error:', error);
res.status(500).send('Internal Server Error: ' + error.message);
}
});
router.post('/mappings', isAdmin, express.json(), (req, res) => {
const newMappings = req.body;
if (saveRoleMappings(newMappings)) {
res.status(200).send('Mappings updated');
} else {
res.status(500).send('Failed to save mappings');
}
});
module.exports = router;
// ==========================================
// HTMX API ENDPOINTS (Return HTML fragments)
// ==========================================
// Servers Matrix Endpoint
router.get('/servers/matrix', isAdmin, async (req, res) => {
// Static server list from infrastructure
const servers = [
{ name: 'Awakened Survival', machine: 'TX1', status: 'online', players: '0/20' },
{ name: 'Fire PvP Arena', machine: 'TX1', status: 'online', players: '0/50' },
{ name: 'Frost Creative', machine: 'TX1', status: 'online', players: '0/30' },
{ name: 'Knight Hardcore', machine: 'NC1', status: 'online', players: '0/25' },
{ name: 'Master Skyblock', machine: 'NC1', status: 'online', players: '0/40' },
{ name: 'Legend Factions', machine: 'NC1', status: 'online', players: '0/60' },
{ name: 'Sovereign Network Hub', machine: 'TX1', status: 'online', players: '0/100' }
];
let html = '<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">';
servers.forEach(server => {
const statusColor = server.status === 'online' ? 'bg-green-500' : 'bg-red-500';
html += `
<div class="bg-white dark:bg-darkcard rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between mb-2">
<div class="font-medium dark:text-white">${server.name}</div>
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full ${statusColor}"></span>
<span class="text-xs text-gray-500 dark:text-gray-400">${server.status}</span>
</div>
</div>
<div class="text-sm text-gray-600 dark:text-gray-400">
<div>Machine: ${server.machine}</div>
<div>Players: ${server.players}</div>
</div>
</div>
`;
});
html += '</div>';
html += `
<div class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<p class="text-sm text-blue-800 dark:text-blue-200">
💡 <strong>Note:</strong> This is static data. Real-time Pterodactyl API integration coming soon.
</p>
</div>
`;
res.send(html);
});
// Players Table Endpoint
router.get('/players/table', isAdmin, async (req, res) => {
const { Pool } = require('pg');
const pool = new Pool({
host: '127.0.0.1',
user: 'arbiter',
password: 'FireFrost2026!Arbiter',
database: 'arbiter_db'
});
try {
const result = await pool.query(`
SELECT
s.id,
s.discord_id,
s.tier_level,
p.tier_name,
s.status,
s.created_at,
s.mrr_value
FROM subscriptions s
LEFT JOIN stripe_products p ON s.tier_level = p.tier_level
ORDER BY s.created_at DESC
LIMIT 100
`);
if (result.rows.length === 0) {
res.send(`
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
<p class="text-lg">👥 No subscribers yet</p>
<p class="text-sm mt-2">Subscribers will appear here after first signup</p>
</div>
`);
} else {
let html = `
<table class="w-full">
<thead class="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Discord ID</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Tier</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Status</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">MRR</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Since</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
`;
result.rows.forEach(row => {
const statusColor = row.status === 'active' ? 'text-green-600' :
row.status === 'lifetime' ? 'text-purple-600' :
row.status === 'grace_period' ? 'text-yellow-600' :
'text-gray-600';
const date = new Date(row.created_at);
html += `
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-4 py-3 text-sm font-mono dark:text-white">${row.discord_id || 'N/A'}</td>
<td class="px-4 py-3 text-sm dark:text-white">${row.tier_name || 'Tier ' + row.tier_level}</td>
<td class="px-4 py-3">
<span class="text-sm font-medium ${statusColor}">${row.status}</span>
</td>
<td class="px-4 py-3 text-sm text-right dark:text-white">$${parseFloat(row.mrr_value || 0).toFixed(2)}</td>
<td class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400">${date.toLocaleDateString()}</td>
</tr>
`;
});
html += '</tbody></table>';
res.send(html);
}
} catch (error) {
res.send(`<div class="p-6 text-red-600">Error: ${error.message}</div>`);
}
});
// Grace Period List Endpoint
router.get('/grace/list', isAdmin, async (req, res) => {
const { Pool } = require('pg');
const pool = new Pool({
host: '127.0.0.1',
user: 'arbiter',
password: 'FireFrost2026!Arbiter',
database: 'arbiter_db'
});
try {
const result = await pool.query(`
SELECT discord_id, tier_level, grace_period_started_at, grace_period_ends_at
FROM subscriptions
WHERE status = 'grace_period'
ORDER BY grace_period_ends_at ASC
`);
if (result.rows.length === 0) {
res.send(`
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
<p class="text-lg">✅ No users in grace period!</p>
<p class="text-sm mt-2">All subscribers are current on payments</p>
</div>
`);
} else {
let html = '<div class="divide-y divide-gray-200 dark:divide-gray-700">';
result.rows.forEach(row => {
const endsAt = new Date(row.grace_period_ends_at);
const hoursLeft = Math.round((endsAt - new Date()) / (1000 * 60 * 60));
html += `
<div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-800">
<div class="flex justify-between items-center">
<div>
<div class="font-medium dark:text-white">Discord ID: ${row.discord_id}</div>
<div class="text-sm text-gray-500">Tier ${row.tier_level}</div>
</div>
<div class="text-right">
<div class="text-yellow-600 dark:text-yellow-400 font-medium">${hoursLeft}h remaining</div>
<div class="text-xs text-gray-500">${endsAt.toLocaleString()}</div>
</div>
</div>
</div>
`;
});
html += '</div>';
res.send(html);
}
} catch (error) {
res.send(`<div class="p-6 text-red-600">Error: ${error.message}</div>`);
}
});
// Audit Log Feed Endpoint
router.get('/audit/feed', isAdmin, async (req, res) => {
const { Pool } = require('pg');
const pool = new Pool({
host: '127.0.0.1',
user: 'arbiter',
password: 'FireFrost2026!Arbiter',
database: 'arbiter_db'
});
try {
const result = await pool.query(`
SELECT *
FROM webhook_events_processed
ORDER BY processed_at DESC
LIMIT 50
`);
if (result.rows.length === 0) {
res.send(`
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
<p class="text-lg">📋 No webhook events yet</p>
<p class="text-sm mt-2">Events will appear here as Stripe webhooks are processed</p>
</div>
`);
} else {
let html = '<div class="divide-y divide-gray-200 dark:divide-gray-700">';
result.rows.forEach(row => {
const timestamp = new Date(row.processed_at);
const eventType = row.event_type || 'unknown';
const eventId = row.stripe_event_id || row.id || 'unknown';
const eventColor = eventType.includes('succeeded') ? 'text-green-600' :
eventType.includes('failed') ? 'text-red-600' :
eventType.includes('dispute') ? 'text-red-600' :
'text-blue-600';
html += `
<div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-800">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="font-mono text-xs text-gray-500 mb-1">${eventId}</div>
<div class="font-medium ${eventColor}">${eventType}</div>
</div>
<div class="text-right text-sm text-gray-500 dark:text-gray-400">
${timestamp.toLocaleString()}
</div>
</div>
</div>
`;
});
html += '</div>';
res.send(html);
}
} catch (error) {
res.send(`<div class="p-6 text-red-600">Error: ${error.message}</div>`);
}
});
// Role Mismatches Diagnostic Endpoint
router.get('/roles/mismatches', isAdmin, async (req, res) => {
const { Pool } = require('pg');
const pool = new Pool({
host: '127.0.0.1',
user: 'arbiter',
password: 'FireFrost2026!Arbiter',
database: 'arbiter_db'
});
try {
// Get subscription counts by tier
const result = await pool.query(`
SELECT tier_level, COUNT(*) as count, status
FROM subscriptions
WHERE status IN ('active', 'lifetime')
GROUP BY tier_level, status
ORDER BY tier_level
`);
if (result.rows.length === 0) {
res.send(`
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
<p class="text-lg">✅ No active subscriptions</p>
<p class="text-sm mt-2">Role diagnostics will run when users subscribe</p>
</div>
`);
} else {
let html = `
<div class="p-6">
<div class="mb-4">
<h3 class="text-lg font-medium dark:text-white mb-2">📊 Subscription Summary</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Active subscribers by tier (Discord role sync coming soon)</p>
</div>
<div class="space-y-2">
`;
result.rows.forEach(row => {
const statusColor = row.status === 'active' ? 'text-green-600' : 'text-purple-600';
html += `
<div class="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-800 rounded">
<span class="text-sm dark:text-white">Tier ${row.tier_level}</span>
<div class="flex items-center gap-3">
<span class="${statusColor} text-sm font-medium">${row.status}</span>
<span class="text-sm dark:text-white">${row.count} subscriber${row.count > 1 ? 's' : ''}</span>
</div>
</div>
`;
});
html += `
</div>
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<p class="text-sm text-blue-800 dark:text-blue-200">
💡 <strong>Coming Soon:</strong> Discord API integration to compare database tiers with actual Discord roles
</p>
</div>
</div>
`;
res.send(html);
}
} catch (error) {
res.send(`<div class="p-6 text-red-600">Error: ${error.message}</div>`);
}
});