diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
new file mode 100644
index 0000000..ad0e2b6
--- /dev/null
+++ b/.gitea/workflows/deploy.yml
@@ -0,0 +1,63 @@
+name: Deploy GSM
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'gsm-backend/**'
+ - 'gsm-frontend/**'
+ workflow_dispatch:
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Install backend dependencies
+ run: |
+ cd gsm-backend
+ npm ci --production
+
+ - name: Install frontend dependencies and build
+ run: |
+ cd gsm-frontend
+ npm ci
+ npm run build
+
+ - name: Deploy Backend
+ uses: appleboy/scp-action@v0.1.7
+ with:
+ host: 192.168.2.30
+ username: root
+ key: ${{ secrets.SSH_DEPLOY_KEY }}
+ source: "gsm-backend/"
+ target: "/opt/gameserver-monitor/"
+ strip_components: 1
+
+ - name: Deploy Frontend
+ uses: appleboy/scp-action@v0.1.7
+ with:
+ host: 192.168.2.30
+ username: root
+ key: ${{ secrets.SSH_DEPLOY_KEY }}
+ source: "gsm-frontend/dist/"
+ target: "/opt/gameserver-monitor/frontend/"
+ strip_components: 2
+
+ - name: Restart Services
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: 192.168.2.30
+ username: root
+ key: ${{ secrets.SSH_DEPLOY_KEY }}
+ script: |
+ cd /opt/gameserver-monitor
+ pm2 restart gsm-backend || pm2 start backend/server.js --name gsm-backend
+ echo "Deploy complete!"
diff --git a/ActivityLog.jsx b/ActivityLog.jsx
deleted file mode 100644
index 8aaddd2..0000000
--- a/ActivityLog.jsx
+++ /dev/null
@@ -1,172 +0,0 @@
-import { useState, useEffect } from 'react'
-import { useUser } from '../context/UserContext'
-import { getActivityLog } from '../api'
-
-const actionLabels = {
- server_start: 'Server gestartet',
- server_stop: 'Server gestoppt',
- server_restart: 'Server neugestartet',
- rcon_command: 'RCON Befehl',
- autoshutdown_config: 'Auto-Shutdown geändert',
- zomboid_config: 'Config geändert',
- factorio_world_create: 'Welt erstellt',
- factorio_world_delete: 'Welt gelöscht'
-}
-
-const actionIcons = {
- server_start: '▶️',
- server_stop: '⏹️',
- server_restart: '🔄',
- rcon_command: '💻',
- autoshutdown_config: '⏱️',
- zomboid_config: '📝',
- factorio_world_create: '🌍',
- factorio_world_delete: '🗑️'
-}
-
-const serverLabels = {
- minecraft: 'Minecraft',
- factorio: 'Factorio',
- zomboid: 'Project Zomboid',
- vrising: 'V Rising'
-}
-
-function getAvatarUrl(discordId, avatar) {
- if (!discordId || !avatar) return null
- return `https://cdn.discordapp.com/avatars/${discordId}/${avatar}.png?size=32`
-}
-
-function getDiscordProfileUrl(discordId) {
- return `https://discord.com/users/${discordId}`
-}
-
-export default function ActivityLog({ onClose }) {
- const { token } = useUser()
- const [logs, setLogs] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState('')
-
- useEffect(() => {
- const fetchLogs = async () => {
- try {
- const data = await getActivityLog(token, 100)
- setLogs(data)
- setError('')
- } catch (err) {
- setError('Fehler beim Laden des Activity Logs')
- } finally {
- setLoading(false)
- }
- }
- fetchLogs()
- }, [token])
-
- const formatDate = (dateStr) => {
- const date = new Date(dateStr + 'Z')
- const now = new Date()
- const diff = now - date
-
- if (diff < 60000) return 'Gerade eben'
- if (diff < 3600000) return Math.floor(diff / 60000) + ' Min'
- if (diff < 86400000) return Math.floor(diff / 3600000) + ' Std'
-
- return date.toLocaleDateString('de-DE', {
- day: '2-digit',
- month: '2-digit',
- hour: '2-digit',
- minute: '2-digit'
- })
- }
-
- return (
-
-
e.stopPropagation()}>
-
-
Activity Log
-
-
-
-
- {error && (
-
{error}
- )}
-
- {loading ? (
-
Laden...
- ) : logs.length === 0 ? (
-
Noch keine Aktivitäten
- ) : (
-
- {logs.map((log) => {
- const avatarUrl = getAvatarUrl(log.discord_id, log.avatar)
- const profileUrl = log.discord_id ? getDiscordProfileUrl(log.discord_id) : null
-
- return (
-
- {/* Avatar or Action Icon */}
- {avatarUrl ? (
-
-
-
- ) : (
-
-
- {log.username?.charAt(0)?.toUpperCase()}
-
-
- )}
-
-
-
- {profileUrl ? (
-
- {log.username}
-
- ) : (
-
{log.username}
- )}
-
- {actionIcons[log.action] || '📋'} {actionLabels[log.action] || log.action}
-
- {log.target && (
-
- {serverLabels[log.target] || log.target}
-
- )}
-
- {log.details && (
-
- {log.details}
-
- )}
-
-
-
- {formatDate(log.created_at)}
-
-
- )
- })}
-
- )}
-
-
-
- )
-}
diff --git a/App.jsx b/App.jsx
deleted file mode 100644
index 6991efa..0000000
--- a/App.jsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import { useState, useEffect } from 'react'
-import { BrowserRouter, Routes, Route, Navigate, useSearchParams, useNavigate } from 'react-router-dom'
-import { UserProvider } from './context/UserContext'
-import Dashboard from './pages/Dashboard'
-import ServerDetail from './pages/ServerDetail'
-
-// OAuth Callback Handler
-function AuthCallback({ onLogin }) {
- const [searchParams] = useSearchParams()
- const navigate = useNavigate()
- const [error, setError] = useState(null)
-
- useEffect(() => {
- const token = searchParams.get('token')
- const errorParam = searchParams.get('error')
-
- if (errorParam) {
- const errorMessages = {
- 'discord_denied': 'Discord-Anmeldung abgebrochen',
- 'no_code': 'Kein Autorisierungscode erhalten',
- 'not_in_guild': 'Du bist nicht Mitglied des Discord-Servers',
- 'oauth_failed': 'Anmeldung fehlgeschlagen'
- }
- setError(errorMessages[errorParam] || 'Unbekannter Fehler')
- setTimeout(() => navigate('/'), 3000)
- return
- }
-
- if (token) {
- onLogin(token)
- navigate('/')
- } else {
- navigate('/')
- }
- }, [searchParams, onLogin, navigate])
-
- if (error) {
- return (
-
-
-
{error}
-
Weiterleitung...
-
-
- )
- }
-
- return (
-
-
Anmeldung wird verarbeitet...
-
- )
-}
-
-// Login page for unauthenticated users
-function LoginPage() {
- const [searchParams] = useSearchParams()
- const error = searchParams.get('error')
-
- const errorMessages = {
- 'discord_denied': 'Discord-Anmeldung abgebrochen',
- 'no_code': 'Kein Autorisierungscode erhalten',
- 'not_in_guild': 'Du bist nicht Mitglied des Discord-Servers',
- 'oauth_failed': 'Anmeldung fehlgeschlagen'
- }
-
- const handleDiscordLogin = () => {
- window.location.href = '/api/auth/discord'
- }
-
- return (
-
-
-

-
Gameserver Management
-
Melde dich mit Discord an, um fortzufahren
-
- {error && (
-
- {errorMessages[error] || 'Anmeldung fehlgeschlagen'}
-
- )}
-
-
-
-
- Nur für Mitglieder des Discord-Servers
-
-
-
- )
-}
-
-export default function App() {
- const [token, setToken] = useState(localStorage.getItem('gsm_token'))
-
- const handleLogin = (newToken) => {
- localStorage.setItem('gsm_token', newToken)
- setToken(newToken)
- }
-
- const handleLogout = () => {
- localStorage.removeItem('gsm_token')
- setToken(null)
- }
-
- return (
-
-
-
-
- ) : (
-
- )
- }
- />
-
- ) : (
-
- )
- }
- />
- }
- />
- }
- />
- }
- />
- } />
-
-
-
- )
-}
diff --git a/CLAUDE.md b/CLAUDE.md
index b37470d..46870ab 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -17,6 +17,7 @@ The homelab consists of:
- **V Rising Server (192.168.2.52)**: Dedicated server (LXC)
- **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)
## Key Technical Details
diff --git a/Dashboard.jsx b/Dashboard.jsx
deleted file mode 100644
index ff2ff6c..0000000
--- a/Dashboard.jsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import { useState, useEffect } from 'react'
-import { useNavigate } from 'react-router-dom'
-import { getServers } from '../api'
-import { useUser } from '../context/UserContext'
-import ServerCard from '../components/ServerCard'
-import UserManagement from '../components/UserManagement'
-import ActivityLog from '../components/ActivityLog'
-
-export default function Dashboard({ onLogout }) {
- const navigate = useNavigate()
- const { user, token, loading: userLoading, isSuperadmin, role, avatarUrl } = useUser()
- const [servers, setServers] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState('')
- const [showUserMgmt, setShowUserMgmt] = useState(false)
- const [showActivityLog, setShowActivityLog] = useState(false)
-
- const fetchServers = async () => {
- try {
- const data = await getServers(token)
- setServers(data)
- setError('')
- } catch (err) {
- if (err.message.includes('401') || err.message.includes('403')) {
- onLogout()
- } else {
- setError('Verbindung zum Server fehlgeschlagen')
- }
- } finally {
- setLoading(false)
- }
- }
-
- useEffect(() => {
- if (!userLoading && token) {
- fetchServers()
- const interval = setInterval(fetchServers, 10000)
- return () => clearInterval(interval)
- }
- }, [token, userLoading])
-
- const roleLabels = {
- user: 'Viewer',
- moderator: 'Operator',
- superadmin: 'Admin'
- }
-
- if (userLoading) {
- return (
-
- )
- }
-
- const onlineCount = servers.filter(s => s.running).length
- document.title = 'Dashboard | Zeasy GSM'
- const totalPlayers = servers.reduce((sum, s) => sum + (s.players?.online || 0), 0)
-
- return (
-
- {/* Header */}
-
-
-
-
-
-
-
-
-
Gameserver Management
-
-
-
- {onlineCount}/{servers.length} online
-
- |
-
- {totalPlayers} Spieler
-
-
-
- {/* User info with Discord avatar */}
-
- {avatarUrl && (
-

- )}
-
-
{user?.username}
-
{roleLabels[role]}
-
-
-
- {isSuperadmin && (
- <>
-
-
- >
- )}
-
-
-
-
-
-
- {/* Main Content */}
-
- {error && (
-
- {error}
-
- )}
- {loading ? (
-
-
Server werden geladen...
-
- ) : (
-
- {servers.map((server, index) => (
-
- navigate('/server/' + server.id)}
- isAuthenticated={true}
- />
-
- ))}
-
- )}
-
-
- {/* Modals */}
- {showUserMgmt && (
-
setShowUserMgmt(false)} />
- )}
- {showActivityLog && (
- setShowActivityLog(false)} />
- )}
-
- )
-}
diff --git a/UserContext.jsx b/UserContext.jsx
deleted file mode 100644
index 86287cd..0000000
--- a/UserContext.jsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { createContext, useContext, useState, useEffect } from 'react'
-import { getMe } from '../api'
-
-const UserContext = createContext(null)
-
-export function UserProvider({ children, token }) {
- const [user, setUser] = useState(null)
- const [loading, setLoading] = useState(true)
-
- useEffect(() => {
- if (!token) {
- setUser(null)
- setLoading(false)
- return
- }
-
- getMe(token)
- .then(data => {
- setUser(data)
- setLoading(false)
- })
- .catch(() => {
- // Token invalid, clear it
- localStorage.removeItem('gsm_token')
- setUser(null)
- setLoading(false)
- })
- }, [token])
-
- // Get Discord avatar URL
- const getAvatarUrl = () => {
- if (!user?.discordId || !user?.avatar) {
- return null
- }
- return `https://cdn.discordapp.com/avatars/${user.discordId}/${user.avatar}.png?size=64`
- }
-
- const value = {
- user,
- token,
- loading,
- role: user?.role || 'user',
- isModerator: ['moderator', 'superadmin'].includes(user?.role),
- isSuperadmin: user?.role === 'superadmin',
- avatarUrl: getAvatarUrl()
- }
-
- return (
-
- {children}
-
- )
-}
-
-export function useUser() {
- const context = useContext(UserContext)
- if (!context) {
- throw new Error('useUser must be used within UserProvider')
- }
- return context
-}
diff --git a/UserManagement.jsx b/UserManagement.jsx
deleted file mode 100644
index cf2f285..0000000
--- a/UserManagement.jsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { useState, useEffect } from 'react'
-import { useUser } from '../context/UserContext'
-import { getUsers } from '../api'
-
-export default function UserManagement({ onClose }) {
- const { token } = useUser()
- const [users, setUsers] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState('')
-
- useEffect(() => {
- const fetchUsers = async () => {
- try {
- const data = await getUsers(token)
- setUsers(data)
- setError('')
- } catch (err) {
- setError('Fehler beim Laden der Benutzer')
- } finally {
- setLoading(false)
- }
- }
- fetchUsers()
- }, [token])
-
- const roleLabels = {
- user: 'Viewer',
- moderator: 'Operator',
- superadmin: 'Admin'
- }
-
- const roleColors = {
- user: 'text-neutral-400',
- moderator: 'text-blue-400',
- superadmin: 'text-yellow-400'
- }
-
- const getAvatarUrl = (user) => {
- if (!user.discord_id || !user.avatar) return null
- return `https://cdn.discordapp.com/avatars/${user.discord_id}/${user.avatar}.png?size=64`
- }
-
- return (
-
-
e.stopPropagation()}>
-
-
Benutzerliste
-
-
-
-
- {error && (
-
{error}
- )}
-
-
- Benutzer, die sich über Discord angemeldet haben. Rollen werden durch Discord-Rollen bestimmt.
-
-
- {loading ? (
-
Laden...
- ) : users.length === 0 ? (
-
Noch keine Benutzer angemeldet
- ) : (
-
- {users.map((user) => (
-
- {getAvatarUrl(user) ? (
-
})
- ) : (
-
-
- {user.username?.charAt(0)?.toUpperCase()}
-
-
- )}
-
-
-
{user.username}
-
- {user.discord_id}
-
-
-
-
- {roleLabels[user.role]}
-
-
- ))}
-
- )}
-
-
-
- )
-}
diff --git a/ZeasyWG-Alex.conf b/ZeasyWG-Alex.conf
deleted file mode 100644
index 028fdaa..0000000
--- a/ZeasyWG-Alex.conf
+++ /dev/null
@@ -1,10 +0,0 @@
-[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/ZomboidConfigEditor.jsx b/ZomboidConfigEditor.jsx
deleted file mode 100644
index 493bac0..0000000
--- a/ZomboidConfigEditor.jsx
+++ /dev/null
@@ -1,326 +0,0 @@
-import { useState, useEffect, useRef } from 'react'
-import { FileText, Save, RefreshCw, AlertTriangle, Check, X, ChevronDown } from 'lucide-react'
-import { getZomboidConfigs, getZomboidConfig, saveZomboidConfig } from '../api'
-
-export default function ZomboidConfigEditor({ token }) {
- const [files, setFiles] = useState([])
- const [selectedFile, setSelectedFile] = useState(null)
- const [content, setContent] = useState('')
- const [originalContent, setOriginalContent] = useState('')
- const [loading, setLoading] = useState(true)
- const [saving, setSaving] = useState(false)
- const [error, setError] = useState(null)
- const [success, setSuccess] = useState(null)
- const [hasChanges, setHasChanges] = useState(false)
- const textareaRef = useRef(null)
- const highlightRef = useRef(null)
-
- // Load file list
- useEffect(() => {
- loadFiles()
- }, [token])
-
- // Track changes
- useEffect(() => {
- setHasChanges(content !== originalContent)
- }, [content, originalContent])
-
- // Sync scroll between textarea and highlight div
- const handleScroll = () => {
- if (highlightRef.current && textareaRef.current) {
- highlightRef.current.scrollTop = textareaRef.current.scrollTop
- highlightRef.current.scrollLeft = textareaRef.current.scrollLeft
- }
- }
-
- async function loadFiles() {
- setLoading(true)
- setError(null)
- try {
- const data = await getZomboidConfigs(token)
- setFiles(data.files || [])
- if (data.files?.length > 0 && !selectedFile) {
- loadFile(data.files[0].filename)
- }
- } catch (err) {
- setError('Fehler beim Laden der Config-Dateien: ' + err.message)
- } finally {
- setLoading(false)
- }
- }
-
- async function loadFile(filename) {
- setLoading(true)
- setError(null)
- setSuccess(null)
- try {
- const data = await getZomboidConfig(token, filename)
- setSelectedFile(filename)
- setContent(data.content)
- setOriginalContent(data.content)
- } catch (err) {
- setError('Fehler beim Laden: ' + err.message)
- } finally {
- setLoading(false)
- }
- }
-
- async function handleSave() {
- if (!selectedFile || !hasChanges) return
-
- setSaving(true)
- setError(null)
- setSuccess(null)
- try {
- await saveZomboidConfig(token, selectedFile, content)
- setOriginalContent(content)
- setSuccess('Config gespeichert! Server-Neustart erforderlich für Änderungen.')
- setTimeout(() => setSuccess(null), 5000)
- } catch (err) {
- setError('Fehler beim Speichern: ' + err.message)
- } finally {
- setSaving(false)
- }
- }
-
- function handleDiscard() {
- setContent(originalContent)
- setError(null)
- setSuccess(null)
- }
-
- function getFileDescription(filename) {
- const descriptions = {
- 'Project.ini': 'Server-Einstellungen (PVP, Spieler, Netzwerk)',
- 'Project_SandboxVars.lua': 'Gameplay-Einstellungen (Zombies, Loot, Schwierigkeit)',
- 'Project_spawnpoints.lua': 'Spawn-Punkte für neue Spieler',
- 'Project_spawnregions.lua': 'Spawn-Regionen Konfiguration'
- }
- return descriptions[filename] || filename
- }
-
- // Highlight syntax based on file type
- function highlightSyntax(text, filename) {
- if (!text) return ''
-
- const isLua = filename?.endsWith('.lua')
- const lines = text.split('\n')
-
- return lines.map((line, i) => {
- let highlighted = line
- .replace(/&/g, '&')
- .replace(//g, '>')
-
- if (isLua) {
- // Lua: -- comments
- if (line.trim().startsWith('--')) {
- highlighted = `${highlighted}`
- } else if (line.includes('--')) {
- const idx = line.indexOf('--')
- const code = highlighted.substring(0, idx)
- const comment = highlighted.substring(idx)
- highlighted = `${code}${comment}`
- }
- // Highlight true/false/nil
- highlighted = highlighted
- .replace(/\b(true|false|nil)\b/g, '$1')
- // Highlight numbers
- highlighted = highlighted
- .replace(/\b(\d+\.?\d*)\b/g, '$1')
- } else {
- // INI: # comments
- if (line.trim().startsWith('#')) {
- highlighted = `${highlighted}`
- }
- // Highlight key=value
- else if (line.includes('=')) {
- const idx = line.indexOf('=')
- const key = highlighted.substring(0, idx)
- const value = highlighted.substring(idx + 1)
- highlighted = `${key}=${value}`
- }
- }
-
- return highlighted
- }).join('\n')
- }
-
- if (loading && files.length === 0) {
- return (
-
-
- Lade Config-Dateien...
-
- )
- }
-
- return (
-
- {/* File selector */}
-
-
-
-
-
-
-
-
-
- {/* File description */}
- {selectedFile && (
-
-
- {getFileDescription(selectedFile)}
-
- )}
-
- {/* Error/Success messages */}
- {error && (
-
- )}
-
- {success && (
-
-
- {success}
-
- )}
-
- {/* Editor with syntax highlighting */}
-
- {/* Highlighted background layer */}
-
-
- {/* Transparent textarea for editing */}
-
-
- {/* Actions */}
-
-
- {selectedFile && files.find(f => f.filename === selectedFile)?.modified && (
- Zuletzt geändert: {files.find(f => f.filename === selectedFile).modified}
- )}
-
-
-
- {hasChanges && (
-
- )}
-
-
-
-
-
- {/* Legend */}
-
-
Legende
-
-
-
- Kommentare
-
- {selectedFile?.endsWith('.ini') && (
- <>
-
-
- Einstellung
-
-
-
- Wert
-
- >
- )}
- {selectedFile?.endsWith('.lua') && (
- <>
-
-
- Boolean/Nil
-
-
-
- Zahlen
-
- >
- )}
-
-
Änderungen werden erst nach Server-Neustart aktiv. Ein Backup wird automatisch erstellt.
-
-
- )
-}
diff --git a/auth.js b/auth.js
deleted file mode 100644
index 49d8612..0000000
--- a/auth.js
+++ /dev/null
@@ -1,244 +0,0 @@
-import { Router } from 'express';
-import jwt from 'jsonwebtoken';
-import { db, VALID_ROLES, initDiscordUsers } from '../db/init.js';
-import { authenticateToken, requireRole } from '../middleware/auth.js';
-import { getDiscordAuthUrl, exchangeCode, getDiscordUser, getGuildMember, getGuildMemberships, getUserRole, getUserRoleFromMemberships } from '../services/discord.js';
-
-const router = Router();
-
-// Initialize Discord users table
-initDiscordUsers();
-
-// ===== Guest Login =====
-
-// Create guest token (view-only, expires in 24h)
-router.post('/guest', (req, res) => {
- const guestId = 'guest_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9);
-
- const token = jwt.sign(
- {
- id: guestId,
- username: 'Gast',
- role: 'guest',
- isGuest: true
- },
- process.env.JWT_SECRET,
- { expiresIn: '24h' }
- );
-
- res.json({ token });
-});
-
-// ===== Discord OAuth2 =====
-
-// Start Discord OAuth2 flow
-router.get('/discord', (req, res) => {
- res.redirect(getDiscordAuthUrl());
-});
-
-// Discord OAuth2 callback
-router.get('/discord/callback', async (req, res) => {
- const { code, error } = req.query;
-
- // Redirect URL for frontend
- const frontendUrl = process.env.FRONTEND_URL || 'https://gsm.dimension47.de';
-
- if (error) {
- return res.redirect(`${frontendUrl}/login?error=discord_denied`);
- }
-
- if (!code) {
- return res.redirect(`${frontendUrl}/login?error=no_code`);
- }
-
- try {
- // Exchange code for access token
- const tokenData = await exchangeCode(code);
-
- // Get Discord user info
- const discordUser = await getDiscordUser(tokenData.access_token);
-
- // Check if user is in any of the configured guilds
- const memberships = await getGuildMemberships(discordUser.id);
-
- if (!memberships) {
- return res.redirect(`${frontendUrl}/login?error=not_in_guild`);
- }
-
- // Determine role based on Discord roles (highest role from all servers)
- const role = getUserRoleFromMemberships(memberships);
-
- // Use first membership for display name
- const member = memberships[0].member;
-
- // Get display name (nickname or username)
- const displayName = member.nick || discordUser.global_name || discordUser.username;
-
- // Upsert user in database
- const existingUser = db.prepare('SELECT * FROM discord_users WHERE discord_id = ?').get(discordUser.id);
-
- let userId;
- if (existingUser) {
- // Update existing user
- db.prepare(`
- UPDATE discord_users
- SET username = ?, discriminator = ?, avatar = ?, role = ?, updated_at = CURRENT_TIMESTAMP
- WHERE discord_id = ?
- `).run(displayName, discordUser.discriminator || '0', discordUser.avatar, role, discordUser.id);
- userId = existingUser.id;
- } else {
- // Create new user
- const result = db.prepare(`
- INSERT INTO discord_users (discord_id, username, discriminator, avatar, role)
- VALUES (?, ?, ?, ?, ?)
- `).run(discordUser.id, displayName, discordUser.discriminator || '0', discordUser.avatar, role);
- userId = result.lastInsertRowid;
- }
-
- // Create JWT token
- const token = jwt.sign(
- {
- id: userId,
- discordId: discordUser.id,
- username: displayName,
- role,
- avatar: discordUser.avatar
- },
- process.env.JWT_SECRET,
- { expiresIn: '7d' }
- );
-
- // Redirect to frontend with token
- res.redirect(`${frontendUrl}/auth/callback?token=${token}`);
-
- } catch (err) {
- console.error('Discord OAuth error:', err);
- res.redirect(`${frontendUrl}/login?error=oauth_failed`);
- }
-});
-
-// Get current user info
-router.get('/me', authenticateToken, (req, res) => {
- // Check if it's a guest user
- if (req.user.isGuest) {
- return res.json({
- id: req.user.id,
- username: req.user.username,
- role: req.user.role,
- isGuest: true
- });
- }
-
- // Check if it's a Discord user
- if (req.user.discordId) {
- const user = db.prepare('SELECT id, discord_id, username, avatar, role FROM discord_users WHERE id = ?').get(req.user.id);
- if (!user) {
- return res.status(404).json({ error: 'User not found' });
- }
- return res.json({
- id: user.id,
- discordId: user.discord_id,
- username: user.username,
- avatar: user.avatar,
- role: user.role
- });
- }
-
- // Fallback for old users (shouldn't happen after migration)
- const user = db.prepare('SELECT id, username, role FROM users WHERE id = ?').get(req.user.id);
- if (!user) {
- return res.status(404).json({ error: 'User not found' });
- }
- res.json({ id: user.id, username: user.username, role: user.role });
-});
-
-// Refresh user role from Discord (useful if roles changed)
-router.post('/refresh-role', authenticateToken, async (req, res) => {
- if (!req.user.discordId) {
- return res.status(400).json({ error: 'Not a Discord user' });
- }
-
- try {
- const memberships = await getGuildMemberships(req.user.discordId);
-
- if (!memberships) {
- return res.status(403).json({ error: 'No longer in any guild' });
- }
-
- const newRole = getUserRoleFromMemberships(memberships);
-
- db.prepare('UPDATE discord_users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE discord_id = ?')
- .run(newRole, req.user.discordId);
-
- // Generate new token with updated role
- const token = jwt.sign(
- {
- id: req.user.id,
- discordId: req.user.discordId,
- username: req.user.username,
- role: newRole,
- avatar: req.user.avatar
- },
- process.env.JWT_SECRET,
- { expiresIn: '7d' }
- );
-
- res.json({ token, role: newRole });
- } catch (err) {
- console.error('Failed to refresh role:', err);
- res.status(500).json({ error: 'Failed to refresh role' });
- }
-});
-
-// ===== User Management (superadmin only) =====
-
-// Get all Discord users
-router.get('/users', authenticateToken, requireRole('superadmin'), (req, res) => {
- const users = db.prepare(`
- SELECT id, discord_id, username, avatar, role, created_at, updated_at
- FROM discord_users
- ORDER BY created_at DESC
- `).all();
- res.json(users);
-});
-
-// Update user role (override Discord role)
-router.patch('/users/:id/role', authenticateToken, requireRole('superadmin'), (req, res) => {
- const userId = parseInt(req.params.id);
- const { role } = req.body;
-
- if (!VALID_ROLES.includes(role)) {
- return res.status(400).json({ error: 'Invalid role' });
- }
-
- if (userId === req.user.id) {
- return res.status(400).json({ error: 'Cannot change your own role' });
- }
-
- const user = db.prepare('SELECT id FROM discord_users WHERE id = ?').get(userId);
- if (!user) {
- return res.status(404).json({ error: 'User not found' });
- }
-
- db.prepare('UPDATE discord_users SET role = ? WHERE id = ?').run(role, userId);
- res.json({ message: 'Role updated' });
-});
-
-// Delete user
-router.delete('/users/:id', authenticateToken, requireRole('superadmin'), (req, res) => {
- const userId = parseInt(req.params.id);
-
- if (userId === req.user.id) {
- return res.status(400).json({ error: 'Cannot delete yourself' });
- }
-
- const user = db.prepare('SELECT id FROM discord_users WHERE id = ?').get(userId);
- if (!user) {
- return res.status(404).json({ error: 'User not found' });
- }
-
- db.prepare('DELETE FROM discord_users WHERE id = ?').run(userId);
- res.json({ message: 'User deleted' });
-});
-
-export default router;
diff --git a/discord.js b/discord.js
deleted file mode 100644
index 5e2e8a4..0000000
--- a/discord.js
+++ /dev/null
@@ -1,167 +0,0 @@
-// Discord OAuth2 Service
-const DISCORD_API = 'https://discord.com/api/v10';
-
-// Lazy initialization - wird erst bei Verwendung geladen (nach dotenv)
-let _guildConfigs = null;
-
-function getGuildConfigs() {
- if (_guildConfigs === null) {
- _guildConfigs = [
- {
- name: 'Bacanaks',
- guildId: process.env.DISCORD_GUILD_ID_1,
- adminRoleId: process.env.DISCORD_ADMIN_ROLE_ID_1,
- modRoleId: process.env.DISCORD_MOD_ROLE_ID_1
- },
- {
- name: 'Piccadilly',
- guildId: process.env.DISCORD_GUILD_ID_2,
- adminRoleId: process.env.DISCORD_ADMIN_ROLE_ID_2,
- modRoleId: process.env.DISCORD_MOD_ROLE_ID_2
- }
- ].filter(config => config.guildId);
- }
- return _guildConfigs;
-}
-
-export function getDiscordAuthUrl() {
- const params = new URLSearchParams({
- client_id: process.env.DISCORD_CLIENT_ID,
- redirect_uri: process.env.DISCORD_REDIRECT_URI,
- response_type: 'code',
- scope: 'identify guilds.members.read'
- });
- return `https://discord.com/oauth2/authorize?${params}`;
-}
-
-export async function exchangeCode(code) {
- const response = await fetch(`${DISCORD_API}/oauth2/token`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded'
- },
- body: new URLSearchParams({
- client_id: process.env.DISCORD_CLIENT_ID,
- client_secret: process.env.DISCORD_CLIENT_SECRET,
- grant_type: 'authorization_code',
- code,
- redirect_uri: process.env.DISCORD_REDIRECT_URI
- })
- });
-
- if (!response.ok) {
- const error = await response.text();
- throw new Error(`Failed to exchange code: ${error}`);
- }
-
- return response.json();
-}
-
-export async function getDiscordUser(accessToken) {
- const response = await fetch(`${DISCORD_API}/users/@me`, {
- headers: {
- Authorization: `Bearer ${accessToken}`
- }
- });
-
- if (!response.ok) {
- throw new Error('Failed to get Discord user');
- }
-
- return response.json();
-}
-
-// Prüft einen einzelnen Server
-async function fetchGuildMember(guildId, userId) {
- const response = await fetch(
- `${DISCORD_API}/guilds/${guildId}/members/${userId}`,
- {
- headers: {
- Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`
- }
- }
- );
-
- if (!response.ok) {
- if (response.status === 404) {
- return null;
- }
- throw new Error(`Failed to get guild member from ${guildId}`);
- }
-
- return response.json();
-}
-
-// Prüft alle konfigurierten Server und gibt Memberships zurück
-export async function getGuildMemberships(userId) {
- const configs = getGuildConfigs();
- const memberships = [];
-
- for (const config of configs) {
- try {
- const member = await fetchGuildMember(config.guildId, userId);
- if (member) {
- memberships.push({
- config,
- member,
- roles: member.roles || []
- });
- }
- } catch (err) {
- console.error(`[Discord] Failed to check membership for guild ${config.name}:`, err.message);
- }
- }
-
- return memberships.length > 0 ? memberships : null;
-}
-
-// Legacy-Funktion für Kompatibilität
-export async function getGuildMember(userId) {
- const memberships = await getGuildMemberships(userId);
- if (!memberships || memberships.length === 0) {
- return null;
- }
- return memberships[0].member;
-}
-
-// Rollen-Priorität: superadmin > moderator > user
-const ROLE_PRIORITY = { superadmin: 3, moderator: 2, user: 1 };
-
-// Bestimmt die höchste Rolle aus allen Server-Memberships
-export function getUserRoleFromMemberships(memberships) {
- if (!memberships || memberships.length === 0) {
- return 'user';
- }
-
- let highestRole = 'user';
-
- for (const { config, roles } of memberships) {
- let role = 'user';
-
- if (roles.includes(config.adminRoleId)) {
- role = 'superadmin';
- } else if (roles.includes(config.modRoleId)) {
- role = 'moderator';
- }
-
- if (ROLE_PRIORITY[role] > ROLE_PRIORITY[highestRole]) {
- highestRole = role;
- }
- }
-
- return highestRole;
-}
-
-// Legacy-Funktion für Kompatibilität
-export function getUserRole(memberRoles) {
- const adminRoleId = process.env.DISCORD_ADMIN_ROLE_ID || process.env.DISCORD_ADMIN_ROLE_ID_1;
- const modRoleId = process.env.DISCORD_MOD_ROLE_ID || process.env.DISCORD_MOD_ROLE_ID_1;
-
- if (memberRoles.includes(adminRoleId)) {
- return 'superadmin';
- }
- if (memberRoles.includes(modRoleId)) {
- return 'moderator';
- }
- return 'user';
-}
diff --git a/discordBot.js b/discordBot.js
deleted file mode 100644
index f6ae4c7..0000000
--- a/discordBot.js
+++ /dev/null
@@ -1,353 +0,0 @@
-import { Client, GatewayIntentBits, EmbedBuilder } from 'discord.js';
-import { readFileSync } from 'fs';
-import { dirname, join } from 'path';
-import { fileURLToPath } from 'url';
-import { getServerStatus } from './ssh.js';
-import { getPlayers, getPlayerList } from './rcon.js';
-
-const __dirname = dirname(fileURLToPath(import.meta.url));
-
-let client = null;
-let statusMessageId = null;
-let statusChannelId = null;
-let infoChannelId = null;
-let alertChannelId = null;
-
-// State tracking for alerts
-const previousServerState = new Map();
-const previousPlayerLists = new Map();
-
-// Server display config
-const serverDisplay = {
- minecraft: { name: 'Minecraft ATM10', icon: '⛏️', color: 0x7B5E3C, address: 'minecraft.zeasy.dev' },
- factorio: { name: 'Factorio', icon: '⚙️', color: 0xF97316, address: 'factorio.zeasy.dev' },
- zomboid: { name: 'Project Zomboid', icon: '🧟', color: 0x4ADE80, address: 'zomboid.zeasy.dev' },
- vrising: { name: 'V Rising', icon: '🧛', color: 0xDC2626, address: 'vrising.zeasy.dev' }
-};
-
-function loadConfig() {
- return JSON.parse(readFileSync(join(__dirname, '..', 'config.json'), 'utf-8'));
-}
-
-async function sendAlert(embed) {
- if (!client || !alertChannelId) return;
-
- try {
- const channel = await client.channels.fetch(alertChannelId);
- if (channel) {
- await channel.send({ embeds: [embed] });
- }
- } catch (err) {
- console.error('[DiscordBot] Error sending alert:', err.message);
- }
-}
-
-async function checkAndSendAlerts(serverStatuses) {
- for (const server of serverStatuses) {
- const display = serverDisplay[server.id] || { name: server.name, icon: '🖥️', color: 0x6B7280 };
- const prevState = previousServerState.get(server.id);
- const prevPlayers = previousPlayerLists.get(server.id) || [];
-
- // Check server status changes
- if (prevState !== undefined && prevState !== server.status) {
- let embed;
-
- if (server.status === 'online' && prevState !== 'online') {
- embed = new EmbedBuilder()
- .setTitle(display.icon + ' Server gestartet')
- .setDescription('**' + display.name + '** ist jetzt online')
- .setColor(0x22C55E)
- .setTimestamp();
- } else if (server.status === 'offline' && prevState === 'online') {
- embed = new EmbedBuilder()
- .setTitle(display.icon + ' Server gestoppt')
- .setDescription('**' + display.name + '** ist jetzt offline')
- .setColor(0xEF4444)
- .setTimestamp();
- } else if (server.status === 'unreachable' && prevState !== 'unreachable') {
- embed = new EmbedBuilder()
- .setTitle('⚠️ Server nicht erreichbar')
- .setDescription('**' + display.name + '** ist nicht erreichbar')
- .setColor(0xF59E0B)
- .setTimestamp();
- }
-
- if (embed) {
- await sendAlert(embed);
- }
- }
-
- // Check player changes (only if server is online)
- if (server.status === 'online' && server.playerList) {
- const currentPlayers = server.playerList;
-
- // Find players who joined
- for (const player of currentPlayers) {
- if (!prevPlayers.includes(player)) {
- const embed = new EmbedBuilder()
- .setTitle('➡️ Spieler beigetreten')
- .setDescription('**' + player + '** hat **' + display.name + '** betreten')
- .setColor(0x22C55E)
- .setTimestamp();
- await sendAlert(embed);
- }
- }
-
- // Find players who left
- for (const player of prevPlayers) {
- if (!currentPlayers.includes(player)) {
- const embed = new EmbedBuilder()
- .setTitle('⬅️ Spieler verlassen')
- .setDescription('**' + player + '** hat **' + display.name + '** verlassen')
- .setColor(0xF59E0B)
- .setTimestamp();
- await sendAlert(embed);
- }
- }
-
- previousPlayerLists.set(server.id, [...currentPlayers]);
- } else {
- previousPlayerLists.set(server.id, []);
- }
-
- // Update previous state
- previousServerState.set(server.id, server.status);
- }
-}
-
-async function setupInfoMessage() {
- try {
- const channel = await client.channels.fetch(infoChannelId);
- if (!channel) {
- console.error('[DiscordBot] Info channel not found');
- return;
- }
-
- const messages = await channel.messages.fetch({ limit: 10 });
- const botMessage = messages.find(m => m.author.id === client.user.id);
-
- const infoEmbed = new EmbedBuilder()
- .setTitle('🎮 Zeasy Gameserver Management')
- .setDescription(
- 'Verwalte und überwache unsere Gameserver bequem über das Web-Interface.\n\n' +
- '**Features:**\n' +
- '• Server starten, stoppen & neustarten\n' +
- '• Live Server-Status & Spielerlisten\n' +
- '• Server-Konsole & RCON-Befehle\n' +
- '• CPU, RAM & Uptime Metriken\n' +
- '• Welten-Verwaltung (Factorio)\n' +
- '• Config-Editor (Project Zomboid)\n\n' +
- '**Zugang:**\n' +
- 'Melde dich mit deinem Discord-Account an. Deine Berechtigungen werden automatisch über deine Discord-Rollen bestimmt.'
- )
- .setColor(0x5865F2)
- .addFields({
- name: '🔗 Web-Interface',
- value: '[server.zeasy.dev](https://server.zeasy.dev)',
- inline: false
- })
- .setFooter({ text: 'Zeasy Software' })
- .setTimestamp();
-
- if (botMessage) {
- await botMessage.edit({ embeds: [infoEmbed] });
- console.log('[DiscordBot] Updated info message');
- } else {
- await channel.send({ embeds: [infoEmbed] });
- console.log('[DiscordBot] Created info message');
- }
- } catch (err) {
- console.error('[DiscordBot] Error setting up info message:', err.message);
- }
-}
-
-export async function initDiscordBot() {
- const token = process.env.DISCORD_BOT_TOKEN;
- statusChannelId = process.env.DISCORD_STATUS_CHANNEL_ID;
- infoChannelId = process.env.DISCORD_INFO_CHANNEL_ID;
- alertChannelId = process.env.DISCORD_ALERT_CHANNEL_ID;
-
- if (!token) {
- console.log('[DiscordBot] Missing DISCORD_BOT_TOKEN, bot disabled');
- return;
- }
-
- client = new Client({
- intents: [GatewayIntentBits.Guilds]
- });
-
- client.once('ready', async () => {
- console.log('[DiscordBot] Logged in as ' + client.user.tag);
-
- // Setup info channel
- if (infoChannelId) {
- await setupInfoMessage();
- }
-
- // Setup status channel and alerts
- if (statusChannelId) {
- await findOrCreateStatusMessage();
- // First run - just populate state without sending alerts
- await updateStatusMessage(true);
- // Then start regular updates with alerts
- setInterval(() => updateStatusMessage(false), 60000);
- }
- });
-
- client.login(token).catch(err => {
- console.error('[DiscordBot] Failed to login:', err.message);
- });
-}
-
-async function findOrCreateStatusMessage() {
- try {
- const channel = await client.channels.fetch(statusChannelId);
- if (!channel) {
- console.error('[DiscordBot] Status channel not found');
- return;
- }
-
- const messages = await channel.messages.fetch({ limit: 10 });
- const botMessage = messages.find(m => m.author.id === client.user.id);
-
- if (botMessage) {
- statusMessageId = botMessage.id;
- console.log('[DiscordBot] Found existing status message');
- } else {
- const msg = await channel.send({ embeds: [createLoadingEmbed()] });
- statusMessageId = msg.id;
- console.log('[DiscordBot] Created new status message');
- }
- } catch (err) {
- console.error('[DiscordBot] Error finding/creating status message:', err.message);
- }
-}
-
-function createLoadingEmbed() {
- return new EmbedBuilder()
- .setTitle('🎮 Gameserver Status')
- .setDescription('Lade Server-Status...')
- .setColor(0x6B7280)
- .setTimestamp();
-}
-
-async function updateStatusMessage(skipAlerts = false) {
- if (!client || !statusMessageId || !statusChannelId) return;
-
- try {
- const channel = await client.channels.fetch(statusChannelId);
- const message = await channel.messages.fetch(statusMessageId);
-
- const config = loadConfig();
-
- const serverStatuses = await Promise.all(config.servers.map(async (server) => {
- try {
- const status = await getServerStatus(server);
- const running = status === 'online';
-
- let players = { online: 0, max: null };
- let playerList = { players: [] };
-
- if (running && server.rconPassword) {
- try {
- players = await getPlayers(server);
- playerList = await getPlayerList(server);
- } catch (e) {
- // RCON might fail
- }
- }
-
- return {
- id: server.id,
- name: server.name,
- type: server.type,
- status: running ? 'online' : 'offline',
- running,
- players: players.online || 0,
- maxPlayers: players.max,
- playerList: playerList.players || []
- };
- } catch (err) {
- return {
- id: server.id,
- name: server.name,
- type: server.type,
- status: 'unreachable',
- running: false,
- players: 0,
- maxPlayers: null,
- playerList: []
- };
- }
- }));
-
- // Send alerts if enabled
- if (!skipAlerts && alertChannelId) {
- await checkAndSendAlerts(serverStatuses);
- } else if (skipAlerts) {
- // Just populate initial state
- for (const server of serverStatuses) {
- previousServerState.set(server.id, server.status);
- previousPlayerLists.set(server.id, server.playerList || []);
- }
- }
-
- const embeds = [];
-
- const onlineCount = serverStatuses.filter(s => s.running).length;
- const totalPlayers = serverStatuses.reduce((sum, s) => sum + s.players, 0);
-
- const headerEmbed = new EmbedBuilder()
- .setTitle('🎮 Gameserver Status')
- .setDescription('**' + onlineCount + '/' + serverStatuses.length + '** Server online • **' + totalPlayers + '** Spieler')
- .setColor(onlineCount > 0 ? 0x22C55E : 0xEF4444)
- .setTimestamp()
- .setFooter({ text: 'Aktualisiert alle 60 Sekunden' });
-
- embeds.push(headerEmbed);
-
- for (const server of serverStatuses) {
- const display = serverDisplay[server.id] || { name: server.name, icon: '🖥️', color: 0x6B7280, address: '' };
-
- const serverEmbed = new EmbedBuilder()
- .setTitle(display.icon + ' ' + display.name)
- .setColor(server.running ? display.color : 0x4B5563);
-
- if (server.running) {
- let description = '✅ **Online**\n';
-
- if (display.address) {
- description += '```' + display.address + '```';
- }
-
- description += '👥 **Spieler:** ' + server.players;
- if (server.maxPlayers) {
- description += '/' + server.maxPlayers;
- }
-
- if (server.playerList && server.playerList.length > 0) {
- const names = server.playerList.slice(0, 15).join(', ');
- description += '\n' + names;
- if (server.playerList.length > 15) {
- description += ' *+' + (server.playerList.length - 15) + ' mehr*';
- }
- }
-
- serverEmbed.setDescription(description);
- } else {
- serverEmbed.setDescription('❌ **Offline**');
- }
-
- embeds.push(serverEmbed);
- }
-
- await message.edit({ embeds });
-
- } catch (err) {
- console.error('[DiscordBot] Error updating status message:', err.message);
- }
-}
-
-export function getDiscordClient() {
- return client;
-}
diff --git a/HANDOFF-SATISFACTORY.md b/docs/HANDOFF-SATISFACTORY.md
similarity index 100%
rename from HANDOFF-SATISFACTORY.md
rename to docs/HANDOFF-SATISFACTORY.md
diff --git a/docs/gameserver-hinzufuegen.md b/docs/gameserver-hinzufuegen.md
index 703f06b..266e9c9 100644
--- a/docs/gameserver-hinzufuegen.md
+++ b/docs/gameserver-hinzufuegen.md
@@ -64,7 +64,9 @@ Steam App IDs:
- V Rising: `1829350`
- Factorio: `427520` (oder manuell von factorio.com)
-## Schritt 2: Systemd Service erstellen
+## Schritt 2: Service erstellen (systemd oder PM2)
+
+### Option A: Systemd Service (empfohlen für root-Zugang)
Erstelle `/etc/systemd/system/.service`:
@@ -92,6 +94,34 @@ systemctl daemon-reload
systemctl enable
```
+### Option B: PM2 (für User ohne root-Zugang)
+
+PM2 eignet sich für Server, wo kein Root-Zugang verfügbar ist (z.B. externe VMs).
+
+```bash
+# NVM installieren
+curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
+source ~/.nvm/nvm.sh
+
+# Node.js und PM2 installieren
+nvm install --lts
+npm install -g pm2
+
+# Server starten (Beispiel Terraria)
+cd /home//
+pm2 start ./StartServer.sh --name
+
+# Speichern für Autostart
+pm2 save
+```
+
+Für Autostart nach Reboot muss ein Admin den folgenden Befehl mit sudo ausführen:
+```bash
+sudo env PATH=$PATH:/home//.nvm/versions/node//bin pm2 startup systemd -u --hp /home/
+```
+
+In der GSM config.json `"runtime": "pm2"` und `"serviceName": ""` setzen.
+
## Schritt 3: Node Exporter installieren
Für Metriken im GSM Dashboard:
@@ -352,3 +382,4 @@ CREATE TABLE server_display_settings (
| V Rising | 192.168.2.52 | LXC | 9876-9877/UDP |
| Palworld | 192.168.2.53 | LXC | 8211/UDP, 27015/UDP |
| Project Zomboid | 10.0.30.66 | VM (extern) | 16261-16262/UDP |
+| Terraria | 10.0.30.202 | VM (extern/VPN) | 7777/TCP |
diff --git a/gsm.md b/docs/gsm.md
similarity index 100%
rename from gsm.md
rename to docs/gsm.md
diff --git a/infrastructure.md b/docs/infrastructure.md
similarity index 100%
rename from infrastructure.md
rename to docs/infrastructure.md
diff --git a/todo.md b/docs/todo.md
similarity index 100%
rename from todo.md
rename to docs/todo.md
diff --git a/gsm-backend/routes/servers.js b/gsm-backend/routes/servers.js
index 259eaba..b95e329 100644
--- a/gsm-backend/routes/servers.js
+++ b/gsm-backend/routes/servers.js
@@ -200,9 +200,16 @@ router.get("/zomboid/config", authenticateToken, requireRole("moderator"), async
const server = config.servers.find(s => s.type === "zomboid");
if (!server) return res.status(404).json({ error: "Zomboid server not configured" });
+ if (isHostFailed(server.host, server.sshUser)) {
+ return res.status(503).json({ error: "Server host is unreachable", unreachable: true });
+ }
+
const files = await listZomboidConfigs(server);
res.json({ files });
} 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 });
}
});
@@ -419,10 +426,18 @@ router.get("/terraria/config", authenticateToken, requireRole("moderator"), asyn
const config = loadConfig();
const server = config.servers.find(s => s.id === "terraria");
if (!server) return res.status(404).json({ error: "Server not found" });
+
+ if (isHostFailed(server.host, server.sshUser)) {
+ return res.status(503).json({ error: "Server host is unreachable", unreachable: true });
+ }
+
const content = await readTerrariaConfig(server);
res.json({ content });
} catch (error) {
console.error("Error reading Terraria config:", error);
+ if (error.message.includes('unreachable') || error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')) {
+ return res.status(503).json({ error: 'Server host is unreachable', unreachable: true });
+ }
res.status(500).json({ error: error.message });
}
});
@@ -452,10 +467,18 @@ router.get("/openttd/config", authenticateToken, requireRole("moderator"), async
const config = loadConfig();
const server = config.servers.find(s => s.id === "openttd");
if (!server) return res.status(404).json({ error: "Server not found" });
+
+ if (isHostFailed(server.host, server.sshUser)) {
+ return res.status(503).json({ error: "Server host is unreachable", unreachable: true });
+ }
+
const content = await readOpenTTDConfig(server);
res.json({ content });
} catch (error) {
console.error("Error reading OpenTTD config:", error);
+ if (error.message.includes('unreachable') || error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')) {
+ return res.status(503).json({ error: 'Server host is unreachable', unreachable: true });
+ }
res.status(500).json({ error: error.message });
}
});
@@ -487,11 +510,41 @@ router.get('/:id', optionalAuth, async (req, res) => {
}
try {
+ // Check if host is unreachable
+ const hostUnreachable = isHostFailed(server.host, server.sshUser);
+ if (hostUnreachable) {
+ const metrics = await getCurrentMetrics(server.id).catch(() => ({
+ cpu: 0, cpuCores: 1, memory: 0, memoryUsed: 0, memoryTotal: 0, uptime: 0
+ }));
+ const memTotal = formatBytes(metrics.memoryTotal);
+ const memUsed = formatBytes(metrics.memoryUsed, memTotal.unit);
+ return res.json({
+ id: server.id,
+ name: server.name,
+ type: server.type,
+ status: "unreachable",
+ running: false,
+ metrics: {
+ cpu: metrics.cpu,
+ cpuCores: metrics.cpuCores,
+ memory: metrics.memory,
+ memoryUsed: memUsed.value,
+ memoryTotal: memTotal.value,
+ memoryUnit: memTotal.unit,
+ uptime: 0
+ },
+ players: { online: 0, max: null, list: [] },
+ hasRcon: !!server.rconPassword
+ });
+ }
+
const [status, metrics, players, playerList, processUptime] = await Promise.all([
getServerStatus(server),
- getCurrentMetrics(server.id),
- server.rconPassword ? getPlayers(server) : { online: 0, max: null },
- server.rconPassword ? getPlayerList(server) : { players: [] },
+ getCurrentMetrics(server.id).catch(() => ({
+ cpu: 0, cpuCores: 1, memory: 0, memoryUsed: 0, memoryTotal: 0, uptime: 0
+ })),
+ server.rconPassword ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null },
+ server.rconPassword ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] },
getProcessUptime(server).catch(() => 0)
]);
@@ -520,6 +573,7 @@ router.get('/:id', optionalAuth, async (req, res) => {
hasRcon: !!server.rconPassword
});
} catch (err) {
+ console.error(`Error fetching server ${req.params.id}:`, err.message);
res.status(500).json({ error: err.message });
}
});
@@ -571,11 +625,18 @@ router.get('/:id/logs', authenticateToken, requireRole('moderator'), async (req,
const server = config.servers.find(s => s.id === req.params.id);
if (!server) return res.status(404).json({ error: 'Server not found' });
+ if (isHostFailed(server.host, server.sshUser)) {
+ return res.status(503).json({ error: 'Server host is unreachable', unreachable: true });
+ }
+
try {
const lines = parseInt(req.query.lines) || 100;
const logs = await getConsoleLog(server, lines);
res.json({ logs });
} 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 });
}
});
@@ -586,12 +647,19 @@ router.post('/:id/start', authenticateToken, requireRole('moderator'), async (re
const server = config.servers.find(s => s.id === req.params.id);
if (!server) return res.status(404).json({ error: 'Server not found' });
+ if (isHostFailed(server.host, server.sshUser)) {
+ return res.status(503).json({ error: 'Server host is unreachable', unreachable: true });
+ }
+
try {
const { save } = req.body || {};
await startServer(server, { save });
logActivity(req.user.id, req.user.username, 'server_start', server.id, save ? 'Save: ' + save : null, req.user.discordId, req.user.avatar);
res.json({ message: 'Server starting' });
} 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 });
}
});
@@ -601,11 +669,18 @@ router.post('/:id/stop', authenticateToken, requireRole('moderator'), async (req
const server = config.servers.find(s => s.id === req.params.id);
if (!server) return res.status(404).json({ error: 'Server not found' });
+ if (isHostFailed(server.host, server.sshUser)) {
+ return res.status(503).json({ error: 'Server host is unreachable', unreachable: true });
+ }
+
try {
await stopServer(server);
logActivity(req.user.id, req.user.username, 'server_stop', server.id, null, req.user.discordId, req.user.avatar);
res.json({ message: 'Server stopping' });
} 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 });
}
});
@@ -615,11 +690,18 @@ router.post('/:id/restart', authenticateToken, requireRole('moderator'), async (
const server = config.servers.find(s => s.id === req.params.id);
if (!server) return res.status(404).json({ error: 'Server not found' });
+ if (isHostFailed(server.host, server.sshUser)) {
+ return res.status(503).json({ error: 'Server host is unreachable', unreachable: true });
+ }
+
try {
await restartServer(server);
logActivity(req.user.id, req.user.username, 'server_restart', server.id, null, req.user.discordId, req.user.avatar);
res.json({ message: 'Server restarting' });
} 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 });
}
});
diff --git a/temp_Dashboard.jsx b/temp_Dashboard.jsx
deleted file mode 100644
index 512dfe3..0000000
--- a/temp_Dashboard.jsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import { useState, useEffect } from 'react'
-import { useNavigate } from 'react-router-dom'
-import { getServers } from '../api'
-import { useUser } from '../context/UserContext'
-import ServerCard from '../components/ServerCard'
-import SettingsModal from '../components/SettingsModal'
-import UserManagement from '../components/UserManagement'
-import LoginModal from '../components/LoginModal'
-
-export default function Dashboard({ onLogin, onLogout }) {
- const navigate = useNavigate()
- const { user, token, loading: userLoading, isSuperadmin, role } = useUser()
- const [servers, setServers] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState('')
- const [showSettings, setShowSettings] = useState(false)
- const [showUserMgmt, setShowUserMgmt] = useState(false)
- const [showLogin, setShowLogin] = useState(false)
-
- const isAuthenticated = !!token
-
- const fetchServers = async () => {
- try {
- const data = await getServers(token)
- setServers(data)
- setError('')
- } catch (err) {
- if (err.message.includes('401') || err.message.includes('403')) {
- if (isAuthenticated) {
- onLogout()
- }
- } else {
- setError('Verbindung zum Server fehlgeschlagen')
- }
- } finally {
- setLoading(false)
- }
- }
-
- useEffect(() => {
- if (!userLoading) {
- fetchServers()
- const interval = setInterval(fetchServers, 10000)
- return () => clearInterval(interval)
- }
- }, [token, userLoading])
-
- const roleLabels = {
- user: 'Betrachter',
- moderator: 'Operator',
- superadmin: 'Admin'
- }
-
- if (userLoading) {
- return (
-
- )
- }
-
- const onlineCount = servers.filter(s => s.running).length
- document.title = 'Dashboard | Zeasy GSM'
- const totalPlayers = servers.reduce((sum, s) => sum + (s.players?.online || 0), 0)
-
- return (
-
- {/* Header */}
-
-
-
-
-


-
Gameserver Management
-
-
-
- {onlineCount}/{servers.length} online
-
- |
-
- {totalPlayers} Spieler
-
-
-
- {isAuthenticated ? (
- <>
-
-
{user?.username}
-
{roleLabels[role]}
-
- {isSuperadmin && (
-
- )}
-
-
- >
- ) : (
-
- )}
-
-
-
-
-
- {/* Main Content */}
-
- {error && (
-
- {error}
-
- )}
- {loading ? (
-
-
Server werden geladen...
-
- ) : (
-
- {servers.map((server, index) => (
-
- navigate('/server/' + server.id)}
- isAuthenticated={isAuthenticated}
- />
-
- ))}
-
- )}
-
-
- {/* Modals */}
- {showSettings && (
-
setShowSettings(false)} />
- )}
- {showUserMgmt && (
- setShowUserMgmt(false)} />
- )}
- {showLogin && (
- setShowLogin(false)} />
- )}
-
- )
-}
diff --git a/temp_ServerCard.jsx b/temp_ServerCard.jsx
deleted file mode 100644
index 9ed8b7b..0000000
--- a/temp_ServerCard.jsx
+++ /dev/null
@@ -1,208 +0,0 @@
-const serverInfo = {
- minecraft: {
- address: 'minecraft.dimension47.de',
- logo: '/minecraft.png',
- links: [
- { label: 'ATM10 Modpack', url: 'https://www.curseforge.com/minecraft/modpacks/all-the-mods-10' }
- ]
- },
- factorio: {
- hint: 'Serverpasswort: affe',
- address: 'factorio.dimension47.de',
- logo: '/factorio.png',
- links: [
- { label: 'Steam', url: 'https://store.steampowered.com/app/427520/Factorio/' }
- ]
- },
- vrising: {
- address: 'Zeasy Software Vampire',
- logo: '/vrising.png',
- 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/' }
- ]
- }
-}
-
-const getServerInfo = (serverName) => {
- const name = serverName.toLowerCase()
- 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
-}
-
-export default function ServerCard({ server, onClick, isAuthenticated }) {
- const info = getServerInfo(server.name)
-
- const formatUptime = (seconds) => {
- const hours = Math.floor(seconds / 3600)
- if (hours > 24) {
- const days = Math.floor(hours / 24)
- return days + 'd ' + (hours % 24) + 'h'
- }
- const minutes = Math.floor((seconds % 3600) / 60)
- return hours + 'h ' + minutes + 'm'
- }
-
- const cpuPercent = Math.min(server.metrics.cpu, 100)
- const memPercent = Math.min(server.metrics.memory, 100)
-
- const getProgressColor = (percent) => {
- if (percent > 80) return 'progress-bar-danger'
- if (percent > 60) return 'progress-bar-warning'
- return 'progress-bar-success'
- }
-
- const getStatusBadge = () => {
- const status = server.status || (server.running ? 'online' : 'offline')
- switch (status) {
- case 'online':
- return { class: 'badge badge-success', text: 'Online' }
- case 'starting':
- return { class: 'badge badge-warning', text: 'Startet...' }
- case 'stopping':
- return { class: 'badge badge-warning', text: 'Stoppt...' }
- case 'unreachable':
- return { class: 'badge badge-muted', text: 'Nicht erreichbar' }
- default:
- return { class: 'badge badge-destructive', text: 'Offline' }
- }
- }
-
- const statusBadge = getStatusBadge()
-
- return (
-
- {/* Header */}
-
-
- {info && info.logo &&

}
-
{server.name}
-
-
- {statusBadge.text}
-
-
-
- {/* Server Address & Links */}
- {info && (
-
- )}
-
- {/* Whitelist notice for Minecraft - only for authenticated users */}
- {isAuthenticated && server.type === 'minecraft' && (
-
- Whitelist erforderlich - im Whitelist-Tab freischalten
-
- )}
-
- {/* Factorio notice - only for authenticated users */}
- {isAuthenticated && server.type === 'factorio' && (
-
- Serverpasswort: affe
-
- )}
-
- {/* V Rising notice - only for authenticated users */}
- {isAuthenticated && server.type === 'vrising' && (
-
- In der Serverliste suchen - Passwort: affe
-
- )}
-
- {/* Project Zomboid notice - only for authenticated users */}
- {isAuthenticated && server.type === 'zomboid' && (
-
- Version 42.13
-
- )}
-
- {/* Metrics */}
-
- {/* CPU */}
-
-
- CPU
- {server.metrics.cpu.toFixed(1)}%
-
-
-
-
- {/* RAM */}
-
-
- Arbeitsspeicher
-
- {server.metrics.memoryUsed?.toFixed(1) || 0} / {server.metrics.memoryTotal?.toFixed(1) || 0} {server.metrics.memoryUnit}
-
-
-
-
-
-
- {/* Footer Stats */}
-
-
- {server.players.online}
- {server.players.max ? ' / ' + server.players.max : ''} Spieler
-
- {server.running && (
-
- Laufzeit: {formatUptime(server.metrics.uptime)}
-
- )}
-
-
- {/* Players List */}
- {server.players?.list?.length > 0 && (
-
-
- {server.players.list.map((player, i) => (
-
- {player}
-
- ))}
-
-
- )}
-
- )
-}
diff --git a/temp_ServerDetail.jsx b/temp_ServerDetail.jsx
deleted file mode 100644
index bd73b37..0000000
--- a/temp_ServerDetail.jsx
+++ /dev/null
@@ -1,640 +0,0 @@
-import { useState, useEffect, useRef } from 'react'
-import { useParams, useNavigate } from 'react-router-dom'
-import { getServers, serverAction, sendRcon, getServerLogs, getWhitelist, getFactorioCurrentSave, getAutoShutdownSettings, setAutoShutdownSettings } from '../api'
-import { useUser } from '../context/UserContext'
-import MetricsChart from '../components/MetricsChart'
-import FactorioWorldManager from '../components/FactorioWorldManager'
-
-const getServerLogo = (serverName) => {
- const name = serverName.toLowerCase()
- 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() {
- const { serverId } = useParams()
- const navigate = useNavigate()
- const { token, isModerator } = useUser()
- const [server, setServer] = useState(null)
- const [loading, setLoading] = useState(true)
-
- const [activeTab, setActiveTab] = useState('overview')
- const [rconCommand, setRconCommand] = useState('')
- const [rconHistory, setRconHistory] = useState([])
- const [logs, setLogs] = useState('')
- const [whitelistPlayers, setWhitelistPlayers] = useState([])
-
- 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)
-
- // Auto-shutdown state
- const [autoShutdown, setAutoShutdown] = useState({ enabled: false, timeoutMinutes: 15, emptySinceMinutes: null })
- const [autoShutdownLoading, setAutoShutdownLoading] = useState(false)
-
- const fetchCurrentSave = async () => {
- if (token && serverId === 'factorio') {
- try {
- const result = await getFactorioCurrentSave(token)
- setCurrentSave(result)
- } catch (err) {
- console.error('Failed to fetch current save:', err)
- }
- }
- }
-
- const fetchServer = async () => {
- try {
- const servers = await getServers(token)
- const found = servers.find(s => s.id === serverId)
- if (found) {
- setServer(found); document.title = found.name + " | Zeasy GSM"
- } else {
- navigate('/')
- }
- } catch (err) {
- console.error(err)
- navigate('/')
- } finally {
- setLoading(false)
- }
- }
-
- const fetchAutoShutdownSettings = async () => {
- if (token && serverId) {
- try {
- const data = await getAutoShutdownSettings(token, serverId)
- setAutoShutdown(data)
- } catch (err) {
- console.error('Failed to load auto-shutdown settings:', err)
- }
- }
- }
-
- const handleAutoShutdownToggle = async () => {
- setAutoShutdownLoading(true)
- try {
- await setAutoShutdownSettings(token, serverId, !autoShutdown.enabled, autoShutdown.timeoutMinutes)
- setAutoShutdown(prev => ({ ...prev, enabled: !prev.enabled }))
- } catch (err) {
- console.error(err)
- }
- setAutoShutdownLoading(false)
- }
-
- const handleAutoShutdownTimeoutChange = async (newTimeout) => {
- const timeout = Math.max(1, Math.min(1440, parseInt(newTimeout) || 15))
- setAutoShutdown(prev => ({ ...prev, timeoutMinutes: timeout }))
-
- clearTimeout(window.autoShutdownSaveTimeout)
- window.autoShutdownSaveTimeout = setTimeout(async () => {
- try {
- await setAutoShutdownSettings(token, serverId, autoShutdown.enabled, timeout)
- } catch (err) {
- console.error(err)
- }
- }, 500)
- }
-
- useEffect(() => {
- fetchServer()
- fetchCurrentSave()
- const interval = setInterval(() => {
- fetchServer()
- fetchCurrentSave()
- }, 10000)
- return () => clearInterval(interval)
- }, [token, serverId])
-
- useEffect(() => {
- if (activeTab === 'settings' && isModerator) {
- fetchAutoShutdownSettings()
- const interval = setInterval(fetchAutoShutdownSettings, 10000)
- return () => clearInterval(interval)
- }
- }, [activeTab, isModerator, token, serverId])
-
- const handleAction = async (action) => {
- // Immediately set status locally
- const newStatus = action === 'start' ? 'starting' : (action === 'stop' ? 'stopping' : 'starting')
- setServer(prev => ({ ...prev, status: newStatus }))
- try {
- await serverAction(token, server.id, action)
- setTimeout(() => {
- fetchServer()
-
- }, 2000)
- } catch (err) {
- console.error(err)
-
- }
- }
-
- const handleRcon = async (e) => {
- e.preventDefault()
- if (!rconCommand.trim()) return
- const cmd = rconCommand
- setRconCommand('')
- try {
- const { response } = await sendRcon(token, server.id, cmd)
- setRconHistory([...rconHistory, { cmd, res: response, time: new Date() }])
- } catch (err) {
- setRconHistory([...rconHistory, { cmd, res: 'Error: ' + err.message, time: new Date(), error: true }])
- }
- }
-
- const fetchLogs = async () => {
- try {
- const data = await getServerLogs(token, server.id, 50)
- setLogs(data.logs || '')
- setLogsUpdated(new Date())
- if (logsRef.current) {
- logsRef.current.scrollTop = logsRef.current.scrollHeight
- }
- } catch (err) {
- console.error(err)
- }
- }
-
- useEffect(() => {
- if (activeTab === 'console' && isModerator && server) {
- fetchLogs()
- const interval = setInterval(fetchLogs, 5000)
- return () => clearInterval(interval)
- }
- }, [activeTab, isModerator, server])
-
- useEffect(() => {
- if (activeTab === 'whitelist' && isModerator && server?.type === 'minecraft') {
- fetchWhitelist()
- }
- }, [activeTab, server])
-
- useEffect(() => {
- if (rconRef.current) {
- rconRef.current.scrollTop = rconRef.current.scrollHeight
- }
- }, [rconHistory])
-
-
- const fetchWhitelist = async () => {
- if (!server?.hasRcon) return
- try {
- const { players } = await getWhitelist(token, server.id)
- setWhitelistPlayers(players)
- } catch (err) {
- console.error("Failed to fetch whitelist:", err)
- }
- }
-
- const addToWhitelist = async (e) => {
- e.preventDefault()
- if (!whitelistInput.trim()) return
- setWhitelistLoading(true)
- try {
- await sendRcon(token, server.id, 'whitelist add ' + whitelistInput.trim())
- setWhitelistInput('')
- await fetchWhitelist()
- } catch (err) {
- console.error('Failed to add to whitelist:', err)
- } finally {
- setWhitelistLoading(false)
- }
- }
-
- const removeFromWhitelist = async (player) => {
- setWhitelistLoading(true)
- try {
- await sendRcon(token, server.id, 'whitelist remove ' + player)
- await fetchWhitelist()
- } catch (err) {
- console.error('Failed to remove from whitelist:', err)
- } finally {
- setWhitelistLoading(false)
- }
- }
-
-const formatUptime = (seconds) => {
- const days = Math.floor(seconds / 86400)
- const hours = Math.floor((seconds % 86400) / 3600)
- const minutes = Math.floor((seconds % 3600) / 60)
- if (days > 0) return days + 'd ' + hours + 'h ' + minutes + 'm'
- return hours + 'h ' + minutes + 'm'
- }
-
- const tabs = [
- { id: 'overview', label: 'Übersicht' },
- { id: 'metrics', label: 'Metriken' },
- ...(isModerator ? [
- { id: 'console', label: 'Konsole' },
- ] : []),
- ...(isModerator && server?.type === 'minecraft' ? [
- { id: 'whitelist', label: 'Whitelist' },
- ] : []),
- ...(isModerator && server?.type === 'factorio' ? [
- { id: 'worlds', label: 'Welten' },
- ] : []),
- ...(isModerator ? [
- { id: 'settings', label: 'Einstellungen' },
- ] : []),
- ]
-
- if (loading) {
- return (
-
- )
- }
-
- if (!server) {
- return (
-
-
Server nicht gefunden
-
- )
- }
-
- const cpuPercent = Math.min(server.metrics.cpu, 100)
- const memPercent = Math.min(server.metrics.memory, 100)
-
- const getStatusBadge = () => {
- const status = server.status || (server.running ? 'online' : 'offline')
- switch (status) {
- case 'online':
- return { class: 'badge badge-success', text: 'Online' }
- case 'starting':
- return { class: 'badge badge-warning', text: 'Startet...' }
- case 'stopping':
- return { class: 'badge badge-warning', text: 'Stoppt...' }
- default:
- return { class: 'badge badge-destructive', text: 'Offline' }
- }
- }
-
- const statusBadge = getStatusBadge()
-
- return (
-
- {/* Header */}
-
-
- {/* Content */}
-
- {/* Overview Tab */}
- {activeTab === 'overview' && (
-
- {/* Stats Grid */}
-
-
-
CPU Auslastung
-
{server.metrics.cpu.toFixed(1)}%
-
-
-
-
Arbeitsspeicher
-
- {server.metrics.memoryUsed?.toFixed(1)} {server.metrics.memoryUnit}
-
-
- von {server.metrics.memoryTotal?.toFixed(1)} {server.metrics.memoryUnit}
-
-
-
-
Spieler
-
{server.players.online}
-
- {server.players.max ? 'von ' + server.players.max + ' max' : 'Kein Limit'}
-
-
-
-
CPU Kerne
-
{server.metrics.cpuCores}
-
- {server.type === 'factorio' && currentSave?.save && (
-
-
{server.running ? 'Aktuelle Welt' : 'Nächste Welt'}
-
{currentSave.save}
- {!server.running && currentSave.source === 'newest' && (
-
neuester Speicherstand
- )}
-
- )}
-
-
- {/* Players List */}
- {server.players?.list?.length > 0 && (
-
-
Spieler Online
-
- {server.players.list.map((player, i) => (
- {player}
- ))}
-
-
- )}
-
- {/* Power Controls */}
- {isModerator && (
-
-
Server Steuerung
-
-
- {(server.status === 'online' || (server.running && server.status !== 'starting' && server.status !== 'stopping')) ? (
- <>
-
-
- >
- ) : (
-
- )}
-
-
- )}
-
- )}
-
- {/* Metrics Tab */}
- {activeTab === 'metrics' && (
-
- )}
-
- {/* Console Tab - Logs + RCON */}
- {activeTab === 'console' && isModerator && (
-
- {/* Logs */}
-
-
- Server Logs (letzte 50 Zeilen)
- {logsUpdated && (
-
- Aktualisiert: {logsUpdated.toLocaleTimeString('de-DE')}
-
- )}
-
-
-
-
- {logs || 'Laden...'}
-
-
- {/* RCON History */}
- {rconHistory.length > 0 && (
-
-
RCON Verlauf
- {rconHistory.map((entry, i) => (
-
-
- [{entry.time.toLocaleTimeString('de-DE')}] > {entry.cmd}
-
-
- {entry.res}
-
-
- ))}
-
- )}
-
- {/* RCON Input */}
- {server.hasRcon && (
-
- )}
-
- )}
-
- {/* Whitelist Tab - Minecraft only */}
- {activeTab === 'whitelist' && isModerator && server.type === 'minecraft' && (
-
-
-
Zur Whitelist hinzufügen
-
-
-
-
-
-
Whitelist Spieler ({whitelistPlayers.length})
-
-
- {whitelistPlayers.length > 0 ? (
-
- {whitelistPlayers.map((player, i) => (
-
- {player}
-
-
- ))}
-
- ) : (
-
Keine Spieler auf der Whitelist
- )}
-
-
- )}
-
- {/* Worlds Tab - Factorio only */}
- {activeTab === 'worlds' && isModerator && server.type === 'factorio' && (
-
- {server.running || server.status === 'starting' || server.status === 'stopping' ? (
-
-
Weltverwaltung ist gesperrt während der Server läuft
-
Server stoppen um Speicherstände zu verwalten
-
- ) : (
-
- )}
-
- )}
-
- {/* Settings Tab */}
- {activeTab === 'settings' && isModerator && (
-
-
-
Auto-Shutdown
-
- Server automatisch stoppen wenn keine Spieler online sind
-
-
-
- {/* Toggle Switch */}
-
-
-
- {autoShutdown.enabled ? 'Aktiviert' : 'Deaktiviert'}
-
-
-
- {/* Timeout mit +/- Buttons */}
- {autoShutdown.enabled && (
-
-
Timeout:
-
-
-
- {autoShutdown.timeoutMinutes}
-
-
-
-
Minuten
-
- )}
-
- {/* Status */}
- {autoShutdown.enabled && server.running && (
-
-
- Status:
- {autoShutdown.emptySinceMinutes !== null ? (
-
- Leer seit {autoShutdown.emptySinceMinutes} Min. Shutdown in {autoShutdown.timeoutMinutes - autoShutdown.emptySinceMinutes} Min.
-
- ) : (
-
- Spieler online
-
- )}
-
-
- )}
-
-
-
- )}
-
-
- )
-}
diff --git a/zomboid_funcs.js b/zomboid_funcs.js
deleted file mode 100644
index ab61c1b..0000000
--- a/zomboid_funcs.js
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-// ============ ZOMBOID CONFIG FUNCTIONS ============
-
-const ZOMBOID_CONFIG_PATH = "/home/pzuser/Zomboid/Server";
-const ALLOWED_CONFIG_FILES = ["Project.ini", "Project_SandboxVars.lua", "Project_spawnpoints.lua", "Project_spawnregions.lua"];
-
-export async function listZomboidConfigs(server) {
- const ssh = await getConnection(server.host, server.sshUser);
- const cmd = `ls -la ${ZOMBOID_CONFIG_PATH}/*.ini ${ZOMBOID_CONFIG_PATH}/*.lua 2>/dev/null`;
- const result = await ssh.execCommand(cmd);
-
- if (result.code !== 0 || !result.stdout.trim()) {
- return [];
- }
-
- const files = [];
- const lines = result.stdout.trim().split("\n");
-
- for (const line of lines) {
- const match = line.match(/(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\S+\s+\d+\s+[\d:]+)\s+(.+)$/);
- if (match) {
- const fullPath = match[7];
- const filename = fullPath.split("/").pop();
-
- if (!ALLOWED_CONFIG_FILES.includes(filename)) continue;
-
- files.push({
- filename,
- size: parseInt(match[5]),
- modified: match[6]
- });
- }
- }
-
- return files;
-}
-
-export async function readZomboidConfig(server, filename) {
- if (!ALLOWED_CONFIG_FILES.includes(filename)) {
- throw new Error("File not allowed");
- }
- if (filename.includes("/") || filename.includes("..")) {
- throw new Error("Invalid filename");
- }
-
- const ssh = await getConnection(server.host, server.sshUser);
- const result = await ssh.execCommand(`cat ${ZOMBOID_CONFIG_PATH}/${filename}`);
-
- if (result.code !== 0) {
- throw new Error(result.stderr || "Failed to read config file");
- }
-
- return result.stdout;
-}
-
-export async function writeZomboidConfig(server, filename, content) {
- if (!ALLOWED_CONFIG_FILES.includes(filename)) {
- throw new Error("File not allowed");
- }
- if (filename.includes("/") || filename.includes("..")) {
- throw new Error("Invalid filename");
- }
-
- const ssh = await getConnection(server.host, server.sshUser);
-
- // Create backup
- const backupName = `${filename}.backup.${Date.now()}`;
- await ssh.execCommand(`cp ${ZOMBOID_CONFIG_PATH}/${filename} ${ZOMBOID_CONFIG_PATH}/${backupName} 2>/dev/null || true`);
-
- // Write file using sftp
- const sftp = await ssh.requestSFTP();
- const filePath = `${ZOMBOID_CONFIG_PATH}/${filename}`;
-
- await new Promise((resolve, reject) => {
- sftp.writeFile(filePath, content, (err) => {
- if (err) reject(err);
- else resolve();
- });
- });
-
- // Clean up old backups (keep last 5)
- await ssh.execCommand(`ls -t ${ZOMBOID_CONFIG_PATH}/${filename}.backup.* 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true`);
-
- return true;
-}