Add Hytale server support with tmux runtime
All checks were successful
Deploy GSM / deploy (push) Successful in 29s

- Add tmux runtime support to ssh.js (status, start, stop, logs, uptime)
- Add Hytale server configuration to config.json
- Add Hytale server info and logo to frontend (ServerCard, ServerDetail)
- Add Hytale emoji to Discord notification mapping
- Update documentation with Hytale server details

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 13:39:58 +01:00
parent 8af57aa81a
commit b2dde62476
7 changed files with 74 additions and 2 deletions

View File

@@ -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

View File

@@ -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"
}
]
}

View File

@@ -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()

View File

@@ -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());

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

View File

@@ -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
}

View File

@@ -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() {