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:
192
temp/MetricsChart.jsx
Normal file
192
temp/MetricsChart.jsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { AreaChart, Area, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
|
||||
import { getMetricsHistory } from '../api'
|
||||
import { useUser } from '../context/UserContext'
|
||||
|
||||
const GRAFANA_URL = 'https://grafana.dimension47.de'
|
||||
|
||||
export default function MetricsChart({ serverId, serverName, expanded = false }) {
|
||||
const { token } = useUser()
|
||||
const [data, setData] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [range, setRange] = useState('1h')
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const metrics = await getMetricsHistory(token, serverId, range)
|
||||
setData(metrics)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch metrics:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
const interval = setInterval(fetchData, 60000)
|
||||
return () => clearInterval(interval)
|
||||
}, [token, serverId, range])
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes < 1024) return bytes.toFixed(0) + ' B/s'
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB/s'
|
||||
return (bytes / 1024 / 1024).toFixed(1) + ' MB/s'
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="bg-black/50 border border-[#00ff41]/20 rounded p-4">
|
||||
<div className="h-20 flex items-center justify-center">
|
||||
<span className="text-[#00ff41]/50 font-mono text-sm animate-pulse">LOADING_METRICS...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data || data.cpu.length === 0) {
|
||||
return (
|
||||
<div className="bg-black/50 border border-[#00ff41]/20 rounded p-4">
|
||||
<div className="h-20 flex items-center justify-center">
|
||||
<span className="text-[#00ff41]/30 font-mono text-sm">NO_DATA_AVAILABLE</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const combinedData = data.cpu.map((point, i) => ({
|
||||
timestamp: point.timestamp,
|
||||
cpu: point.value,
|
||||
memory: data.memory[i]?.value || 0,
|
||||
networkRx: data.networkRx[i]?.value || 0,
|
||||
networkTx: data.networkTx[i]?.value || 0
|
||||
}))
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-black/95 border border-[#00ff41]/50 rounded p-3 font-mono text-xs">
|
||||
<div className="text-[#00ff41]/60 mb-2">{formatTime(label)}</div>
|
||||
{payload.map((entry, i) => (
|
||||
<div key={i} className="flex justify-between gap-4" style={{ color: entry.color }}>
|
||||
<span>{entry.name}:</span>
|
||||
<span className="font-bold">
|
||||
{entry.name.includes('Network') ? formatBytes(entry.value) : entry.value.toFixed(1) + '%'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const ChartCard = ({ title, dataKey, color, domain, formatter }) => (
|
||||
<div className="bg-black/50 border border-[#00ff41]/20 rounded p-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<span className="text-[#00ff41]/60 font-mono text-xs">{title}</span>
|
||||
<span className="text-[#00ff41] font-mono text-sm font-bold">
|
||||
{formatter
|
||||
? formatter(combinedData[combinedData.length - 1]?.[dataKey] || 0)
|
||||
: (combinedData[combinedData.length - 1]?.[dataKey] || 0).toFixed(1) + '%'
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={expanded ? 150 : 80}>
|
||||
<AreaChart data={combinedData}>
|
||||
<defs>
|
||||
<linearGradient id={`gradient-${dataKey}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={color} stopOpacity={0.4}/>
|
||||
<stop offset="95%" stopColor={color} stopOpacity={0.05}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{expanded && (
|
||||
<>
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={formatTime}
|
||||
tick={{ fill: '#00ff41', fontSize: 10, opacity: 0.5 }}
|
||||
axisLine={{ stroke: '#00ff41', opacity: 0.2 }}
|
||||
tickLine={false}
|
||||
minTickGap={50}
|
||||
/>
|
||||
<YAxis
|
||||
domain={domain || [0, 'auto']}
|
||||
tick={{ fill: '#00ff41', fontSize: 10, opacity: 0.5 }}
|
||||
axisLine={{ stroke: '#00ff41', opacity: 0.2 }}
|
||||
tickLine={false}
|
||||
width={40}
|
||||
tickFormatter={formatter ? (v) => formatBytes(v).split(' ')[0] : (v) => v.toFixed(0)}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
</>
|
||||
)}
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey={dataKey}
|
||||
name={title}
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
fill={`url(#gradient-${dataKey})`}
|
||||
isAnimationActive={true}
|
||||
animationDuration={500}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[#00ff41]/60 font-mono text-xs">
|
||||
// METRICS_HISTORY
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
value={range}
|
||||
onChange={(e) => setRange(e.target.value)}
|
||||
className="input-matrix px-3 py-1 rounded font-mono text-xs"
|
||||
>
|
||||
<option value="15m">15 MIN</option>
|
||||
<option value="1h">1 HOUR</option>
|
||||
<option value="6h">6 HOURS</option>
|
||||
<option value="24h">24 HOURS</option>
|
||||
</select>
|
||||
<a
|
||||
href={`${GRAFANA_URL}/d/rYdddlPWk/node-exporter-full?var-job=${serverId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-matrix px-3 py-1 text-xs font-mono flex items-center gap-2"
|
||||
>
|
||||
<span>📊</span>
|
||||
GRAFANA
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
{expanded ? (
|
||||
<div className="space-y-4">
|
||||
<ChartCard title="CPU_USAGE" dataKey="cpu" color="#00ff41" domain={[0, 100]} />
|
||||
<ChartCard title="RAM_USAGE" dataKey="memory" color="#00ffff" domain={[0, 100]} />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<ChartCard title="NETWORK_RX" dataKey="networkRx" color="#ff00ff" formatter={formatBytes} />
|
||||
<ChartCard title="NETWORK_TX" dataKey="networkTx" color="#ffff00" formatter={formatBytes} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<ChartCard title="CPU" dataKey="cpu" color="#00ff41" domain={[0, 100]} />
|
||||
<ChartCard title="RAM" dataKey="memory" color="#00ffff" domain={[0, 100]} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user