- 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>
327 lines
12 KiB
JavaScript
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> > {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">></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>
|
|
)
|
|
}
|