All checks were successful
Deploy GSM / deploy (push) Successful in 30s
- Unreachable cards now show a disabled 'Starten' button so moderator cards stay the same height as reachable ones - Dashboard hides unreachable servers by default; a toggle button (bottom of header area, only visible when unreachable count > 0) flips the view and persists the preference in localStorage Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
349 lines
12 KiB
JavaScript
349 lines
12 KiB
JavaScript
import { useState } from 'react'
|
|
import { serverAction } from '../api'
|
|
import ConfirmModal from './ConfirmModal'
|
|
|
|
const serverInfo = {
|
|
minecraft: {
|
|
address: 'minecraft.zeasy.dev',
|
|
logo: '/minecraft.png',
|
|
links: [
|
|
{ label: 'Biohazard Modpack', url: 'https://www.curseforge.com/minecraft/modpacks/biohazard-project-genesis' }
|
|
]
|
|
},
|
|
factorio: {
|
|
hint: 'Serverpasswort: affe',
|
|
address: 'factorio.zeasy.dev',
|
|
logo: '/factorio.png',
|
|
links: [
|
|
{ label: 'Steam', url: 'https://store.steampowered.com/app/427520/Factorio/' }
|
|
]
|
|
},
|
|
vrising: {
|
|
address: 'Zeasy Software Vampire',
|
|
logo: '/vrising.png',
|
|
links: [
|
|
{ label: 'Steam', url: 'https://store.steampowered.com/app/1604030/V_Rising/' }
|
|
]
|
|
},
|
|
zomboid: {
|
|
hint: 'Version 42.13.1',
|
|
address: 'pz.zeasy.dev:16261',
|
|
logo: '/zomboid.png',
|
|
links: [
|
|
{ label: 'Steam', url: 'https://store.steampowered.com/app/108600/Project_Zomboid/' }
|
|
]
|
|
},
|
|
palworld: {
|
|
address: 'palworld.zeasy.dev:8211',
|
|
logo: '/palworld.png',
|
|
links: [
|
|
{ label: 'Steam', url: 'https://store.steampowered.com/app/1623730/Palworld/' }
|
|
]
|
|
},
|
|
terraria: {
|
|
address: 'terraria.zeasy.dev:7777',
|
|
logo: '/terraria.png',
|
|
links: [
|
|
{ label: 'Steam', url: 'https://store.steampowered.com/app/105600/Terraria/' }
|
|
]
|
|
},
|
|
openttd: {
|
|
address: 'openttd.zeasy.dev:3979',
|
|
logo: '/openttd.png',
|
|
links: [
|
|
{ label: 'Steam', url: 'https://store.steampowered.com/app/1536610/OpenTTD/' }
|
|
]
|
|
},
|
|
hytale: {
|
|
address: 'hytale.zeasy.dev',
|
|
logo: '/hytale.png',
|
|
links: [
|
|
{ label: 'Website', url: 'https://hytale.com/' }
|
|
]
|
|
},
|
|
spaceengineers: {
|
|
address: 'space.zeasy.dev:27020',
|
|
logo: '/spaceengineers.png',
|
|
links: [
|
|
{ label: 'Steam', url: 'https://store.steampowered.com/app/244850/Space_Engineers/' }
|
|
]
|
|
}
|
|
}
|
|
|
|
const getServerInfo = (serverName) => {
|
|
const name = serverName.toLowerCase()
|
|
if (name.includes('minecraft') || name.includes('all the mods')) return serverInfo.minecraft
|
|
if (name.includes('factorio')) return serverInfo.factorio
|
|
if (name.includes('vrising') || name.includes('v rising')) return serverInfo.vrising
|
|
if (name.includes('zomboid')) return serverInfo.zomboid
|
|
if (name.includes('palworld')) return serverInfo.palworld
|
|
if (name.includes('terraria')) return serverInfo.terraria
|
|
if (name.includes('openttd')) return serverInfo.openttd
|
|
if (name.includes('hytale')) return serverInfo.hytale
|
|
if (name.includes('space engineers') || name.includes('spaceengineers')) return serverInfo.spaceengineers
|
|
return null
|
|
}
|
|
|
|
export default function ServerCard({ server, onClick, isAuthenticated, isGuest, displaySettings, isModerator, token, onServerAction }) {
|
|
const defaultInfo = getServerInfo(server.name)
|
|
const [confirmAction, setConfirmAction] = useState(null)
|
|
const [actionLoading, setActionLoading] = useState(false)
|
|
|
|
// Merge default info with database display settings (database takes priority)
|
|
const info = defaultInfo ? {
|
|
...defaultInfo,
|
|
address: displaySettings?.address || defaultInfo.address,
|
|
hint: displaySettings?.hint || defaultInfo.hint
|
|
} : (displaySettings ? {
|
|
address: displaySettings.address,
|
|
hint: displaySettings.hint,
|
|
logo: null,
|
|
links: []
|
|
} : null)
|
|
|
|
const formatUptime = (seconds) => {
|
|
const hours = Math.floor(seconds / 3600)
|
|
if (hours > 24) {
|
|
const days = Math.floor(hours / 24)
|
|
return days + 'd ' + (hours % 24) + 'h'
|
|
}
|
|
const minutes = Math.floor((seconds % 3600) / 60)
|
|
return hours + 'h ' + minutes + 'm'
|
|
}
|
|
|
|
const cpuPercent = Math.min(server.metrics.cpu, 100)
|
|
const memPercent = Math.min(server.metrics.memory, 100)
|
|
|
|
const getProgressColor = (percent) => {
|
|
if (percent > 80) return 'progress-bar-danger'
|
|
if (percent > 60) return 'progress-bar-warning'
|
|
return 'progress-bar-success'
|
|
}
|
|
|
|
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: 'Starting...' }
|
|
case 'stopping':
|
|
return { class: 'badge badge-warning', text: 'Stopping...' }
|
|
case 'unreachable':
|
|
return { class: 'badge bg-neutral-600 text-neutral-400', text: 'Nicht erreichbar' }
|
|
default:
|
|
return { class: 'badge badge-destructive', text: 'Offline' }
|
|
}
|
|
}
|
|
|
|
const statusBadge = getStatusBadge()
|
|
const isUnreachable = server.status === 'unreachable'
|
|
|
|
const isClickable = !isUnreachable && !isGuest && onClick
|
|
|
|
const handleAction = async (action) => {
|
|
setActionLoading(true)
|
|
try {
|
|
await serverAction(token, server.id, action)
|
|
if (onServerAction) onServerAction()
|
|
} catch (err) {
|
|
console.error(err)
|
|
} finally {
|
|
setActionLoading(false)
|
|
setConfirmAction(null)
|
|
}
|
|
}
|
|
|
|
const handleActionClick = (action, e) => {
|
|
e.stopPropagation()
|
|
if (action === 'start') {
|
|
handleAction(action)
|
|
} else {
|
|
setConfirmAction(action)
|
|
}
|
|
}
|
|
|
|
const isActionDisabled = actionLoading || server.status === 'starting' || server.status === 'stopping'
|
|
|
|
return (
|
|
<div
|
|
className={isUnreachable ? "card p-5 opacity-50 cursor-not-allowed" : (isClickable ? "card card-clickable p-5" : "card p-5")}
|
|
onClick={isClickable ? onClick : undefined}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-3">
|
|
{info && info.logo && <img src={info.logo} alt="" className="h-8 w-8 object-contain" />}
|
|
<h3 className="text-lg font-semibold text-white">{server.name}</h3>
|
|
</div>
|
|
<span className={statusBadge.class}>
|
|
{statusBadge.text}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Server Address & Links */}
|
|
{info && (
|
|
<div className="mb-4 flex items-center gap-3 text-sm">
|
|
<code className="text-neutral-400 bg-neutral-800 px-2 py-0.5 rounded">
|
|
{info.address}
|
|
</code>
|
|
{info.links.map((link, i) => (
|
|
<a
|
|
key={i}
|
|
href={link.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="text-blue-400 hover:text-blue-300 hover:underline"
|
|
>
|
|
{link.label}
|
|
</a>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Server hint - only for authenticated users */}
|
|
{isAuthenticated && info?.hint && (
|
|
<div className="mb-4 text-xs text-neutral-500">
|
|
{info.hint}
|
|
</div>
|
|
)}
|
|
|
|
{/* Whitelist notice for Minecraft - only if no custom hint is set */}
|
|
{isAuthenticated && server.type === 'minecraft' && !displaySettings?.hint && (
|
|
<div className="mb-4 text-xs text-neutral-500">
|
|
Whitelist erforderlich - im Whitelist-Tab freischalten
|
|
</div>
|
|
)}
|
|
|
|
{/* Metrics */}
|
|
<div className="space-y-3">
|
|
{/* CPU */}
|
|
<div>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-neutral-400">CPU</span>
|
|
<span className="text-white">{server.metrics.cpu.toFixed(1)}%</span>
|
|
</div>
|
|
<div className="progress">
|
|
<div
|
|
className={'progress-bar ' + getProgressColor(cpuPercent)}
|
|
style={{ width: cpuPercent + '%' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* RAM */}
|
|
<div>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-neutral-400">Memory</span>
|
|
<span className="text-white">
|
|
{server.metrics.memoryUsed?.toFixed(1) || 0} / {server.metrics.memoryTotal?.toFixed(1) || 0} {server.metrics.memoryUnit}
|
|
</span>
|
|
</div>
|
|
<div className="progress">
|
|
<div
|
|
className={'progress-bar ' + getProgressColor(memPercent)}
|
|
style={{ width: memPercent + '%' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer Stats */}
|
|
<div className="flex items-center justify-between mt-4 pt-4 border-t border-neutral-800 text-sm">
|
|
<div className="flex items-center gap-3">
|
|
<div className="text-neutral-400">
|
|
<span className="text-white font-medium">{server.players.online}</span>
|
|
{server.players.max ? ' / ' + server.players.max : ''} Spieler
|
|
</div>
|
|
{server.running && server.autoShutdown?.enabled && server.autoShutdown?.emptySinceMinutes !== null && (
|
|
<div className="flex items-center gap-1.5 text-yellow-500">
|
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span className="text-xs">
|
|
Shutdown in {server.autoShutdown.timeoutMinutes - server.autoShutdown.emptySinceMinutes}m
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{server.running && (
|
|
<div className="text-neutral-400">
|
|
Laufzeit: <span className="text-white">{formatUptime(server.metrics.uptime)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Players List */}
|
|
{server.players?.list?.length > 0 && (
|
|
<div className="mt-3 pt-3 border-t border-neutral-800">
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{server.players.list.map((player, i) => (
|
|
<span key={i} className="badge badge-secondary">
|
|
{player}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Server Controls - only for moderators */}
|
|
{isModerator && (
|
|
<div className="mt-3 pt-3 border-t border-neutral-800">
|
|
<div className="flex flex-wrap gap-2">
|
|
{isUnreachable ? (
|
|
<button
|
|
disabled
|
|
title="Host nicht erreichbar"
|
|
className="btn btn-primary btn-sm"
|
|
>
|
|
Starten
|
|
</button>
|
|
) : (server.status === 'online' || (server.running && server.status !== 'starting' && server.status !== 'stopping')) ? (
|
|
<>
|
|
<button
|
|
onClick={(e) => handleActionClick('stop', e)}
|
|
disabled={isActionDisabled}
|
|
className="btn btn-destructive btn-sm"
|
|
>
|
|
{server.status === 'stopping' ? 'Stoppt...' : 'Stoppen'}
|
|
</button>
|
|
<button
|
|
onClick={(e) => handleActionClick('restart', e)}
|
|
disabled={isActionDisabled}
|
|
className="btn btn-secondary btn-sm"
|
|
>
|
|
{server.status === 'starting' ? 'Startet...' : 'Neustarten'}
|
|
</button>
|
|
</>
|
|
) : (
|
|
<button
|
|
onClick={(e) => handleActionClick('start', e)}
|
|
disabled={isActionDisabled}
|
|
className="btn btn-primary btn-sm"
|
|
>
|
|
{server.status === 'starting' ? 'Startet...' : 'Starten'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Confirmation Modal */}
|
|
{confirmAction && (
|
|
<ConfirmModal
|
|
title={confirmAction === 'stop' ? 'Server stoppen?' : 'Server neustarten?'}
|
|
message={confirmAction === 'stop'
|
|
? `Bist du sicher, dass du ${server.name} stoppen möchtest?`
|
|
: `Bist du sicher, dass du ${server.name} neustarten möchtest?`
|
|
}
|
|
confirmText={confirmAction === 'stop' ? 'Stoppen' : 'Neustarten'}
|
|
variant={confirmAction === 'stop' ? 'danger' : 'primary'}
|
|
onConfirm={() => handleAction(confirmAction)}
|
|
onCancel={() => setConfirmAction(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|