diff --git a/services/arbiter-3.0/src/panel/files.js b/services/arbiter-3.0/src/panel/files.js index 3f22409..cf8753e 100644 --- a/services/arbiter-3.0/src/panel/files.js +++ b/services/arbiter-3.0/src/panel/files.js @@ -15,4 +15,30 @@ async function writeWhitelistFile(serverIdentifier, whitelistArray) { return true; } -module.exports = { writeWhitelistFile }; +async function readServerProperties(serverIdentifier) { + const endpoint = `${process.env.PANEL_URL}/api/client/servers/${serverIdentifier}/files/contents?file=server.properties`; + try { + const res = await fetch(endpoint, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${process.env.PANEL_CLIENT_KEY}`, + 'Accept': 'text/plain' + } + }); + + if (!res.ok) { + if (res.status === 404) return { exists: false, whitelistEnabled: false, raw: '' }; + throw new Error(`Failed to read properties: ${res.statusText}`); + } + + const content = await res.text(); + // Parse the file for the white-list flag + const whitelistEnabled = content.includes('white-list=true'); + return { exists: true, whitelistEnabled, raw: content }; + } catch (error) { + console.error(`Error reading properties for ${serverIdentifier}:`, error); + return { exists: false, whitelistEnabled: false, raw: '' }; + } +} + +module.exports = { writeWhitelistFile, readServerProperties }; diff --git a/services/arbiter-3.0/src/routes/admin/index.js b/services/arbiter-3.0/src/routes/admin/index.js index f5bef86..fca2425 100644 --- a/services/arbiter-3.0/src/routes/admin/index.js +++ b/services/arbiter-3.0/src/routes/admin/index.js @@ -4,7 +4,7 @@ const { requireTrinityAccess } = require('./middleware'); // Sub-routers (We will populate these as we go) const playersRouter = require('./players'); -// const serversRouter = require('./servers'); +const serversRouter = require('./servers'); // const financialsRouter = require('./financials'); router.use(requireTrinityAccess); @@ -18,6 +18,6 @@ router.get('/dashboard', (req, res) => { }); router.use('/players', playersRouter); -// router.use('/servers', serversRouter); +router.use('/servers', serversRouter); module.exports = router; diff --git a/services/arbiter-3.0/src/routes/admin/servers.js b/services/arbiter-3.0/src/routes/admin/servers.js new file mode 100644 index 0000000..61c9d2f --- /dev/null +++ b/services/arbiter-3.0/src/routes/admin/servers.js @@ -0,0 +1,111 @@ +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' || s.node === 'Node 3' || s.name.includes('TX')); + const ncServers = enrichedServers.filter(s => s.node === 'NC1' || s.node === 'Node 2' || s.name.includes('NC')); + + res.render('admin/servers/_matrix_body', { txServers, ncServers }); +}); + +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`); +}); + +module.exports = router; diff --git a/services/arbiter-3.0/src/views/admin/servers/_matrix_body.ejs b/services/arbiter-3.0/src/views/admin/servers/_matrix_body.ejs new file mode 100644 index 0000000..af2dc65 --- /dev/null +++ b/services/arbiter-3.0/src/views/admin/servers/_matrix_body.ejs @@ -0,0 +1,27 @@ +
No servers found on this node.
<% } %> +No servers found on this node.
<% } %> +<%= server.identifier %>
+Real-time status and whitelist controls
+Loading Fleet Telemetry...
+