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 @@ +
+ +
+

+ 🔥 Dallas Node (TX1) +

+
+ <% txServers.forEach(server => { %> + <%- include('_server_card', { server }) %> + <% }) %> + <% if(txServers.length === 0) { %>

No servers found on this node.

<% } %> +
+
+ +
+

+ ❄️ Charlotte Node (NC1) +

+
+ <% ncServers.forEach(server => { %> + <%- include('_server_card', { server }) %> + <% }) %> + <% if(ncServers.length === 0) { %>

No servers found on this node.

<% } %> +
+
+ +
diff --git a/services/arbiter-3.0/src/views/admin/servers/_server_card.ejs b/services/arbiter-3.0/src/views/admin/servers/_server_card.ejs new file mode 100644 index 0000000..09639c4 --- /dev/null +++ b/services/arbiter-3.0/src/views/admin/servers/_server_card.ejs @@ -0,0 +1,63 @@ +<% + const isOnline = server.log.is_online; + const hasError = !!server.log.last_error; + + let borderClass = 'border-gray-200 dark:border-gray-700'; // Default / Offline + if (isOnline && !hasError) borderClass = 'border-green-500 shadow-[0_0_10px_rgba(34,197,94,0.2)]'; + if (hasError) borderClass = 'border-red-500 shadow-[0_0_10px_rgba(239,68,68,0.2)]'; +%> + +
+ +
+
+

<%= server.name %>

+

<%= server.identifier %>

+
+
+ + + <%= isOnline ? 'Online' : 'Offline' %> + +
+
+ +
+
+ Whitelist + + <%= server.whitelistEnabled ? '✅ Enabled' : '🔓 Disabled' %> + +
+
+ Last Sync + + <%= server.log.last_successful_sync ? new Date(server.log.last_successful_sync).toLocaleString() : 'Never' %> + +
+
+ + <% if (hasError) { %> +
+ Error: <%= server.log.last_error %> +
+ <% } %> + +
+ + + +
+
diff --git a/services/arbiter-3.0/src/views/admin/servers/index.ejs b/services/arbiter-3.0/src/views/admin/servers/index.ejs new file mode 100644 index 0000000..bb71315 --- /dev/null +++ b/services/arbiter-3.0/src/views/admin/servers/index.ejs @@ -0,0 +1,29 @@ +<%- include('../../layout', { body: ` +
+
+

Server Matrix

+

Real-time status and whitelist controls

+
+
+ + +
+
+ +
+ +
+
+
+

Loading Fleet Telemetry...

+
+
+ +
+`}) %>