Add modular server update feature with Discord notifications
All checks were successful
Deploy GSM / deploy (push) Successful in 25s

- Add serverUpdates.js service with handler registry for extensibility
- Implement Factorio Docker image update (pull + container recreate)
- Add GET/POST /servers/:id/update routes for check/perform
- Add ServerUpdateButton component with auto-check and confirm dialog
- Integrate update card in ServerDetail overview tab
- Auto-send Discord notification on successful update

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alexander Zielonka
2026-01-23 11:42:22 +01:00
parent ed60bc33c7
commit d840faeda9
5 changed files with 540 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
import { EmbedBuilder } from 'discord.js';
import { authenticateToken, optionalAuth, requireRole, rejectGuest } from '../middleware/auth.js';
import { getServerStatus, startServer, stopServer, restartServer, getConsoleLog, getProcessUptime, listFactorioSaves, createFactorioWorld, deleteFactorioSave, getFactorioCurrentSave, isHostFailed, listZomboidConfigs, readZomboidConfig, writeZomboidConfig, listPalworldConfigs, readPalworldConfig, writePalworldConfig, readTerrariaConfig, writeTerrariaConfig, readOpenTTDConfig, writeOpenTTDConfig, readHytaleConfig, writeHytaleConfig } from '../services/ssh.js';
import { supportsUpdate, checkForUpdate, performUpdate } from '../services/serverUpdates.js';
import { sendRconCommand, getPlayers, getPlayerList } from '../services/rcon.js';
import { getServerMetricsHistory, getCurrentMetrics } from '../services/prometheus.js';
import { initWhitelistCache, getCachedWhitelist, setCachedWhitelist, initFactorioTemplates, getFactorioTemplates, createFactorioTemplate, deleteFactorioTemplate, initFactorioWorldSettings, getFactorioWorldSettings, saveFactorioWorldSettings, deleteFactorioWorldSettings, initAutoShutdownSettings, getAutoShutdownSettings, setAutoShutdownSettings, initActivityLog, logActivity, getActivityLog, initServerDisplaySettings, getServerDisplaySettings, getAllServerDisplaySettings, setServerDisplaySettings, initGuildSettings } from '../db/init.js';
@@ -749,6 +750,75 @@ router.post('/:id/restart', authenticateToken, requireRole('moderator'), async (
}
});
// ============ SERVER UPDATE ROUTES ============
// Check for server updates (moderator+)
router.get('/:id/update', authenticateToken, requireRole('moderator'), async (req, res) => {
const config = loadConfig();
const server = config.servers.find(s => s.id === req.params.id);
if (!server) return res.status(404).json({ error: 'Server not found' });
// Check if this server type supports updates
if (!supportsUpdate(server.type)) {
return res.json({
supported: false,
hasUpdate: false,
message: 'Updates für diesen Servertyp nicht unterstützt'
});
}
if (isHostFailed(server.host, server.sshUser)) {
return res.status(503).json({ error: 'Server host is unreachable', unreachable: true });
}
try {
const result = await checkForUpdate(server);
res.json(result);
} catch (err) {
if (err.message.includes('unreachable') || err.message.includes('ECONNREFUSED') || err.message.includes('ETIMEDOUT')) {
return res.status(503).json({ error: 'Server host is unreachable', unreachable: true });
}
res.status(500).json({ error: err.message });
}
});
// Perform server update (moderator+)
router.post('/:id/update', authenticateToken, requireRole('moderator'), async (req, res) => {
const config = loadConfig();
const server = config.servers.find(s => s.id === req.params.id);
if (!server) return res.status(404).json({ error: 'Server not found' });
if (!supportsUpdate(server.type)) {
return res.status(400).json({ error: 'Updates für diesen Servertyp nicht unterstützt' });
}
if (isHostFailed(server.host, server.sshUser)) {
return res.status(503).json({ error: 'Server host is unreachable', unreachable: true });
}
// Check if server is running
try {
const status = await getServerStatus(server);
if (status === 'online' || status === 'starting') {
return res.status(400).json({ error: 'Server muss gestoppt sein um zu updaten' });
}
} catch (err) {
// Continue if status check fails
}
try {
const sendDiscord = req.body?.sendDiscord !== false; // Default true
const result = await performUpdate(server, sendDiscord);
logActivity(req.user.id, req.user.username, 'server_update', server.id, result.newVersion || 'latest', req.user.discordId, req.user.avatar);
res.json(result);
} catch (err) {
if (err.message.includes('unreachable') || err.message.includes('ECONNREFUSED') || err.message.includes('ETIMEDOUT')) {
return res.status(503).json({ error: 'Server host is unreachable', unreachable: true });
}
res.status(500).json({ error: err.message });
}
});
// Get whitelist (with server-side caching, guests not allowed)
router.get('/:id/whitelist', authenticateToken, rejectGuest, async (req, res) => {
const config = loadConfig();