diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 2c41e8e..b140acf 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -3,12 +3,8 @@
"allow": [
"Bash(ssh:*)",
"Bash(scp:*)",
- "Bash(veth.*)",
- "Bash(docker.*)",
+ "Bash(docker:*)",
"Bash(curl:*)",
- "Bash(findstr:*)",
- "Bash(cat:*)",
- "Bash(powershell -Command @'\n$content = @\"\"\nimport { useState, useEffect } from 'react'\nimport { useNavigate } from 'react-router-dom'\nimport { getServers } from '../api'\nimport { useUser } from '../context/UserContext'\nimport ServerCard from '../components/ServerCard'\nimport SettingsModal from '../components/SettingsModal'\nimport UserManagement from '../components/UserManagement'\n\nexport default function Dashboard\\({ onLogout }\\) {\n const navigate = useNavigate\\(\\)\n const { user, token, loading: userLoading, isSuperadmin, role } = useUser\\(\\)\n const [servers, setServers] = useState\\([]\\)\n const [loading, setLoading] = useState\\(true\\)\n const [error, setError] = useState\\(''\\)\n const [showSettings, setShowSettings] = useState\\(false\\)\n const [showUserMgmt, setShowUserMgmt] = useState\\(false\\)\n\n const fetchServers = async \\(\\) => {\n try {\n const data = await getServers\\(token\\)\n setServers\\(data\\)\n setError\\(''\\)\n } catch \\(err\\) {\n setError\\('Failed to connect to server'\\)\n if \\(err.message.includes\\('401'\\) || err.message.includes\\('403'\\)\\) {\n onLogout\\(\\)\n }\n } finally {\n setLoading\\(false\\)\n }\n }\n\n useEffect\\(\\(\\) => {\n if \\(!userLoading\\) {\n fetchServers\\(\\)\n const interval = setInterval\\(fetchServers, 10000\\)\n return \\(\\) => clearInterval\\(interval\\)\n }\n }, [token, userLoading]\\)\n\n const roleLabels = {\n user: 'Viewer',\n moderator: 'Operator',\n superadmin: 'Admin'\n }\n\n if \\(userLoading\\) {\n return \\(\n
\n \\)\n }\n\n const onlineCount = servers.filter\\(s => s.running\\).length\n const totalPlayers = servers.reduce\\(\\(sum, s\\) => sum + \\(s.players?.online || 0\\), 0\\)\n\n return \\(\n \n {/* Header */}\n
\n \n
\n
\n
\n Gameserver Monitor\n
\n
\n \n {onlineCount}/{servers.length} online\n \n |\n \n {totalPlayers} players\n \n
\n
\n\n
\n
\n
{user?.username}
\n
{roleLabels[role]}
\n
\n\n {isSuperadmin && \\(\n
\n \\)}\n\n
\n\n
\n
\n
\n
\n \n\n {/* Main Content */}\n
\n {error && \\(\n \n {error}\n
\n \\)}\n\n {loading ? \\(\n \n \\) : \\(\n \n {servers.map\\(\\(server, index\\) => \\(\n
\n navigate\\('/server/' + server.id\\)}\n />\n
\n \\)\\)}\n
\n \\)}\n \n\n {/* Modals */}\n {showSettings && \\(\n
setShowSettings\\(false\\)} />\n \\)}\n\n {showUserMgmt && \\(\n setShowUserMgmt\\(false\\)} />\n \\)}\n \n \\)\n}\n\"\"@\n$content | Out-File -FilePath \"\"Dashboard.jsx\"\" -Encoding UTF8\n'@)",
"Bash(git add:*)"
]
}
diff --git a/ZeasyWG-Alex.conf b/ZeasyWG-Alex.conf
new file mode 100644
index 0000000..028fdaa
--- /dev/null
+++ b/ZeasyWG-Alex.conf
@@ -0,0 +1,10 @@
+[Interface]
+PrivateKey = WC2HEXQ10sm5sE9Apj5wBBv2/CSRmpQNUovT1xSO8kM=
+Address = 10.0.200.201/32
+DNS = 10.0.0.1
+
+[Peer]
+PublicKey = wgP8/WMTzhr45bH2GFabcYUhycCLF/pPczghWgLJN0Q=
+Endpoint = beeck.zeasy.dev:47199
+AllowedIPs = 10.0.0.0/16
+PersistentKeepalive = 60
\ No newline at end of file
diff --git a/gsm-backend/services/rcon.js b/gsm-backend/services/rcon.js
new file mode 100644
index 0000000..c86b886
--- /dev/null
+++ b/gsm-backend/services/rcon.js
@@ -0,0 +1,159 @@
+import { Rcon } from 'rcon-client';
+
+const rconConnections = new Map();
+const playerCache = new Map();
+const CACHE_TTL = 30000; // 30 seconds
+
+async function getConnection(server) {
+ const key = `${server.host}:${server.rconPort}`;
+
+ if (rconConnections.has(key)) {
+ const conn = rconConnections.get(key);
+ if (conn.authenticated) {
+ return conn;
+ }
+ rconConnections.delete(key);
+ }
+
+ const rcon = await Rcon.connect({
+ host: server.host,
+ port: server.rconPort,
+ password: server.rconPassword,
+ timeout: 5000
+ });
+
+ rcon.on('error', (err) => {
+ console.error(`RCON error for ${key}:`, err.message);
+ rconConnections.delete(key);
+ });
+
+ rcon.on('end', () => {
+ rconConnections.delete(key);
+ });
+
+ // Handle socket errors to prevent crash
+ if (rcon.socket) {
+ rcon.socket.on('error', (err) => {
+ console.error(`RCON socket error for ${key}:`, err.message);
+ rconConnections.delete(key);
+ });
+ }
+
+ rconConnections.set(key, rcon);
+ return rcon;
+}
+
+export async function sendRconCommand(server, command) {
+ if (!server.rconPassword) {
+ throw new Error('RCON password not configured');
+ }
+
+ const rcon = await getConnection(server);
+ return await rcon.send(command);
+}
+
+export async function getPlayers(server) {
+ const cacheKey = `${server.id}-count`;
+ const cached = playerCache.get(cacheKey);
+
+ if (cached && Date.now() - cached.time < CACHE_TTL) {
+ return cached.data;
+ }
+
+ try {
+ let result = { online: 0, max: null };
+
+ if (server.type === 'minecraft') {
+ const response = await sendRconCommand(server, 'list');
+ const match = response.match(/There are (\d+) of a max of (\d+) players online/);
+ if (match) {
+ result = { online: parseInt(match[1]), max: parseInt(match[2]) };
+ }
+ } else if (server.type === 'factorio') {
+ const response = await sendRconCommand(server, '/players online count');
+ const match = response.match(/(\d+)/);
+ if (match) {
+ result = { online: parseInt(match[1]), max: null };
+ }
+ } else if (server.type === 'zomboid') {
+ const response = await sendRconCommand(server, 'players');
+ // Format: "Players connected (X):" or "Players connected (X): Player1, Player2"
+ const match = response.match(/Players connected \((\d+)\)/);
+ if (match) {
+ result = { online: parseInt(match[1]), max: null };
+ }
+ } else if (server.type === 'vrising') {
+ const response = await sendRconCommand(server, 'listusers');
+ // Count lines that contain player info
+ const lines = response.split('\n').filter(l => l.trim() && !l.includes('listusers'));
+ result = { online: lines.length, max: null };
+ }
+
+ playerCache.set(cacheKey, { data: result, time: Date.now() });
+ return result;
+ } catch (err) {
+ console.error(`Failed to get players for ${server.id}:`, err.message);
+ // Clear cache on error - server might be offline
+ playerCache.delete(cacheKey);
+ return { online: 0, max: null };
+ }
+}
+
+export async function getPlayerList(server) {
+ const cacheKey = `${server.id}-list`;
+ const cached = playerCache.get(cacheKey);
+
+ if (cached && Date.now() - cached.time < CACHE_TTL) {
+ return cached.data;
+ }
+
+ try {
+ let players = [];
+
+ if (server.type === 'minecraft') {
+ const response = await sendRconCommand(server, 'list');
+ // Format: "There are X of a max of Y players online: Player1, Player2, Player3"
+ const colonIndex = response.indexOf(':');
+ if (colonIndex !== -1) {
+ const playerPart = response.substring(colonIndex + 1).trim();
+ if (playerPart) {
+ players = playerPart.split(',').map(p => p.trim()).filter(p => p);
+ }
+ }
+ } else if (server.type === 'factorio') {
+ const response = await sendRconCommand(server, '/players online');
+ // Format: "Online players (X):\n player1 (online)\n player2 (online)"
+ const lines = response.split('\n');
+ for (const line of lines) {
+ const match = line.match(/^\s*(\S+)\s*\(online\)/);
+ if (match) {
+ players.push(match[1]);
+ }
+ }
+ } else if (server.type === 'zomboid') {
+ const response = await sendRconCommand(server, 'players');
+ // Format: "Players connected (X):" or "Players connected (X): Player1, Player2"
+ const colonIndex = response.indexOf(':');
+ if (colonIndex !== -1) {
+ const playerPart = response.substring(colonIndex + 1).trim();
+ if (playerPart) {
+ players = playerPart.split(',').map(p => p.trim()).filter(p => p);
+ }
+ }
+ } else if (server.type === 'vrising') {
+ const response = await sendRconCommand(server, 'listusers');
+ // Parse player names from listusers output
+ const lines = response.split('\n').filter(l => l.trim() && !l.includes('listusers'));
+ players = lines.map(l => l.trim()).filter(p => p);
+ }
+
+ const result = { players };
+ playerCache.set(cacheKey, { data: result, time: Date.now() });
+ return result;
+ } catch (err) {
+ console.error(`Failed to get player list for ${server.id}:`, err.message);
+ // Clear cache on error - server might be offline
+ playerCache.delete(cacheKey);
+ return { players: [] };
+ }
+}
diff --git a/gsm-backend/services/ssh.js b/gsm-backend/services/ssh.js
new file mode 100644
index 0000000..8edbe96
--- /dev/null
+++ b/gsm-backend/services/ssh.js
@@ -0,0 +1,286 @@
+import { NodeSSH } from 'node-ssh';
+import { readFileSync } from 'fs';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const sshConnections = new Map();
+
+function loadConfig() {
+ return JSON.parse(readFileSync(join(__dirname, '..', 'config.json'), 'utf-8'));
+}
+
+async function getConnection(host, username = "root") {
+ if (sshConnections.has(username + "@" + host)) {
+ const conn = sshConnections.get(username + "@" + host);
+ if (conn.isConnected()) return conn;
+ }
+
+ const ssh = new NodeSSH();
+ await ssh.connect({
+ host,
+ username: username,
+ privateKeyPath: '/root/.ssh/id_ed25519'
+ });
+
+ sshConnections.set(username + "@" + host, ssh);
+ return ssh;
+}
+
+// Returns: "online", "starting", "stopping", "offline"
+export async function getServerStatus(server) {
+ try {
+ const ssh = await getConnection(server.host, server.sshUser);
+
+ if (server.runtime === 'docker') {
+ const result = await ssh.execCommand(`docker inspect --format='{{.State.Status}}' ${server.containerName} 2>/dev/null`);
+ const status = result.stdout.trim();
+ if (status === 'running') return 'online';
+ if (status === 'restarting' || status === 'created') return 'starting';
+ if (status === 'removing' || status === 'paused') return 'stopping';
+ return 'offline';
+ } else if (server.runtime === 'systemd') {
+ const result = await ssh.execCommand(`systemctl is-active ${server.serviceName}`);
+ const status = result.stdout.trim();
+ if (status === 'active') return 'online';
+ if (status === 'activating' || status === 'reloading') return 'starting';
+ if (status === 'deactivating') return 'stopping';
+ return 'offline';
+ } else {
+ const result = await ssh.execCommand(`screen -ls | grep -E "\\.${server.screenName}[[:space:]]"`);
+ if (result.code === 0) {
+ const uptimeResult = await ssh.execCommand(`ps -o etimes= -p $(screen -ls | grep "\\.${server.screenName}" | awk '{print $1}' | cut -d. -f1) 2>/dev/null | head -1`);
+ const uptime = parseInt(uptimeResult.stdout.trim()) || 999;
+ if (uptime < 60) return 'starting';
+ return 'online';
+ }
+ return 'offline';
+ }
+ } catch (err) {
+ console.error(`Failed to get status for ${server.id}:`, err.message);
+ return 'offline';
+ }
+}
+
+export async function startServer(server, options = {}) {
+ const ssh = await getConnection(server.host, server.sshUser);
+
+ if (server.runtime === 'docker') {
+ // Factorio with specific save
+ if (server.type === 'factorio' && options.save) {
+ const saveName = options.save.endsWith('.zip') ? options.save.replace('.zip', '') : options.save;
+ // Stop and remove existing container
+ await ssh.execCommand(`docker stop ${server.containerName} 2>/dev/null || true`);
+ await ssh.execCommand(`docker rm ${server.containerName} 2>/dev/null || true`);
+ // Start with specific save
+ await ssh.execCommand(`
+ docker run -d \
+ --name ${server.containerName} \
+ -p 34197:34197/udp \
+ -p 27015:27015/tcp \
+ -v /srv/docker/factorio/data:/factorio \
+ -e SAVE_NAME=${saveName} -e LOAD_LATEST_SAVE=false \
+ -e TZ=Europe/Berlin \
+ -e PUID=845 \
+ -e PGID=845 \
+ --restart=unless-stopped \
+ factoriotools/factorio
+ `);
+ } else {
+ await ssh.execCommand(`docker start ${server.containerName}`);
+ }
+ } else if (server.runtime === 'systemd') {
+ await ssh.execCommand(`systemctl start ${server.serviceName}`);
+ } else {
+ await ssh.execCommand(`screen -S ${server.screenName} -X quit 2>/dev/null`);
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ await ssh.execCommand(`cd ${server.workDir} && screen -dmS ${server.screenName} ${server.startCmd}`);
+ }
+}
+
+export async function stopServer(server) {
+ const ssh = await getConnection(server.host, server.sshUser);
+
+ if (server.runtime === 'docker') {
+ await ssh.execCommand(`docker stop ${server.containerName}`);
+ } else if (server.runtime === 'systemd') {
+ await ssh.execCommand(`systemctl stop ${server.serviceName}`);
+ } else {
+ // Different stop commands per server type
+ const stopCmd = server.type === 'zomboid' ? 'quit' : 'stop';
+ await ssh.execCommand(`screen -S ${server.screenName} -p 0 -X stuff '${stopCmd}\n'`);
+
+ for (let i = 0; i < 30; i++) {
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ const check = await ssh.execCommand(`screen -ls | grep -E "\\.${server.screenName}[[:space:]]"`);
+ if (check.code !== 0) {
+ console.log(`Server ${server.id} stopped after ${(i + 1) * 2} seconds`);
+ return;
+ }
+ }
+
+ console.log(`Force killing ${server.id} after timeout`);
+ await ssh.execCommand(`screen -S ${server.screenName} -X quit`);
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ }
+}
+
+export async function restartServer(server) {
+ await stopServer(server);
+ await new Promise(resolve => setTimeout(resolve, 2000));
+ await startServer(server);
+}
+
+export async function getConsoleLog(server, lines = 50) {
+ const ssh = await getConnection(server.host, server.sshUser);
+
+ if (server.runtime === 'docker') {
+ const result = await ssh.execCommand(`/usr/local/bin/docker-logs-tz ${server.containerName} ${lines}`);
+ return result.stdout || result.stderr;
+ } else if (server.runtime === 'systemd') {
+ const result = await ssh.execCommand(`tail -n ${lines} ${server.workDir}/logs/VRisingServer.log 2>/dev/null || journalctl -u ${server.serviceName} -n ${lines} --no-pager`);
+ return result.stdout || result.stderr;
+ } else if (server.logFile) {
+ const result = await ssh.execCommand(`tail -n ${lines} ${server.logFile} 2>/dev/null || echo No log file found`);
+ return result.stdout;
+ } else {
+ const result = await ssh.execCommand(`tail -n ${lines} ${server.workDir}/logs/latest.log 2>/dev/null || echo No log file found`);
+ return result.stdout;
+ }
+}
+
+export async function getProcessUptime(server) {
+ try {
+ const ssh = await getConnection(server.host, server.sshUser);
+
+ if (server.runtime === "docker") {
+ const result = await ssh.execCommand(`docker inspect --format="{{.State.StartedAt}}" ${server.containerName} 2>/dev/null`);
+ if (result.stdout.trim()) {
+ const startTime = new Date(result.stdout.trim());
+ if (!isNaN(startTime.getTime())) {
+ return Math.floor((Date.now() - startTime.getTime()) / 1000);
+ }
+ }
+ } else if (server.runtime === "systemd") {
+ const result = await ssh.execCommand(`systemctl show ${server.serviceName} --property=ActiveEnterTimestamp | cut -d= -f2 | xargs -I{} date -d "{}" +%s 2>/dev/null`);
+ const startEpoch = parseInt(result.stdout.trim());
+ if (!isNaN(startEpoch)) {
+ return Math.floor(Date.now() / 1000) - startEpoch;
+ }
+ } else {
+ const result = await ssh.execCommand(`ps -o etimes= -p $(pgrep -f "SCREEN.*${server.screenName}") 2>/dev/null | head -1`);
+ const uptime = parseInt(result.stdout.trim());
+ if (!isNaN(uptime)) return uptime;
+ }
+ return 0;
+ } catch (err) {
+ console.error(`Failed to get uptime for ${server.id}:`, err.message);
+ return 0;
+ }
+}
+
+// ============ FACTORIO-SPECIFIC FUNCTIONS ============
+
+export async function listFactorioSaves(server) {
+ const ssh = await getConnection(server.host, server.sshUser);
+ const result = await ssh.execCommand('ls -la /srv/docker/factorio/data/saves/*.zip 2>/dev/null');
+
+ if (result.code !== 0 || !result.stdout.trim()) {
+ return [];
+ }
+
+ const saves = [];
+ const lines = result.stdout.trim().split('\n');
+
+ for (const line of lines) {
+ const match = line.match(/(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+\s+\d+\s+[\d:]+)\s+(.+)$/);
+ if (match) {
+ const filename = match[7].split('/').pop();
+ // Skip autosaves
+ if (filename.startsWith('_autosave')) continue;
+
+ const size = match[5];
+ const modified = match[6];
+
+ saves.push({
+ name: filename.replace('.zip', ''),
+ filename,
+ size,
+ modified
+ });
+ }
+ }
+
+ return saves;
+}
+
+export async function deleteFactorioSave(server, saveName) {
+ const ssh = await getConnection(server.host, server.sshUser);
+ const filename = saveName.endsWith('.zip') ? saveName : `${saveName}.zip`;
+
+ // Security check - prevent path traversal
+ if (filename.includes('/') || filename.includes('..')) {
+ throw new Error('Invalid save name');
+ }
+
+ const result = await ssh.execCommand(`rm /srv/docker/factorio/data/saves/${filename}`);
+ if (result.code !== 0) {
+ throw new Error(result.stderr || 'Failed to delete save');
+ }
+}
+
+export async function createFactorioWorld(server, saveName, settings) {
+ const ssh = await getConnection(server.host, server.sshUser);
+
+ // Security check
+ if (saveName.includes("/") || saveName.includes("..") || saveName.includes(" ")) {
+ throw new Error("Invalid save name - no spaces or special characters allowed");
+ }
+
+ // Write map-gen-settings.json
+ const settingsJson = JSON.stringify(settings, null, 2);
+ await ssh.execCommand(`cat > /srv/docker/factorio/data/config/map-gen-settings.json << 'SETTINGSEOF'
+${settingsJson}
+SETTINGSEOF`);
+
+ // Create new world using --create flag (correct method)
+ console.log(`Creating new Factorio world: ${saveName}`);
+ const createResult = await ssh.execCommand(`
+ docker run --rm \
+ -v /srv/docker/factorio/data:/factorio \
+ factoriotools/factorio \
+ /opt/factorio/bin/x64/factorio \
+ --create /factorio/saves/${saveName}.zip \
+ --map-gen-settings /factorio/config/map-gen-settings.json
+ `, { execOptions: { timeout: 120000 } });
+
+ console.log("World creation output:", createResult.stdout, createResult.stderr);
+
+ // Verify save was created
+ const checkResult = await ssh.execCommand(`ls /srv/docker/factorio/data/saves/${saveName}.zip`);
+ if (checkResult.code !== 0) {
+ throw new Error("World creation failed - save file not found. Output: " + createResult.stderr);
+ }
+
+ return true;
+}
+
+export async function getFactorioCurrentSave(server) {
+ const ssh = await getConnection(server.host, server.sshUser);
+
+ // Check if container has SAVE_NAME env set
+ const envResult = await ssh.execCommand(`docker inspect --format="{{range .Config.Env}}{{println .}}{{end}}" ${server.containerName} 2>/dev/null | grep "^SAVE_NAME="`);
+ if (envResult.stdout.trim()) {
+ const saveName = envResult.stdout.trim().replace("SAVE_NAME=", "");
+ return { save: saveName, source: "configured" };
+ }
+
+ // Otherwise find newest save file
+ const result = await ssh.execCommand("ls -t /srv/docker/factorio/data/saves/*.zip 2>/dev/null | head -1");
+ if (result.stdout.trim()) {
+ const filename = result.stdout.trim().split("/").pop().replace(".zip", "");
+ return { save: filename, source: "newest" };
+ }
+
+ return { save: null, source: null };
+}
diff --git a/gsm-frontend/public/zomboid.png b/gsm-frontend/public/zomboid.png
new file mode 100644
index 0000000..67d2e8a
Binary files /dev/null and b/gsm-frontend/public/zomboid.png differ
diff --git a/gsm-frontend/src/components/ServerCard.jsx b/gsm-frontend/src/components/ServerCard.jsx
index db8d6ef..dd8e665 100644
--- a/gsm-frontend/src/components/ServerCard.jsx
+++ b/gsm-frontend/src/components/ServerCard.jsx
@@ -20,6 +20,14 @@ const serverInfo = {
links: [
{ label: 'Steam', url: 'https://store.steampowered.com/app/1604030/V_Rising/' }
]
+ },
+ zomboid: {
+ hint: 'Version 42.13',
+ address: 'pz.zeasy.dev:16261',
+ logo: '/zomboid.png',
+ links: [
+ { label: 'Steam', url: 'https://store.steampowered.com/app/108600/Project_Zomboid/' }
+ ]
}
}
@@ -28,6 +36,7 @@ const getServerInfo = (serverName) => {
if (name.includes('minecraft') || name.includes('all the mods')) return serverInfo.minecraft
if (name.includes('factorio')) return serverInfo.factorio
if (name.includes('vrising') || name.includes('v rising')) return serverInfo.vrising
+ if (name.includes('zomboid')) return serverInfo.zomboid
return null
}
@@ -127,6 +136,13 @@ export default function ServerCard({ server, onClick, isAuthenticated }) {
)}
+ {/* Project Zomboid notice - only for authenticated users */}
+ {isAuthenticated && server.type === 'zomboid' && (
+
+ Version 42.13
+
+ )}
+
{/* Metrics */}
{/* CPU */}
diff --git a/gsm-frontend/src/pages/ServerDetail.jsx b/gsm-frontend/src/pages/ServerDetail.jsx
index 785644c..ce5709f 100644
--- a/gsm-frontend/src/pages/ServerDetail.jsx
+++ b/gsm-frontend/src/pages/ServerDetail.jsx
@@ -10,6 +10,7 @@ const getServerLogo = (serverName) => {
if (name.includes("minecraft") || name.includes("all the mods")) return "/minecraft.png"
if (name.includes("factorio")) return "/factorio.png"
if (name.includes("vrising") || name.includes("v rising")) return "/vrising.png"
+ if (name.includes("zomboid")) return "/zomboid.png"
return null
}
export default function ServerDetail() {
@@ -28,6 +29,7 @@ export default function ServerDetail() {
const [whitelistInput, setWhitelistInput] = useState('')
const [whitelistLoading, setWhitelistLoading] = useState(false)
const [currentSave, setCurrentSave] = useState(null)
+ const [logsUpdated, setLogsUpdated] = useState(null)
const logsRef = useRef(null)
const rconRef = useRef(null)
@@ -62,7 +64,10 @@ export default function ServerDetail() {
useEffect(() => {
fetchServer()
fetchCurrentSave()
- const interval = setInterval(fetchServer, 10000)
+ const interval = setInterval(() => {
+ fetchServer()
+ fetchCurrentSave()
+ }, 10000)
return () => clearInterval(interval)
}, [token, serverId])
@@ -97,8 +102,9 @@ export default function ServerDetail() {
const fetchLogs = async () => {
try {
- const data = await getServerLogs(token, server.id, 20)
+ const data = await getServerLogs(token, server.id, 50)
setLogs(data.logs || '')
+ setLogsUpdated(new Date())
if (logsRef.current) {
logsRef.current.scrollTop = logsRef.current.scrollHeight
}
@@ -299,6 +305,15 @@ const formatUptime = (seconds) => {
CPU Cores
{server.metrics.cpuCores}
+ {server.type === 'factorio' && currentSave?.save && (
+
+
{server.running ? 'Current World' : 'Next World'}
+
{currentSave.save}
+ {!server.running && currentSave.source === 'newest' && (
+
newest save
+ )}
+
+ )}
{/* Players List */}
@@ -318,14 +333,6 @@ const formatUptime = (seconds) => {
Server Controls
- {/* Factorio: Show which save will be loaded */}
- {server.type === 'factorio' && !server.running && currentSave?.save && (
-
- Will load: {currentSave.save}
- {currentSave.source === 'newest' && (newest save)}
-
- )}
-
{(server.status === 'online' || (server.running && server.status !== 'starting' && server.status !== 'stopping')) ? (
<>
@@ -369,7 +376,14 @@ const formatUptime = (seconds) => {
{/* Logs */}
-
Server Logs (last 20 lines)
+
+ Server Logs (last 50 lines)
+ {logsUpdated && (
+
+ Aktualisiert: {logsUpdated.toLocaleTimeString('de-DE')}
+
+ )}
+