Files
CubeAdmin/app/(dashboard)/server/page.tsx
2026-03-08 15:49:34 +01:00

363 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState, useCallback } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Settings, RefreshCw, Download, Server, Map } from "lucide-react";
import { toast } from "sonner";
interface ServerSettings {
minecraftPath?: string;
serverJar?: string;
serverVersion?: string;
serverType?: string;
maxRam?: number;
minRam?: number;
rconEnabled?: boolean;
rconPort?: number;
javaArgs?: string;
autoStart?: boolean;
restartOnCrash?: boolean;
backupEnabled?: boolean;
backupSchedule?: string;
bluemapEnabled?: boolean;
bluemapUrl?: string;
}
const SERVER_TYPES = ["vanilla", "paper", "spigot", "bukkit", "fabric", "forge", "bedrock"];
export default function ServerPage() {
const [settings, setSettings] = useState<ServerSettings>({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [versions, setVersions] = useState<string[]>([]);
const [loadingVersions, setLoadingVersions] = useState(false);
const [selectedType, setSelectedType] = useState("paper");
const fetchSettings = useCallback(async () => {
try {
const res = await fetch("/api/server/settings");
if (res.ok) {
const data = await res.json();
if (data.settings) {
setSettings(data.settings);
setSelectedType(data.settings.serverType ?? "paper");
}
}
} finally {
setLoading(false);
}
}, []);
const fetchVersions = useCallback(async (type: string) => {
setLoadingVersions(true);
try {
const res = await fetch(`/api/server/versions?type=${type}`);
if (res.ok) setVersions((await res.json()).versions);
} finally {
setLoadingVersions(false);
}
}, []);
useEffect(() => { fetchSettings(); }, [fetchSettings]);
useEffect(() => { fetchVersions(selectedType); }, [selectedType, fetchVersions]);
const save = async (updates: Partial<ServerSettings>) => {
setSaving(true);
try {
const res = await fetch("/api/server/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updates),
});
if (!res.ok) throw new Error((await res.json()).error);
setSettings((prev) => ({ ...prev, ...updates }));
toast.success("Settings saved");
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to save");
} finally {
setSaving(false);
}
};
if (loading) {
return (
<div className="p-6 space-y-6">
<Skeleton className="h-8 w-48 bg-zinc-800" />
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-48 w-full bg-zinc-800 rounded-xl" />
))}
</div>
);
}
return (
<div className="p-6 space-y-6">
<div>
<h1 className="text-2xl font-bold text-white">Server Settings</h1>
<p className="text-zinc-400 text-sm mt-1">
Configure your Minecraft server and CubeAdmin options
</p>
</div>
<Tabs defaultValue="general" className="space-y-4">
<TabsList className="bg-zinc-900 border border-zinc-800">
<TabsTrigger value="general" className="data-[state=active]:bg-zinc-800 data-[state=active]:text-white text-zinc-400">
General
</TabsTrigger>
<TabsTrigger value="performance" className="data-[state=active]:bg-zinc-800 data-[state=active]:text-white text-zinc-400">
Performance
</TabsTrigger>
<TabsTrigger value="updates" className="data-[state=active]:bg-zinc-800 data-[state=active]:text-white text-zinc-400">
Updates
</TabsTrigger>
<TabsTrigger value="integrations" className="data-[state=active]:bg-zinc-800 data-[state=active]:text-white text-zinc-400">
Integrations
</TabsTrigger>
</TabsList>
{/* General */}
<TabsContent value="general" className="space-y-4">
<Card className="bg-zinc-900 border-zinc-800">
<CardHeader>
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
<Server className="w-4 h-4 text-emerald-500" />
Server Configuration
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-zinc-300">Minecraft Server Path</Label>
<Input
value={settings.minecraftPath ?? ""}
onChange={(e) => setSettings(p => ({ ...p, minecraftPath: e.target.value }))}
placeholder="/opt/minecraft/server"
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 font-mono text-sm"
/>
</div>
<div className="space-y-1.5">
<Label className="text-zinc-300">Server JAR filename</Label>
<Input
value={settings.serverJar ?? ""}
onChange={(e) => setSettings(p => ({ ...p, serverJar: e.target.value }))}
placeholder="server.jar"
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 font-mono text-sm"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800 border border-zinc-700">
<div>
<p className="text-sm font-medium text-zinc-300">Auto-start on boot</p>
<p className="text-xs text-zinc-500 mt-0.5">Start server when CubeAdmin starts</p>
</div>
<Switch
checked={settings.autoStart ?? false}
onCheckedChange={(v) => save({ autoStart: v })}
/>
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800 border border-zinc-700">
<div>
<p className="text-sm font-medium text-zinc-300">Auto-restart on crash</p>
<p className="text-xs text-zinc-500 mt-0.5">Automatically restart if server crashes</p>
</div>
<Switch
checked={settings.restartOnCrash ?? false}
onCheckedChange={(v) => save({ restartOnCrash: v })}
/>
</div>
</div>
<Button
onClick={() => save({ minecraftPath: settings.minecraftPath, serverJar: settings.serverJar })}
disabled={saving}
className="bg-emerald-600 hover:bg-emerald-500 text-white"
size="sm"
>
{saving ? "Saving..." : "Save Changes"}
</Button>
</CardContent>
</Card>
</TabsContent>
{/* Performance */}
<TabsContent value="performance" className="space-y-4">
<Card className="bg-zinc-900 border-zinc-800">
<CardHeader>
<CardTitle className="text-base font-medium text-zinc-300">JVM Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-zinc-300">Min RAM (MB)</Label>
<Input
type="number"
value={settings.minRam ?? 512}
onChange={(e) => setSettings(p => ({ ...p, minRam: parseInt(e.target.value) }))}
className="bg-zinc-800 border-zinc-700 text-white"
min={256}
/>
</div>
<div className="space-y-1.5">
<Label className="text-zinc-300">Max RAM (MB)</Label>
<Input
type="number"
value={settings.maxRam ?? 2048}
onChange={(e) => setSettings(p => ({ ...p, maxRam: parseInt(e.target.value) }))}
className="bg-zinc-800 border-zinc-700 text-white"
min={512}
/>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-zinc-300">Additional Java Arguments</Label>
<Input
value={settings.javaArgs ?? ""}
onChange={(e) => setSettings(p => ({ ...p, javaArgs: e.target.value }))}
placeholder="-XX:+UseG1GC -XX:MaxGCPauseMillis=200"
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 font-mono text-sm"
/>
<p className="text-xs text-zinc-500">
Aikar&apos;s flags are applied by default with the Docker image.
</p>
</div>
<Button
onClick={() => save({ minRam: settings.minRam, maxRam: settings.maxRam, javaArgs: settings.javaArgs })}
disabled={saving}
className="bg-emerald-600 hover:bg-emerald-500 text-white"
size="sm"
>
{saving ? "Saving..." : "Save Changes"}
</Button>
</CardContent>
</Card>
</TabsContent>
{/* Updates */}
<TabsContent value="updates" className="space-y-4">
<Card className="bg-zinc-900 border-zinc-800">
<CardHeader>
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
<Download className="w-4 h-4 text-emerald-500" />
Server Version
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-zinc-300">Server Type</Label>
<Select
value={selectedType}
onValueChange={(v) => { if (v) { setSelectedType(v); fetchVersions(v); } }}
>
<SelectTrigger className="bg-zinc-800 border-zinc-700 text-white">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-zinc-800 border-zinc-700">
{SERVER_TYPES.map((t) => (
<SelectItem key={t} value={t} className="text-zinc-300 focus:bg-zinc-700 focus:text-white capitalize">
{t.charAt(0).toUpperCase() + t.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-zinc-300">Version</Label>
<Select
value={settings.serverVersion ?? ""}
onValueChange={(v) => setSettings(p => ({ ...p, serverVersion: v ?? undefined }))}
disabled={loadingVersions}
>
<SelectTrigger className="bg-zinc-800 border-zinc-700 text-white">
<SelectValue placeholder={loadingVersions ? "Loading..." : "Select version"} />
</SelectTrigger>
<SelectContent className="bg-zinc-800 border-zinc-700 max-h-60">
{versions.map((v) => (
<SelectItem key={v} value={v} className="text-zinc-300 focus:bg-zinc-700 focus:text-white">
{v}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center gap-3 p-3 rounded-lg bg-amber-500/10 border border-amber-500/30 text-amber-400 text-sm">
<span></span>
<p>Changing server version requires a server restart. Always backup first!</p>
</div>
<Button
onClick={() => save({ serverType: selectedType, serverVersion: settings.serverVersion })}
disabled={saving || !settings.serverVersion}
className="bg-emerald-600 hover:bg-emerald-500 text-white"
size="sm"
>
{saving ? "Saving..." : "Apply Version"}
</Button>
</CardContent>
</Card>
</TabsContent>
{/* Integrations */}
<TabsContent value="integrations" className="space-y-4">
<Card className="bg-zinc-900 border-zinc-800">
<CardHeader>
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
<Map className="w-4 h-4 text-emerald-500" />
BlueMap Integration
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800 border border-zinc-700">
<div>
<p className="text-sm font-medium text-zinc-300">Enable BlueMap</p>
<p className="text-xs text-zinc-500 mt-0.5">Show the 3D map in the Map section</p>
</div>
<Switch
checked={settings.bluemapEnabled ?? false}
onCheckedChange={(v) => save({ bluemapEnabled: v })}
/>
</div>
{settings.bluemapEnabled && (
<div className="space-y-1.5">
<Label className="text-zinc-300">BlueMap URL</Label>
<Input
value={settings.bluemapUrl ?? ""}
onChange={(e) => setSettings(p => ({ ...p, bluemapUrl: e.target.value }))}
placeholder="http://localhost:8100"
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 font-mono text-sm"
/>
<p className="text-xs text-zinc-500">
The URL where BlueMap is accessible from your browser.
</p>
</div>
)}
<Button
onClick={() => save({ bluemapEnabled: settings.bluemapEnabled, bluemapUrl: settings.bluemapUrl })}
disabled={saving}
className="bg-emerald-600 hover:bg-emerald-500 text-white"
size="sm"
>
{saving ? "Saving..." : "Save"}
</Button>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}