Initial push

This commit is contained in:
2026-03-08 15:49:34 +01:00
parent 8da12bb7d1
commit 47127f276d
101 changed files with 13844 additions and 8 deletions

View File

@@ -0,0 +1,362 @@
"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>
);
}