Files
GSM/temp_ServerDetail.jsx
2026-01-07 02:41:37 +01:00

641 lines
24 KiB
JavaScript

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 (
<div className="min-h-screen flex items-center justify-center">
<div className="text-neutral-400">Laden...</div>
</div>
)
}
if (!server) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-neutral-400">Server nicht gefunden</div>
</div>
)
}
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 (
<div className="min-h-screen page-enter">
{/* Header */}
<header className="border-b border-neutral-800 bg-neutral-900/50 backdrop-blur-sm sticky top-0 z-10">
<div className="container-main py-4">
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/')}
className="btn btn-ghost"
>
Zurück
</button>
<div className="flex-1">
<div className="flex items-center gap-3">
{getServerLogo(server.name) && <img src={getServerLogo(server.name)} alt="" className="h-8 w-8 object-contain" />}
<h1 className="text-xl font-semibold text-white">{server.name}</h1>
<span className={statusBadge.class}>
{statusBadge.text}
</span>
</div>
{server.running && (
<p className="text-sm text-neutral-400 mt-1">
Laufzeit: {formatUptime(server.metrics.uptime)}
</p>
)}
</div>
</div>
{/* Tabs */}
<div className="tabs mt-4">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={'tab ' + (activeTab === tab.id ? 'tab-active' : '')}
>
{tab.label}
</button>
))}
</div>
</div>
</header>
{/* Content */}
<main className="container-main py-6">
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="space-y-6 tab-content">
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="card p-4">
<div className="text-sm text-neutral-400">CPU Auslastung</div>
<div className="text-2xl font-semibold text-white mt-1">{server.metrics.cpu.toFixed(1)}%</div>
<div className="progress mt-2">
<div className="progress-bar" style={{ width: cpuPercent + '%' }} />
</div>
</div>
<div className="card p-4">
<div className="text-sm text-neutral-400">Arbeitsspeicher</div>
<div className="text-2xl font-semibold text-white mt-1">
{server.metrics.memoryUsed?.toFixed(1)} {server.metrics.memoryUnit}
</div>
<div className="text-xs text-neutral-500 mt-1">
von {server.metrics.memoryTotal?.toFixed(1)} {server.metrics.memoryUnit}
</div>
</div>
<div className="card p-4">
<div className="text-sm text-neutral-400">Spieler</div>
<div className="text-2xl font-semibold text-white mt-1">{server.players.online}</div>
<div className="text-xs text-neutral-500 mt-1">
{server.players.max ? 'von ' + server.players.max + ' max' : 'Kein Limit'}
</div>
</div>
<div className="card p-4">
<div className="text-sm text-neutral-400">CPU Kerne</div>
<div className="text-2xl font-semibold text-white mt-1">{server.metrics.cpuCores}</div>
</div>
{server.type === 'factorio' && currentSave?.save && (
<div className="card p-4">
<div className="text-sm text-neutral-400">{server.running ? 'Aktuelle Welt' : 'Nächste Welt'}</div>
<div className="text-lg font-semibold text-white mt-1 truncate">{currentSave.save}</div>
{!server.running && currentSave.source === 'newest' && (
<div className="text-xs text-neutral-500 mt-1">neuester Speicherstand</div>
)}
</div>
)}
</div>
{/* Players List */}
{server.players?.list?.length > 0 && (
<div className="card p-4">
<h3 className="text-sm font-medium text-neutral-300 mb-3">Spieler Online</h3>
<div className="flex flex-wrap gap-2">
{server.players.list.map((player, i) => (
<span key={i} className="badge badge-secondary">{player}</span>
))}
</div>
</div>
)}
{/* Power Controls */}
{isModerator && (
<div className="card p-4">
<h3 className="text-sm font-medium text-neutral-300 mb-3">Server Steuerung</h3>
<div className="flex flex-wrap gap-3">
{(server.status === 'online' || (server.running && server.status !== 'starting' && server.status !== 'stopping')) ? (
<>
<button
onClick={() => handleAction('stop')}
disabled={server.status === 'stopping' || server.status === 'starting'}
className="btn btn-destructive"
>
{server.status === 'stopping' ? 'Stoppt...' : 'Server Stoppen'}
</button>
<button
onClick={() => handleAction('restart')}
disabled={server.status === 'stopping' || server.status === 'starting'}
className="btn btn-secondary"
>
{server.status === 'starting' ? 'Startet...' : 'Server Neustarten'}
</button>
</>
) : (
<button
onClick={() => handleAction('start')}
disabled={server.status === 'stopping' || server.status === 'starting'}
className="btn btn-primary"
>
{server.status === 'starting' ? 'Startet...' : 'Server Starten'}
</button>
)}
</div>
</div>
)}
</div>
)}
{/* Metrics Tab */}
{activeTab === 'metrics' && (
<div className="tab-content"><MetricsChart serverId={server.id} serverName={server.name} expanded={true} /></div>
)}
{/* Console Tab - Logs + RCON */}
{activeTab === 'console' && isModerator && (
<div className="space-y-4 tab-content">
{/* Logs */}
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<span className="text-sm text-neutral-400">Server Logs (letzte 50 Zeilen)</span>
{logsUpdated && (
<span className="text-xs text-neutral-600">
Aktualisiert: {logsUpdated.toLocaleTimeString('de-DE')}
</span>
)}
</div>
<button onClick={fetchLogs} className="btn btn-secondary">
Aktualisieren
</button>
</div>
<div
ref={logsRef}
className="terminal p-4 logs-container text-xs text-neutral-300 whitespace-pre-wrap"
>
{logs || 'Laden...'}
</div>
{/* RCON History */}
{rconHistory.length > 0 && (
<div ref={rconRef} className="terminal p-4 max-h-40 overflow-y-auto">
<div className="text-neutral-500 text-xs mb-2">RCON Verlauf</div>
{rconHistory.map((entry, i) => (
<div key={i} className="mb-2 text-sm">
<div className="text-neutral-400">
<span className="text-neutral-600">[{entry.time.toLocaleTimeString('de-DE')}]</span> &gt; {entry.cmd}
</div>
<div className={'whitespace-pre-wrap pl-4 ' + (entry.error ? 'text-red-400' : 'text-neutral-300')}>
{entry.res}
</div>
</div>
))}
</div>
)}
{/* RCON Input */}
{server.hasRcon && (
<form onSubmit={handleRcon} className="flex gap-2">
<input
type="text"
value={rconCommand}
onChange={(e) => setRconCommand(e.target.value)}
placeholder="RCON Befehl..."
className="input flex-1"
/>
<button type="submit" className="btn btn-primary">
Senden
</button>
</form>
)}
</div>
)}
{/* Whitelist Tab - Minecraft only */}
{activeTab === 'whitelist' && isModerator && server.type === 'minecraft' && (
<div className="space-y-4 tab-content">
<div className="card p-4">
<h3 className="text-sm font-medium text-neutral-300 mb-3">Zur Whitelist hinzufügen</h3>
<form onSubmit={addToWhitelist} className="flex gap-2">
<input
type="text"
value={whitelistInput}
onChange={(e) => setWhitelistInput(e.target.value)}
placeholder="Minecraft Benutzername..."
className="input flex-1"
disabled={whitelistLoading || !server.running}
/>
<button type="submit" className="btn btn-primary" disabled={whitelistLoading || !server.running}>
{whitelistLoading ? 'Wird hinzugefügt...' : 'Hinzufügen'}
</button>
</form>
</div>
<div className="card p-4">
<div className="flex justify-between items-center mb-3">
<h3 className="text-sm font-medium text-neutral-300">Whitelist Spieler ({whitelistPlayers.length})</h3>
<button onClick={fetchWhitelist} className="btn btn-ghost text-sm">
Aktualisieren
</button>
</div>
{whitelistPlayers.length > 0 ? (
<div className="flex flex-wrap gap-2">
{whitelistPlayers.map((player, i) => (
<div key={i} className="flex items-center gap-1 bg-neutral-800 rounded-full pl-3 pr-1 py-1">
<span className="text-sm text-neutral-200">{player}</span>
<button
onClick={() => removeFromWhitelist(player)}
disabled={whitelistLoading || !server.running}
className="w-6 h-6 flex items-center justify-center rounded-full hover:bg-neutral-700 text-neutral-400 hover:text-red-400"
>
x
</button>
</div>
))}
</div>
) : (
<div className="text-neutral-500 text-sm">Keine Spieler auf der Whitelist</div>
)}
</div>
</div>
)}
{/* Worlds Tab - Factorio only */}
{activeTab === 'worlds' && isModerator && server.type === 'factorio' && (
<div className="tab-content">
{server.running || server.status === 'starting' || server.status === 'stopping' ? (
<div className="card p-8 text-center">
<div className="text-neutral-400 mb-2">Weltverwaltung ist gesperrt während der Server läuft</div>
<div className="text-neutral-500 text-sm">Server stoppen um Speicherstände zu verwalten</div>
</div>
) : (
<FactorioWorldManager
server={server}
token={token}
onServerAction={fetchServer}
/>
)}
</div>
)}
{/* Settings Tab */}
{activeTab === 'settings' && isModerator && (
<div className="space-y-4 tab-content">
<div className="card p-4">
<h3 className="text-sm font-medium text-neutral-300 mb-4">Auto-Shutdown</h3>
<p className="text-neutral-500 text-sm mb-4">
Server automatisch stoppen wenn keine Spieler online sind
</p>
<div className="space-y-4">
{/* Toggle Switch */}
<div className="flex items-center gap-3">
<button
onClick={handleAutoShutdownToggle}
disabled={autoShutdownLoading}
className={`
relative w-14 h-8 rounded-full transition-all duration-200
${autoShutdown.enabled
? 'bg-green-600 border-green-500'
: 'bg-neutral-700 border-neutral-600'
}
border-2 focus:outline-none focus:ring-2 focus:ring-neutral-500
${autoShutdownLoading ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}
>
<span
className={`
absolute top-1 w-5 h-5 rounded-full bg-white shadow-md
transition-all duration-200 ease-in-out
${autoShutdown.enabled ? 'left-7' : 'left-1'}
`}
/>
</button>
<span className={`text-sm font-medium ${autoShutdown.enabled ? 'text-green-500' : 'text-neutral-500'}`}>
{autoShutdown.enabled ? 'Aktiviert' : 'Deaktiviert'}
</span>
</div>
{/* Timeout mit +/- Buttons */}
{autoShutdown.enabled && (
<div className="flex items-center gap-4 pt-2">
<span className="text-neutral-400 text-sm">Timeout:</span>
<div className="flex items-center gap-1">
<button
onClick={() => handleAutoShutdownTimeoutChange(autoShutdown.timeoutMinutes - 5)}
disabled={autoShutdown.timeoutMinutes <= 5}
className="btn btn-secondary px-3"
>
-
</button>
<span className="text-white text-lg font-medium w-16 text-center">
{autoShutdown.timeoutMinutes}
</span>
<button
onClick={() => handleAutoShutdownTimeoutChange(autoShutdown.timeoutMinutes + 5)}
disabled={autoShutdown.timeoutMinutes >= 1440}
className="btn btn-secondary px-3"
>
+
</button>
</div>
<span className="text-neutral-500 text-sm">Minuten</span>
</div>
)}
{/* Status */}
{autoShutdown.enabled && server.running && (
<div className="border-t border-neutral-800 pt-4 mt-2">
<div className="flex items-center gap-3">
<span className="text-neutral-400 text-sm">Status:</span>
{autoShutdown.emptySinceMinutes !== null ? (
<span className="badge badge-warning">
Leer seit {autoShutdown.emptySinceMinutes} Min. Shutdown in {autoShutdown.timeoutMinutes - autoShutdown.emptySinceMinutes} Min.
</span>
) : (
<span className="badge badge-success">
Spieler online
</span>
)}
</div>
</div>
)}
</div>
</div>
</div>
)}
</main>
</div>
)
}