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>
This commit is contained in:
323
temp/ServerDetail.jsx
Normal file
323
temp/ServerDetail.jsx
Normal file
@@ -0,0 +1,323 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { getServers, serverAction, sendRcon, getServerLogs } from '../api'
|
||||
import { useUser } from '../context/UserContext'
|
||||
import MetricsChart from '../components/MetricsChart'
|
||||
|
||||
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 [actionLoading, setActionLoading] = 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 fetchServer = async () => {
|
||||
try {
|
||||
const servers = await getServers(token)
|
||||
const found = servers.find(s => s.id === serverId)
|
||||
if (found) {
|
||||
setServer(found)
|
||||
} else {
|
||||
navigate('/')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
navigate('/')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchServer()
|
||||
const interval = setInterval(fetchServer, 10000)
|
||||
return () => clearInterval(interval)
|
||||
}, [token, serverId])
|
||||
|
||||
const handleAction = async (action) => {
|
||||
setActionLoading(true)
|
||||
try {
|
||||
await serverAction(token, server.id, action)
|
||||
setTimeout(() => {
|
||||
fetchServer()
|
||||
setActionLoading(false)
|
||||
}, 2000)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setActionLoading(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, 20)
|
||||
setLogs(data.logs || '')
|
||||
if (logsRef.current) {
|
||||
logsRef.current.scrollTop = logsRef.current.scrollHeight
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'logs' && isModerator && server) {
|
||||
fetchLogs()
|
||||
const interval = setInterval(fetchLogs, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [activeTab, isModerator, server])
|
||||
|
||||
useEffect(() => {
|
||||
if (rconRef.current) {
|
||||
rconRef.current.scrollTop = rconRef.current.scrollHeight
|
||||
}
|
||||
}, [rconHistory])
|
||||
|
||||
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' },
|
||||
{ id: 'metrics', label: 'Metrics' },
|
||||
...(isModerator ? [
|
||||
{ id: 'console', label: 'Console' },
|
||||
{ id: 'logs', label: 'Logs' },
|
||||
] : []),
|
||||
]
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-neutral-400">Loading...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!server) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-neutral-400">Server not found</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const cpuPercent = Math.min(server.metrics.cpu, 100)
|
||||
const memPercent = Math.min(server.metrics.memory, 100)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* 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"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-xl font-semibold text-white">{server.name}</h1>
|
||||
<span className={server.running ? 'badge badge-success' : 'badge badge-destructive'}>
|
||||
{server.running ? 'Online' : 'Offline'}
|
||||
</span>
|
||||
</div>
|
||||
{server.running && (
|
||||
<p className="text-sm text-neutral-400 mt-1">
|
||||
Uptime: {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">
|
||||
{/* 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 Usage</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">Memory</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">
|
||||
of {server.metrics.memoryTotal?.toFixed(1)} {server.metrics.memoryUnit}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card p-4">
|
||||
<div className="text-sm text-neutral-400">Players</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 ? 'of ' + server.players.max + ' max' : 'No limit'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card p-4">
|
||||
<div className="text-sm text-neutral-400">CPU Cores</div>
|
||||
<div className="text-2xl font-semibold text-white mt-1">{server.metrics.cpuCores}</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">Online Players</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 Controls</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{server.running ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleAction('stop')}
|
||||
disabled={actionLoading}
|
||||
className="btn btn-destructive"
|
||||
>
|
||||
{actionLoading ? 'Processing...' : 'Stop Server'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAction('restart')}
|
||||
disabled={actionLoading}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
{actionLoading ? 'Processing...' : 'Restart Server'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleAction('start')}
|
||||
disabled={actionLoading}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{actionLoading ? 'Processing...' : 'Start Server'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metrics Tab */}
|
||||
{activeTab === 'metrics' && (
|
||||
<MetricsChart serverId={server.id} serverName={server.name} expanded={true} />
|
||||
)}
|
||||
|
||||
{/* Console Tab */}
|
||||
{activeTab === 'console' && isModerator && (
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
ref={rconRef}
|
||||
className="terminal p-4 h-80 overflow-y-auto"
|
||||
>
|
||||
<div className="text-neutral-500 text-sm mb-2">RCON Console - {server.name}</div>
|
||||
{rconHistory.length === 0 && (
|
||||
<div className="text-neutral-600 text-sm">Waiting for commands...</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()}]</span> > {entry.cmd}
|
||||
</div>
|
||||
<div className={'whitespace-pre-wrap pl-4 ' + (entry.error ? 'text-red-400' : 'text-neutral-300')}>
|
||||
{entry.res}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleRcon} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={rconCommand}
|
||||
onChange={(e) => setRconCommand(e.target.value)}
|
||||
placeholder="Enter command..."
|
||||
className="input flex-1"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Logs Tab */}
|
||||
{activeTab === 'logs' && isModerator && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-neutral-400">Last 20 lines</span>
|
||||
<button onClick={fetchLogs} className="btn btn-secondary">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref={logsRef}
|
||||
className="terminal p-4 logs-container text-xs text-neutral-300 whitespace-pre-wrap"
|
||||
>
|
||||
{logs || 'Loading...'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user