const express = require('express'); const router = express.Router(); const db = require('../../database'); const { getMinecraftServers } = require('../../panel/discovery'); const { readServerProperties, writeWhitelistFile } = require('../../panel/files'); const { reloadWhitelistCommand } = require('../../panel/commands'); // In-memory cache for RV low-bandwidth operations let serverCache = { data: null, lastFetch: 0 }; const CACHE_TTL = 60000; // 60 seconds router.get('/', (req, res) => { res.render('admin/servers/index', { title: 'Server Matrix' }); }); router.get('/matrix', async (req, res) => { const now = Date.now(); let serversData = []; // Use cache if valid, otherwise fetch fresh if (serverCache.data && (now - serverCache.lastFetch < CACHE_TTL)) { serversData = serverCache.data; } else { const discovered = await getMinecraftServers(); // Fetch properties for all discovered servers sequentially to avoid rate limits for (const srv of discovered) { const props = await readServerProperties(srv.identifier); serversData.push({ ...srv, whitelistEnabled: props.whitelistEnabled, rawProps: props.raw }); } serverCache = { data: serversData, lastFetch: now }; } // Join with Database Sync Logs (Always fetch fresh logs, no cache) const { rows: logs } = await db.query('SELECT * FROM server_sync_log'); const logMap = logs.reduce((acc, log) => { acc[log.server_identifier] = log; return acc; }, {}); const enrichedServers = serversData.map(srv => ({ ...srv, log: logMap[srv.identifier] || { is_online: false, last_error: 'Never synced' } })); // Group by Node Location const txServers = enrichedServers.filter(s => s.node === 'TX1'); const ncServers = enrichedServers.filter(s => s.node === 'NC1'); res.render('admin/servers/_matrix_body', { txServers, ncServers, layout: false }); }); router.post('/:identifier/sync', async (req, res) => { const { identifier } = req.params; try { 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 IN ('active', 'grace_period', 'lifetime')` ); await writeWhitelistFile(identifier, players); await reloadWhitelistCommand(identifier); await db.query( "INSERT INTO server_sync_log (server_identifier, last_successful_sync, is_online, last_error) VALUES ($1, NOW(), true, NULL) ON CONFLICT (server_identifier) DO UPDATE SET last_successful_sync = NOW(), is_online = true, last_error = NULL", [identifier] ); res.send(`✅ Synced!`); } catch (error) { await db.query( "INSERT INTO server_sync_log (server_identifier, last_error, is_online) VALUES ($1, $2, false) ON CONFLICT (server_identifier) DO UPDATE SET last_error = $2, is_online = false", [identifier, error.message] ); res.send(`❌ Error`); } }); router.post('/:identifier/toggle-whitelist', async (req, res) => { const { identifier } = req.params; // Clear cache so the UI updates on next poll serverCache.lastFetch = 0; const props = await readServerProperties(identifier); if (!props.exists) return res.send('File not found'); let newContent; if (props.whitelistEnabled) { newContent = props.raw.replace('white-list=true', 'white-list=false'); } else { if (props.raw.includes('white-list=false')) { newContent = props.raw.replace('white-list=false', 'white-list=true'); } else { newContent = props.raw + '\nwhite-list=true'; } } const endpoint = `${process.env.PANEL_URL}/api/client/servers/${identifier}/files/write?file=server.properties`; await fetch(endpoint, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.PANEL_CLIENT_KEY}`, 'Content-Type': 'text/plain' }, body: newContent }); res.send(`⚠️ Requires Restart`); }); // Sync all servers on a specific node router.post('/sync-all/:node', async (req, res) => { const { node } = req.params; const nodeId = node === 'tx1' ? 3 : node === 'nc1' ? 2 : null; if (!nodeId) { return res.send(`Invalid node`); } try { const discovered = await getMinecraftServers(); const nodeServers = discovered.filter(s => s.nodeId === nodeId); 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 IN ('active', 'grace_period', 'lifetime')` ); let synced = 0; let errors = 0; for (const srv of nodeServers) { try { await writeWhitelistFile(srv.identifier, players); await reloadWhitelistCommand(srv.identifier); await db.query( "INSERT INTO server_sync_log (server_identifier, last_successful_sync, is_online, last_error) VALUES ($1, NOW(), true, NULL) ON CONFLICT (server_identifier) DO UPDATE SET last_successful_sync = NOW(), is_online = true, last_error = NULL", [srv.identifier] ); synced++; } catch (err) { await db.query( "INSERT INTO server_sync_log (server_identifier, last_error, is_online) VALUES ($1, $2, false) ON CONFLICT (server_identifier) DO UPDATE SET last_error = $2, is_online = false", [srv.identifier, err.message] ); errors++; } } res.send(`✅ ${synced} synced${errors > 0 ? ` (${errors} errors)` : ''}`); } catch (error) { res.send(`❌ ${error.message}`); } }); module.exports = router;