From 2d9a5910fa7ba44d65ea71d6217b1378cab1705a Mon Sep 17 00:00:00 2001 From: Alexander Zielonka Date: Fri, 9 Jan 2026 12:15:32 +0100 Subject: [PATCH] Cleanup repo, add Gitea CI/CD workflow, improve error handling - Remove temp files and reorganize docs - Add .gitea/workflows/deploy.yml for automated deployment - Add unreachable host checks to server routes (/:id, logs, start/stop/restart) - Add unreachable checks to config routes (zomboid, terraria, openttd) - Return HTTP 503 with unreachable flag instead of crashing Co-Authored-By: Claude Opus 4.5 --- .gitea/workflows/deploy.yml | 63 ++ ActivityLog.jsx | 172 ----- App.jsx | 156 ----- CLAUDE.md | 1 + Dashboard.jsx | 164 ----- UserContext.jsx | 61 -- UserManagement.jsx | 102 --- ZeasyWG-Alex.conf | 10 - ZomboidConfigEditor.jsx | 326 --------- auth.js | 244 ------- discord.js | 167 ----- discordBot.js | 353 ---------- .../HANDOFF-SATISFACTORY.md | 0 docs/gameserver-hinzufuegen.md | 33 +- gsm.md => docs/gsm.md | 0 infrastructure.md => docs/infrastructure.md | 0 todo.md => docs/todo.md | 0 gsm-backend/routes/servers.js | 88 ++- temp_Dashboard.jsx | 168 ----- temp_ServerCard.jsx | 208 ------ temp_ServerDetail.jsx | 640 ------------------ zomboid_funcs.js | 86 --- 22 files changed, 181 insertions(+), 2861 deletions(-) create mode 100644 .gitea/workflows/deploy.yml delete mode 100644 ActivityLog.jsx delete mode 100644 App.jsx delete mode 100644 Dashboard.jsx delete mode 100644 UserContext.jsx delete mode 100644 UserManagement.jsx delete mode 100644 ZeasyWG-Alex.conf delete mode 100644 ZomboidConfigEditor.jsx delete mode 100644 auth.js delete mode 100644 discord.js delete mode 100644 discordBot.js rename HANDOFF-SATISFACTORY.md => docs/HANDOFF-SATISFACTORY.md (100%) rename gsm.md => docs/gsm.md (100%) rename infrastructure.md => docs/infrastructure.md (100%) rename todo.md => docs/todo.md (100%) delete mode 100644 temp_Dashboard.jsx delete mode 100644 temp_ServerCard.jsx delete mode 100644 temp_ServerDetail.jsx delete mode 100644 zomboid_funcs.js 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 ( -
-
- Logo -

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 ( -
-
Laden...
-
- ) - } - - 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 */} -
-
-
-
- - Logo - Logo - - Gameserver Management -
-
- - {onlineCount}/{servers.length} online - - | - - {totalPlayers} Spieler - -
-
- {/* User info with Discord avatar */} -
- {avatarUrl && ( - Avatar - )} -
-
{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 && ( -
- - {error} -
- )} - - {success && ( -
- - {success} -
- )} - - {/* Editor with syntax highlighting */} -
- {/* Highlighted background layer */} -