diff --git a/CLAUDE.md b/CLAUDE.md index e9ec5b5..45e67ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,7 @@ The homelab consists of: - **Palworld Server (192.168.2.53)**: Dedicated server with systemd (LXC) - **Project Zomboid Server (10.0.30.66)**: Dedicated server (external VM) - **Terraria Server (10.0.30.202)**: Vanilla server mit PM2 (external VM, VPN) +- **Hytale Server (10.0.30.204)**: Dedicated server mit tmux (external VM, VPN) ## Key Technical Details @@ -38,7 +39,7 @@ The homelab consists of: - Anzeigeeinstellungen: Superadmins können Verbindungsadresse und Hinweis pro Server anpassen - Activity Log: Protokolliert alle Aktionen mit Discord-Avatar -**Domain**: zeasy.dev with subdomains managed via Cloudflare DDNS (gsm.zeasy.dev, factorio.zeasy.dev, palworld.zeasy.dev, pz.zeasy.dev) +**Domain**: zeasy.dev with subdomains managed via Cloudflare DDNS (gsm.zeasy.dev, factorio.zeasy.dev, palworld.zeasy.dev, pz.zeasy.dev, hytale.zeasy.dev) **SSH Access**: Zugriff auf Server erfolgt über den Pi als Jump-Host: ```bash diff --git a/gsm-backend/config.json b/gsm-backend/config.json index c918e8e..56f6e6b 100644 --- a/gsm-backend/config.json +++ b/gsm-backend/config.json @@ -90,6 +90,19 @@ "sshUser": "openttd", "workDir": "/opt/openttd", "port": 3979 + }, + { + "id": "hytale", + "name": "Hytale", + "host": "10.0.30.204", + "type": "hytale", + "runtime": "tmux", + "external": true, + "tmuxName": "hytale", + "sshUser": "beeck", + "workDir": "/opt/hytale/Server", + "startCmd": "./start-server.sh", + "logCmd": "ls -t /opt/hytale/Server/logs/*.log | head -1 | xargs tail -n" } ] } diff --git a/gsm-backend/routes/servers.js b/gsm-backend/routes/servers.js index 1aa42fe..14db41f 100644 --- a/gsm-backend/routes/servers.js +++ b/gsm-backend/routes/servers.js @@ -841,7 +841,7 @@ router.post("/discord/send-update", authenticateToken, requireRole("superadmin") try { const serverIcons = { minecraft: '⛏️', factorio: '⚙️', zomboid: '🧟', vrising: '🧛', - palworld: '🦎', terraria: '⚔️', openttd: '🚂' + palworld: '🦎', terraria: '⚔️', openttd: '🚂', hytale: '🏰' }; const embed = new EmbedBuilder() diff --git a/gsm-backend/services/ssh.js b/gsm-backend/services/ssh.js index a693473..329895e 100644 --- a/gsm-backend/services/ssh.js +++ b/gsm-backend/services/ssh.js @@ -94,6 +94,15 @@ export async function getServerStatus(server) { if (proc.pm2_env.status === "stopping") return "stopping"; return "offline"; } catch { return "offline"; } + } else if (server.runtime === 'tmux') { + const result = await ssh.execCommand(`tmux has-session -t ${server.tmuxName} 2>/dev/null && echo running || echo stopped`); + if (result.stdout.trim() === 'running') { + const uptimeResult = await ssh.execCommand(`ps -o etimes= -p $(tmux list-panes -t ${server.tmuxName} -F '#{pane_pid}' 2>/dev/null | head -1) 2>/dev/null | head -1`); + const uptime = parseInt(uptimeResult.stdout.trim()) || 999; + if (uptime < 60) return 'starting'; + return 'online'; + } + return 'offline'; } else { const result = await ssh.execCommand(`screen -ls | grep -E "\\.${server.screenName}[[:space:]]"`); if (result.code === 0) { @@ -142,6 +151,10 @@ export async function startServer(server, options = {}) { } else if (server.runtime === 'pm2') { const nvmPrefix = "source ~/.nvm/nvm.sh && "; await ssh.execCommand(nvmPrefix + "pm2 start " + server.serviceName); + } else if (server.runtime === 'tmux') { + await ssh.execCommand(`tmux kill-session -t ${server.tmuxName} 2>/dev/null || true`); + await new Promise(resolve => setTimeout(resolve, 1000)); + await ssh.execCommand(`cd ${server.workDir} && tmux new-session -d -s ${server.tmuxName} '${server.startCmd}'`); } else { await ssh.execCommand(`screen -S ${server.screenName} -X quit 2>/dev/null`); await new Promise(resolve => setTimeout(resolve, 1000)); @@ -159,6 +172,24 @@ export async function stopServer(server) { } else if (server.runtime === 'pm2') { const nvmPrefix = "source ~/.nvm/nvm.sh && "; await ssh.execCommand(nvmPrefix + "pm2 stop " + server.serviceName); + } else if (server.runtime === 'tmux') { + // Send stop command via tmux + const stopCmd = server.stopCmd || 'stop'; + await ssh.execCommand(`tmux send-keys -t ${server.tmuxName} '${stopCmd}' Enter`); + + for (let i = 0; i < 30; i++) { + await new Promise(resolve => setTimeout(resolve, 2000)); + const check = await ssh.execCommand(`tmux has-session -t ${server.tmuxName} 2>/dev/null && echo running || echo stopped`); + if (check.stdout.trim() === 'stopped') { + console.log(`Server ${server.id} stopped after ${(i + 1) * 2} seconds`); + return; + } + } + + console.log(`Force killing ${server.id} after timeout`); + await ssh.execCommand(`tmux kill-session -t ${server.tmuxName} 2>/dev/null || true`); + await new Promise(resolve => setTimeout(resolve, 2000)); + return; } else { // Different stop commands per server type const stopCmd = server.type === 'zomboid' ? 'quit' : 'stop'; @@ -203,6 +234,19 @@ export async function getConsoleLog(server, lines = 50) { const nvmPrefix = "source ~/.nvm/nvm.sh && "; const result = await ssh.execCommand(nvmPrefix + "pm2 logs " + server.serviceName + " --lines " + lines + " --nostream"); return result.stdout || result.stderr; + } else if (server.runtime === 'tmux') { + // For tmux, use logCmd, logFile, or capture pane content + if (server.logCmd) { + const result = await ssh.execCommand(`${server.logCmd} ${lines} 2>/dev/null || echo No log file found`); + return result.stdout; + } 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 { + // Capture tmux pane content (last N lines) + const result = await ssh.execCommand(`tmux capture-pane -t ${server.tmuxName} -p -S -${lines} 2>/dev/null || echo Session not found`); + return result.stdout || 'No logs available'; + } } 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; @@ -241,6 +285,11 @@ export async function getProcessUptime(server) { } } catch {} return 0; + } else if (server.runtime === "tmux") { + const result = await ssh.execCommand(`ps -o etimes= -p $(tmux list-panes -t ${server.tmuxName} -F '#{pane_pid}' 2>/dev/null | head -1) 2>/dev/null | head -1`); + const uptime = parseInt(result.stdout.trim()); + if (!isNaN(uptime)) return uptime; + return 0; } 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()); diff --git a/gsm-frontend/public/hytale.png b/gsm-frontend/public/hytale.png new file mode 100644 index 0000000..581e49d Binary files /dev/null and b/gsm-frontend/public/hytale.png differ diff --git a/gsm-frontend/src/components/ServerCard.jsx b/gsm-frontend/src/components/ServerCard.jsx index 1d395d7..447358f 100644 --- a/gsm-frontend/src/components/ServerCard.jsx +++ b/gsm-frontend/src/components/ServerCard.jsx @@ -53,6 +53,13 @@ const serverInfo = { links: [ { label: 'Steam', url: 'https://store.steampowered.com/app/1536610/OpenTTD/' } ] + }, + hytale: { + address: 'hytale.zeasy.dev', + logo: '/hytale.png', + links: [ + { label: 'Website', url: 'https://hytale.com/' } + ] } } @@ -65,6 +72,7 @@ const getServerInfo = (serverName) => { if (name.includes('palworld')) return serverInfo.palworld if (name.includes('terraria')) return serverInfo.terraria if (name.includes('openttd')) return serverInfo.openttd + if (name.includes('hytale')) return serverInfo.hytale return null } diff --git a/gsm-frontend/src/pages/ServerDetail.jsx b/gsm-frontend/src/pages/ServerDetail.jsx index 7a87a2c..236c059 100644 --- a/gsm-frontend/src/pages/ServerDetail.jsx +++ b/gsm-frontend/src/pages/ServerDetail.jsx @@ -19,6 +19,7 @@ const getServerLogo = (serverName) => { if (name.includes("palworld")) return "/palworld.png" if (name.includes("terraria")) return "/terraria.png" if (name.includes("openttd")) return "/openttd.png" + if (name.includes("hytale")) return "/hytale.png" return null } export default function ServerDetail() {