diff --git a/ActivityLog.jsx b/ActivityLog.jsx
new file mode 100644
index 0000000..8aaddd2
--- /dev/null
+++ b/ActivityLog.jsx
@@ -0,0 +1,172 @@
+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 (
+
-
-
{server.players.online}
- {server.players.max ? ' / ' + server.players.max : ''} players
+
+
+ {server.players.online}
+ {server.players.max ? ' / ' + server.players.max : ''} Spieler
+
+ {server.running && server.autoShutdown?.enabled && server.autoShutdown?.emptySinceMinutes !== null && (
+
+
+
+ Shutdown in {server.autoShutdown.timeoutMinutes - server.autoShutdown.emptySinceMinutes}m
+
+
+ )}
{server.running && (
- Uptime: {formatUptime(server.metrics.uptime)}
+ Laufzeit: {formatUptime(server.metrics.uptime)}
)}
diff --git a/temp_Dashboard.jsx b/temp_Dashboard.jsx
new file mode 100644
index 0000000..512dfe3
--- /dev/null
+++ b/temp_Dashboard.jsx
@@ -0,0 +1,168 @@
+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
new file mode 100644
index 0000000..9ed8b7b
--- /dev/null
+++ b/temp_ServerCard.jsx
@@ -0,0 +1,208 @@
+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
new file mode 100644
index 0000000..bd73b37
--- /dev/null
+++ b/temp_ServerDetail.jsx
@@ -0,0 +1,640 @@
+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
new file mode 100644
index 0000000..ab61c1b
--- /dev/null
+++ b/zomboid_funcs.js
@@ -0,0 +1,86 @@
+
+
+// ============ 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;
+}