Files
GSM/temp/ServerDetailModal.jsx
Alexander Zielonka 2b1fbb9f02 Initial commit: Homelab documentation
- infrastructure.md: Network topology, server overview, credentials
- gsm.md: Gameserver Monitor detailed documentation
- todo.md: Project roadmap and completed tasks
- CLAUDE.md: AI assistant context
- temp/: Frontend component backups

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 06:16:05 +01:00

327 lines
12 KiB
JavaScript

import { useState, useEffect, useRef } from 'react'
import { serverAction, sendRcon, getServerLogs } from '../api'
import { useUser } from '../context/UserContext'
import MetricsChart from './MetricsChart'
export default function ServerDetailModal({ server, onClose, onUpdate }) {
const { token, isModerator } = useUser()
const [loading, setLoading] = useState(false)
const [activeTab, setActiveTab] = useState('overview')
const [rconCommand, setRconCommand] = useState('')
const [rconHistory, setRconHistory] = useState([])
const [logs, setLogs] = useState('')
const logsRef = useRef(null)
const rconRef = useRef(null)
const typeIcons = {
minecraft: '⛏️',
factorio: '⚙️'
}
const handleAction = async (action) => {
setLoading(true)
try {
await serverAction(token, server.id, action)
setTimeout(() => {
onUpdate()
setLoading(false)
}, 2000)
} catch (err) {
console.error(err)
setLoading(false)
}
}
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, 100)
setLogs(data.logs || '')
if (logsRef.current) {
logsRef.current.scrollTop = logsRef.current.scrollHeight
}
} catch (err) {
console.error(err)
}
}
useEffect(() => {
if (activeTab === 'logs' && isModerator) {
fetchLogs()
const interval = setInterval(fetchLogs, 5000)
return () => clearInterval(interval)
}
}, [activeTab, isModerator])
useEffect(() => {
if (rconRef.current) {
rconRef.current.scrollTop = rconRef.current.scrollHeight
}
}, [rconHistory])
useEffect(() => {
const handleEsc = (e) => {
if (e.key === 'Escape') onClose()
}
window.addEventListener('keydown', handleEsc)
return () => window.removeEventListener('keydown', handleEsc)
}, [onClose])
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: 'OVERVIEW', icon: '📊' },
{ id: 'metrics', label: 'METRICS', icon: '📈' },
...(isModerator ? [
{ id: 'console', label: 'CONSOLE', icon: '💻' },
{ id: 'logs', label: 'LOGS', icon: '📜' },
] : []),
]
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 modal-backdrop"
onClick={onClose}
/>
{/* Modal */}
<div className="relative w-full max-w-4xl max-h-[90vh] overflow-hidden bg-black/95 border border-[#00ff41]/50 rounded-lg glow-box fade-in-up">
{/* Header */}
<div className="border-b border-[#00ff41]/30 bg-[#00ff41]/5 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-4xl">{typeIcons[server.type] || '🎮'}</span>
<div>
<h2 className="text-2xl font-bold text-[#00ff41] font-mono tracking-wider glow-green">
{server.name.toUpperCase()}
</h2>
<div className="flex items-center gap-3 mt-1">
<span className={`flex items-center gap-2 text-sm font-mono ${server.running ? 'text-[#00ff41]' : 'text-red-500'}`}>
<span className={`w-2 h-2 rounded-full ${server.running ? 'status-online' : 'status-offline'}`} />
{server.running ? 'ONLINE' : 'OFFLINE'}
</span>
<span className="text-[#00ff41]/50 text-sm font-mono">|</span>
<span className="text-[#00ff41]/50 text-sm font-mono">
UPTIME: {formatUptime(server.metrics.uptime)}
</span>
</div>
</div>
</div>
<button
onClick={onClose}
className="text-[#00ff41]/60 hover:text-[#00ff41] transition-colors p-2"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Tabs */}
<div className="flex gap-1 mt-4">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 font-mono text-sm transition-all duration-300 rounded-t ${
activeTab === tab.id
? 'bg-[#00ff41]/20 text-[#00ff41] border-t border-l border-r border-[#00ff41]/50'
: 'text-[#00ff41]/50 hover:text-[#00ff41] hover:bg-[#00ff41]/5'
}`}
>
<span className="mr-2">{tab.icon}</span>
{tab.label}
</button>
))}
</div>
</div>
{/* Content */}
<div className="p-6 overflow-y-auto max-h-[calc(90vh-200px)]">
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<StatBox label="CPU" value={`${server.metrics.cpu.toFixed(1)}%`} sub={`${server.metrics.cpuCores} cores`} />
<StatBox label="RAM" value={`${server.metrics.memoryUsed?.toFixed(1)} ${server.metrics.memoryUnit}`} sub={`/ ${server.metrics.memoryTotal?.toFixed(1)} ${server.metrics.memoryUnit}`} />
<StatBox label="PLAYERS" value={`${server.players.online}`} sub={server.players.max ? `/ ${server.players.max}` : 'unlimited'} />
<StatBox label="TYPE" value={server.type.toUpperCase()} sub={server.id} />
</div>
{/* Players List */}
{server.players?.list?.length > 0 && (
<div className="bg-black/50 border border-[#00ff41]/20 rounded p-4">
<h3 className="text-[#00ff41] font-mono text-sm mb-3">CONNECTED_USERS:</h3>
<div className="flex flex-wrap gap-2">
{server.players.list.map((player, i) => (
<span
key={i}
className="bg-[#00ff41]/10 border border-[#00ff41]/30 text-[#00ff41] px-3 py-1 rounded font-mono text-sm"
>
{player}
</span>
))}
</div>
</div>
)}
{/* Power Controls */}
{isModerator && (
<div className="bg-black/50 border border-[#00ff41]/20 rounded p-4">
<h3 className="text-[#00ff41] font-mono text-sm mb-3">POWER_CONTROLS:</h3>
<div className="flex gap-3">
{server.running ? (
<>
<button
onClick={() => handleAction('stop')}
disabled={loading}
className="btn-matrix px-6 py-2 text-red-500 border-red-500 hover:bg-red-500/20 disabled:opacity-50"
>
{loading ? 'PROCESSING...' : 'STOP'}
</button>
<button
onClick={() => handleAction('restart')}
disabled={loading}
className="btn-matrix px-6 py-2 text-yellow-500 border-yellow-500 hover:bg-yellow-500/20 disabled:opacity-50"
>
{loading ? 'PROCESSING...' : 'RESTART'}
</button>
</>
) : (
<button
onClick={() => handleAction('start')}
disabled={loading}
className="btn-matrix-solid px-6 py-2 disabled:opacity-50"
>
{loading ? 'PROCESSING...' : 'START'}
</button>
)}
</div>
</div>
)}
</div>
)}
{/* Metrics Tab */}
{activeTab === 'metrics' && (
<div className="space-y-4">
<MetricsChart serverId={server.id} serverName={server.name} expanded={true} />
</div>
)}
{/* Console Tab */}
{activeTab === 'console' && isModerator && server.hasRcon && (
<div className="space-y-4">
<div
ref={rconRef}
className="terminal rounded h-80 overflow-y-auto p-4"
>
<div className="text-[#00ff41]/60 font-mono text-sm mb-2">
// RCON Terminal - {server.name}
</div>
{rconHistory.length === 0 && (
<div className="text-[#00ff41]/40 font-mono text-sm">
Waiting for commands...
</div>
)}
{rconHistory.map((entry, i) => (
<div key={i} className="mb-2">
<div className="text-[#00ff41] font-mono text-sm">
<span className="text-[#00ff41]/50">[{entry.time.toLocaleTimeString()}]</span> &gt; {entry.cmd}
</div>
<div className={`font-mono text-sm whitespace-pre-wrap pl-4 ${entry.error ? 'text-red-500' : 'text-[#00ff41]/70'}`}>
{entry.res}
</div>
</div>
))}
</div>
<form onSubmit={handleRcon} className="flex gap-2">
<span className="text-[#00ff41] font-mono py-2">&gt;</span>
<input
type="text"
value={rconCommand}
onChange={(e) => setRconCommand(e.target.value)}
placeholder="Enter RCON command..."
className="input-matrix flex-1 px-4 py-2 rounded font-mono text-sm"
/>
<button
type="submit"
className="btn-matrix-solid px-6 py-2 font-mono text-sm"
>
EXECUTE
</button>
</form>
</div>
)}
{/* Logs Tab */}
{activeTab === 'logs' && isModerator && (
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-[#00ff41]/60 font-mono text-sm">
// Server Logs - Last 100 lines
</span>
<button
onClick={fetchLogs}
className="btn-matrix px-4 py-1 text-sm font-mono"
>
REFRESH
</button>
</div>
<div
ref={logsRef}
className="terminal rounded h-96 overflow-y-auto p-4 font-mono text-xs text-[#00ff41]/80 whitespace-pre-wrap"
>
{logs || 'Loading...'}
</div>
</div>
)}
</div>
{/* Footer */}
<div className="border-t border-[#00ff41]/30 bg-[#00ff41]/5 px-6 py-3">
<div className="flex justify-between items-center text-[#00ff41]/40 font-mono text-xs">
<span>SERVER_ID: {server.id}</span>
<span>PRESS [ESC] TO CLOSE</span>
</div>
</div>
</div>
</div>
)
}
function StatBox({ label, value, sub }) {
return (
<div className="bg-black/50 border border-[#00ff41]/20 rounded p-4 text-center">
<div className="text-[#00ff41]/50 font-mono text-xs mb-1">{label}</div>
<div className="text-[#00ff41] font-mono text-2xl font-bold glow-green metric-value">{value}</div>
<div className="text-[#00ff41]/40 font-mono text-xs mt-1">{sub}</div>
</div>
)
}