Files
firefrost-services/services/arbiter-3.0/src/routes/admin/servers.js
Claude (Chronicler #60) 2f67708fcf Add Sync All buttons functionality for server matrix
WHAT WAS DONE:
- Added POST /admin/servers/sync-all/:node endpoint
  - Accepts 'tx1' or 'nc1' as node parameter
  - Syncs whitelist to all servers on that node
  - Returns count of synced/errors

- Wired up buttons in index.ejs with htmx
  - hx-post to the new endpoint
  - Results display in #sync-result span

Files changed:
- services/arbiter-3.0/src/routes/admin/servers.js (+45 lines)
- services/arbiter-3.0/src/views/admin/servers/index.ejs

Signed-off-by: Claude (Chronicler #60) <claude@firefrostgaming.com>
2026-04-05 08:34:50 +00:00

158 lines
6.3 KiB
JavaScript

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(`<span class="text-green-500 font-bold text-sm">✅ Synced!</span>`);
} 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(`<span class="text-red-500 font-bold text-sm">❌ Error</span>`);
}
});
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(`<span class="text-yellow-500 font-bold text-sm">⚠️ Requires Restart</span>`);
});
// 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(`<span class="text-red-500">Invalid node</span>`);
}
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(`<span class="text-green-500 font-bold">✅ ${synced} synced</span>${errors > 0 ? ` <span class="text-red-500">(${errors} errors)</span>` : ''}`);
} catch (error) {
res.send(`<span class="text-red-500">❌ ${error.message}</span>`);
}
});
module.exports = router;