xd
This commit is contained in:
File diff suppressed because one or more lines are too long
10
ZeasyWG-Alex.conf
Normal file
10
ZeasyWG-Alex.conf
Normal file
@@ -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
|
||||||
159
gsm-backend/services/rcon.js
Normal file
159
gsm-backend/services/rcon.js
Normal file
@@ -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: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
286
gsm-backend/services/ssh.js
Normal file
286
gsm-backend/services/ssh.js
Normal file
@@ -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 };
|
||||||
|
}
|
||||||
BIN
gsm-frontend/public/zomboid.png
Normal file
BIN
gsm-frontend/public/zomboid.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
@@ -20,6 +20,14 @@ const serverInfo = {
|
|||||||
links: [
|
links: [
|
||||||
{ label: 'Steam', url: 'https://store.steampowered.com/app/1604030/V_Rising/' }
|
{ 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('minecraft') || name.includes('all the mods')) return serverInfo.minecraft
|
||||||
if (name.includes('factorio')) return serverInfo.factorio
|
if (name.includes('factorio')) return serverInfo.factorio
|
||||||
if (name.includes('vrising') || name.includes('v rising')) return serverInfo.vrising
|
if (name.includes('vrising') || name.includes('v rising')) return serverInfo.vrising
|
||||||
|
if (name.includes('zomboid')) return serverInfo.zomboid
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +136,13 @@ export default function ServerCard({ server, onClick, isAuthenticated }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Project Zomboid notice - only for authenticated users */}
|
||||||
|
{isAuthenticated && server.type === 'zomboid' && (
|
||||||
|
<div className="mb-4 text-xs text-neutral-500">
|
||||||
|
Version 42.13
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Metrics */}
|
{/* Metrics */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* CPU */}
|
{/* CPU */}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const getServerLogo = (serverName) => {
|
|||||||
if (name.includes("minecraft") || name.includes("all the mods")) return "/minecraft.png"
|
if (name.includes("minecraft") || name.includes("all the mods")) return "/minecraft.png"
|
||||||
if (name.includes("factorio")) return "/factorio.png"
|
if (name.includes("factorio")) return "/factorio.png"
|
||||||
if (name.includes("vrising") || name.includes("v rising")) return "/vrising.png"
|
if (name.includes("vrising") || name.includes("v rising")) return "/vrising.png"
|
||||||
|
if (name.includes("zomboid")) return "/zomboid.png"
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
export default function ServerDetail() {
|
export default function ServerDetail() {
|
||||||
@@ -28,6 +29,7 @@ export default function ServerDetail() {
|
|||||||
const [whitelistInput, setWhitelistInput] = useState('')
|
const [whitelistInput, setWhitelistInput] = useState('')
|
||||||
const [whitelistLoading, setWhitelistLoading] = useState(false)
|
const [whitelistLoading, setWhitelistLoading] = useState(false)
|
||||||
const [currentSave, setCurrentSave] = useState(null)
|
const [currentSave, setCurrentSave] = useState(null)
|
||||||
|
const [logsUpdated, setLogsUpdated] = useState(null)
|
||||||
const logsRef = useRef(null)
|
const logsRef = useRef(null)
|
||||||
const rconRef = useRef(null)
|
const rconRef = useRef(null)
|
||||||
|
|
||||||
@@ -62,7 +64,10 @@ export default function ServerDetail() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchServer()
|
fetchServer()
|
||||||
fetchCurrentSave()
|
fetchCurrentSave()
|
||||||
const interval = setInterval(fetchServer, 10000)
|
const interval = setInterval(() => {
|
||||||
|
fetchServer()
|
||||||
|
fetchCurrentSave()
|
||||||
|
}, 10000)
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [token, serverId])
|
}, [token, serverId])
|
||||||
|
|
||||||
@@ -97,8 +102,9 @@ export default function ServerDetail() {
|
|||||||
|
|
||||||
const fetchLogs = async () => {
|
const fetchLogs = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getServerLogs(token, server.id, 20)
|
const data = await getServerLogs(token, server.id, 50)
|
||||||
setLogs(data.logs || '')
|
setLogs(data.logs || '')
|
||||||
|
setLogsUpdated(new Date())
|
||||||
if (logsRef.current) {
|
if (logsRef.current) {
|
||||||
logsRef.current.scrollTop = logsRef.current.scrollHeight
|
logsRef.current.scrollTop = logsRef.current.scrollHeight
|
||||||
}
|
}
|
||||||
@@ -299,6 +305,15 @@ const formatUptime = (seconds) => {
|
|||||||
<div className="text-sm text-neutral-400">CPU Cores</div>
|
<div className="text-sm text-neutral-400">CPU Cores</div>
|
||||||
<div className="text-2xl font-semibold text-white mt-1">{server.metrics.cpuCores}</div>
|
<div className="text-2xl font-semibold text-white mt-1">{server.metrics.cpuCores}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{server.type === 'factorio' && currentSave?.save && (
|
||||||
|
<div className="card p-4">
|
||||||
|
<div className="text-sm text-neutral-400">{server.running ? 'Current World' : 'Next World'}</div>
|
||||||
|
<div className="text-lg font-semibold text-white mt-1 truncate">{currentSave.save}</div>
|
||||||
|
{!server.running && currentSave.source === 'newest' && (
|
||||||
|
<div className="text-xs text-neutral-500 mt-1">newest save</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Players List */}
|
{/* Players List */}
|
||||||
@@ -318,14 +333,6 @@ const formatUptime = (seconds) => {
|
|||||||
<div className="card p-4">
|
<div className="card p-4">
|
||||||
<h3 className="text-sm font-medium text-neutral-300 mb-3">Server Controls</h3>
|
<h3 className="text-sm font-medium text-neutral-300 mb-3">Server Controls</h3>
|
||||||
|
|
||||||
{/* Factorio: Show which save will be loaded */}
|
|
||||||
{server.type === 'factorio' && !server.running && currentSave?.save && (
|
|
||||||
<div className="text-sm text-neutral-400 mb-3">
|
|
||||||
Will load: <span className="text-white font-medium">{currentSave.save}</span>
|
|
||||||
{currentSave.source === 'newest' && <span className="text-neutral-500 ml-1">(newest save)</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{(server.status === 'online' || (server.running && server.status !== 'starting' && server.status !== 'stopping')) ? (
|
{(server.status === 'online' || (server.running && server.status !== 'starting' && server.status !== 'stopping')) ? (
|
||||||
<>
|
<>
|
||||||
@@ -369,7 +376,14 @@ const formatUptime = (seconds) => {
|
|||||||
<div className="space-y-4 tab-content">
|
<div className="space-y-4 tab-content">
|
||||||
{/* Logs */}
|
{/* Logs */}
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-sm text-neutral-400">Server Logs (last 20 lines)</span>
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-neutral-400">Server Logs (last 50 lines)</span>
|
||||||
|
{logsUpdated && (
|
||||||
|
<span className="text-xs text-neutral-600">
|
||||||
|
Aktualisiert: {logsUpdated.toLocaleTimeString('de-DE')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<button onClick={fetchLogs} className="btn btn-secondary">
|
<button onClick={fetchLogs} className="btn btn-secondary">
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user