363 lines
15 KiB
TypeScript
363 lines
15 KiB
TypeScript
"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'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>
|
||
);
|
||
}
|