Files
Minikura/apps/web/components/server-form.tsx

1429 lines
52 KiB
TypeScript
Raw Permalink Normal View History

2026-02-13 15:52:13 +07:00
"use client";
import type { GameMode, ServiceType as PrismaServiceType, ServerDifficulty } from "@minikura/db";
import { Info, Plus, Trash2 } from "lucide-react";
import { useState } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
export type ServerType = "VANILLA" | "PAPER" | "SPIGOT" | "PURPUR" | "FABRIC" | "CUSTOM";
export type ServiceType = PrismaServiceType;
export type Difficulty = ServerDifficulty;
export type Mode = GameMode;
export interface EnvVar {
id?: string;
key: string;
value: string;
}
export interface ServerFormData {
// Basic Configuration
id: string;
description: string;
memoryLimit: string;
memoryRequest: string;
cpuRequest: string;
cpuLimit: string;
// Server Type & Version
type: ServerType;
version?: string;
customJarUrl?: string;
eula: boolean;
// Network Configuration
listenPort: string;
serviceType: ServiceType;
nodePort?: string;
// Server Properties
motd?: string;
difficulty: "peaceful" | "easy" | "normal" | "hard";
mode: "survival" | "creative" | "adventure" | "spectator";
maxPlayers: string;
pvp: boolean;
onlineMode: boolean;
allowFlight: boolean;
enableCommandBlock: boolean;
spawnProtection: string;
viewDistance: string;
simulationDistance: string;
// World Configuration
levelName: string;
levelSeed?: string;
levelType?: string;
generatorSettings?: string;
hardcore: boolean;
spawnAnimals: boolean;
spawnMonsters: boolean;
spawnNpcs: boolean;
// Player Management
enableWhitelist: boolean;
whitelist?: string;
whitelistFile?: string;
ops?: string;
opsFile?: string;
// JVM & Performance
useAikarFlags: boolean;
useMeowiceFlags: boolean;
jvmOpts?: string;
jvmXxOpts?: string;
jvmDdOpts?: string;
// Resource Pack
resourcePack?: string;
resourcePackSha1?: string;
resourcePackEnforce: boolean;
// RCON Configuration
enableRcon: boolean;
rconPassword?: string;
rconPort: string;
rconCmdsStartup?: string;
rconCmdsOnConnect?: string;
rconCmdsFirstConnect?: string;
rconCmdsOnDisconnect?: string;
rconCmdsLastDisconnect?: string;
// Query Protocol
enableQuery: boolean;
queryPort: string;
// Auto-Pause
enableAutopause: boolean;
autopauseTimeoutEst: string;
autopauseTimeoutInit: string;
autopauseTimeoutKn: string;
autopausePeriod: string;
autopauseKnockInterface: string;
// Auto-Stop
enableAutostop: boolean;
autostopTimeoutEst: string;
autostopTimeoutInit: string;
autostopPeriod: string;
// Mods & Plugins
plugins?: string;
removeOldPlugins: boolean;
spigetResources?: string;
// Type-Specific Options
paperBuild?: string;
// Advanced
timezone: string;
uid: string;
gid: string;
enableJmx: boolean;
stopDuration: string;
serverIcon?: string;
// Environment Variables
envVars: EnvVar[];
}
const toMode = (value: string): ServerFormData["mode"] => {
if (value === "creative" || value === "adventure" || value === "spectator") {
return value;
}
return "survival";
};
const toDifficulty = (value: string): ServerFormData["difficulty"] => {
if (value === "peaceful" || value === "normal" || value === "hard") {
return value;
}
return "easy";
};
interface ServerFormProps {
initialData?: Partial<ServerFormData>;
onSubmit: (data: ServerFormData) => Promise<void>;
onCancel: () => void;
submitLabel?: string;
loading?: boolean;
}
const InfoTooltip = ({ text }: { text: string }) => (
<div className="inline-flex items-center group relative ml-1">
<Info className="h-4 w-4 text-muted-foreground" />
<span className="absolute left-full ml-2 w-64 bg-popover text-popover-foreground text-xs rounded p-2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-opacity z-50 shadow-lg border">
{text}
</span>
</div>
);
export function ServerForm({
initialData,
onSubmit,
onCancel,
submitLabel = "Create Server",
loading = false,
}: ServerFormProps) {
const defaultType = initialData?.type || "PAPER";
const defaultVersion = defaultType === "CUSTOM" ? "" : initialData?.version || "LATEST";
const [formData, setFormData] = useState<ServerFormData>({
id: initialData?.id || "",
description: initialData?.description || "",
memoryLimit: initialData?.memoryLimit || "2048",
memoryRequest: initialData?.memoryRequest || "1024",
cpuRequest: initialData?.cpuRequest || "500m",
cpuLimit: initialData?.cpuLimit || "2",
type: defaultType,
version: defaultVersion,
eula: initialData?.eula ?? true,
listenPort: initialData?.listenPort || "25565",
serviceType: initialData?.serviceType || "CLUSTER_IP",
difficulty: initialData?.difficulty || "easy",
mode: initialData?.mode || "survival",
maxPlayers: initialData?.maxPlayers || "20",
pvp: initialData?.pvp ?? true,
onlineMode: initialData?.onlineMode ?? true,
allowFlight: initialData?.allowFlight ?? false,
enableCommandBlock: initialData?.enableCommandBlock ?? false,
spawnProtection: initialData?.spawnProtection || "16",
viewDistance: initialData?.viewDistance || "10",
simulationDistance: initialData?.simulationDistance || "10",
levelName: initialData?.levelName || "world",
hardcore: initialData?.hardcore ?? false,
spawnAnimals: initialData?.spawnAnimals ?? true,
spawnMonsters: initialData?.spawnMonsters ?? true,
spawnNpcs: initialData?.spawnNpcs ?? true,
enableWhitelist: initialData?.enableWhitelist ?? false,
useAikarFlags: initialData?.useAikarFlags ?? false,
useMeowiceFlags: initialData?.useMeowiceFlags ?? false,
resourcePackEnforce: initialData?.resourcePackEnforce ?? false,
enableRcon: initialData?.enableRcon ?? true,
rconPort: initialData?.rconPort || "25575",
enableQuery: initialData?.enableQuery ?? false,
queryPort: initialData?.queryPort || "25565",
enableAutopause: initialData?.enableAutopause ?? false,
autopauseTimeoutEst: initialData?.autopauseTimeoutEst || "3600",
autopauseTimeoutInit: initialData?.autopauseTimeoutInit || "600",
autopauseTimeoutKn: initialData?.autopauseTimeoutKn || "120",
autopausePeriod: initialData?.autopausePeriod || "10",
autopauseKnockInterface: initialData?.autopauseKnockInterface || "eth0",
enableAutostop: initialData?.enableAutostop ?? false,
autostopTimeoutEst: initialData?.autostopTimeoutEst || "3600",
autostopTimeoutInit: initialData?.autostopTimeoutInit || "1800",
autostopPeriod: initialData?.autostopPeriod || "10",
removeOldPlugins: initialData?.removeOldPlugins ?? false,
timezone: initialData?.timezone || "UTC",
uid: initialData?.uid || "1000",
gid: initialData?.gid || "1000",
enableJmx: initialData?.enableJmx ?? false,
stopDuration: initialData?.stopDuration || "60",
customJarUrl: initialData?.customJarUrl || "",
envVars: (initialData?.envVars || []).map((envVar) => ({
id: envVar.id || crypto.randomUUID(),
key: envVar.key,
value: envVar.value,
})),
});
const [error, setError] = useState<string | null>(null);
const updateField = <K extends keyof ServerFormData>(key: K, value: ServerFormData[K]) => {
setFormData((prev) => ({ ...prev, [key]: value }));
};
const addEnvVar = () => {
setFormData((prev) => ({
...prev,
envVars: [...prev.envVars, { id: crypto.randomUUID(), key: "", value: "" }],
}));
};
const removeEnvVar = (index: number) => {
setFormData((prev) => ({
...prev,
envVars: prev.envVars.filter((_, i) => i !== index),
}));
};
const updateEnvVar = (index: number, field: "key" | "value", value: string) => {
setFormData((prev) => {
const updated = [...prev.envVars];
updated[index] = { ...updated[index], [field]: value };
return { ...prev, envVars: updated };
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
// Basic validation
if (!formData.id.trim()) {
setError("Server ID is required");
return;
}
if (!/^[a-zA-Z0-9-_]+$/.test(formData.id)) {
setError("ID must be alphanumeric with - or _");
return;
}
if (!formData.eula) {
setError("You must accept the Minecraft EULA to create a server");
return;
}
if (formData.type === "CUSTOM" && !formData.customJarUrl?.trim()) {
setError("Custom jar URL is required for custom servers");
return;
}
try {
await onSubmit(formData);
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-7 lg:grid-cols-10">
<TabsTrigger value="basic">Basic</TabsTrigger>
<TabsTrigger value="server">Server</TabsTrigger>
<TabsTrigger value="world">World</TabsTrigger>
<TabsTrigger value="players">Players</TabsTrigger>
<TabsTrigger value="performance">Performance</TabsTrigger>
<TabsTrigger value="mods">Mods/Plugins</TabsTrigger>
<TabsTrigger value="resources">Resources</TabsTrigger>
<TabsTrigger value="automation">Automation</TabsTrigger>
<TabsTrigger value="network">Network</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
{/* Basic Configuration */}
<TabsContent value="basic" className="space-y-4 mt-4">
<div className="space-y-2">
<Label htmlFor="id">Server ID *</Label>
<Input
id="id"
value={formData.id}
onChange={(e) => updateField("id", e.target.value)}
placeholder="my-server"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => updateField("description", e.target.value)}
placeholder="A brief description of this server"
rows={2}
/>
</div>
<div className="grid grid-cols-4 gap-4">
<div className="space-y-2">
<Label htmlFor="memoryRequest">
Memory Request (MB) *
<InfoTooltip text="Kubernetes guaranteed memory. Java heap sizes from the limit." />
</Label>
<Input
id="memoryRequest"
type="number"
value={formData.memoryRequest}
onChange={(e) => updateField("memoryRequest", e.target.value)}
min="256"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="memoryLimit">
Memory Limit (MB) *
<InfoTooltip text="Kubernetes max memory. Java heap scales from this." />
</Label>
<Input
id="memoryLimit"
type="number"
value={formData.memoryLimit}
onChange={(e) => updateField("memoryLimit", e.target.value)}
min="256"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="cpuRequest">
CPU Request *
<InfoTooltip text="Guaranteed CPU allocation (e.g., 500m = 0.5 cores)" />
</Label>
<Input
id="cpuRequest"
value={formData.cpuRequest}
onChange={(e) => updateField("cpuRequest", e.target.value)}
placeholder="500m"
/>
</div>
<div className="space-y-2">
<Label htmlFor="cpuLimit">
CPU Limit *
<InfoTooltip text="Maximum CPU the server can use" />
</Label>
<Input
id="cpuLimit"
value={formData.cpuLimit}
onChange={(e) => updateField("cpuLimit", e.target.value)}
placeholder="2"
/>
</div>
</div>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="advanced-memory">
<AccordionTrigger>Java Heap Overrides</AccordionTrigger>
<AccordionContent className="space-y-2 text-sm text-muted-foreground">
Heap sizes are derived from the memory limit. Override only if needed.
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="flex items-center space-x-2">
<Checkbox
id="eula"
checked={formData.eula}
onCheckedChange={(checked) => updateField("eula", checked as boolean)}
/>
<Label htmlFor="eula" className="cursor-pointer">
I accept the{" "}
<a
href="https://www.minecraft.net/eula"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
>
Minecraft EULA
</a>{" "}
*
</Label>
</div>
</TabsContent>
{/* Server Type & Configuration */}
<TabsContent value="server" className="space-y-4 mt-4">
{formData.type === "CUSTOM" && (
<div className="rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
Custom jars may ignore built-in server settings. Configure extra values via custom
environment variables if needed.
</div>
)}
<div className="space-y-2">
<Label htmlFor="type">
Server Type *
<InfoTooltip text="Choose the server software (Vanilla, Paper, or Custom Jar)." />
</Label>
<Select
value={formData.type}
onValueChange={(value) => updateField("type", value as ServerType)}
>
<SelectTrigger id="type">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="VANILLA">Vanilla (Official Mojang)</SelectItem>
<SelectItem value="PAPER">Paper (Recommended - High Performance)</SelectItem>
<SelectItem value="CUSTOM">Custom Jar</SelectItem>
</SelectContent>
</Select>
</div>
{formData.type !== "CUSTOM" && (
<div className="space-y-2">
<Label htmlFor="version">
Minecraft Version
<InfoTooltip text="Use LATEST for newest stable, SNAPSHOT for snapshots, or specific version like 1.20.4" />
</Label>
<Input
id="version"
value={formData.version || ""}
onChange={(e) => updateField("version", e.target.value)}
placeholder="LATEST"
/>
</div>
)}
{/* Type-specific options */}
{formData.type === "PAPER" && (
<div className="space-y-2">
<Label htmlFor="paperBuild">Paper Build Number</Label>
<Input
id="paperBuild"
value={formData.paperBuild || ""}
onChange={(e) => updateField("paperBuild", e.target.value)}
placeholder="Leave empty for latest"
/>
</div>
)}
{formData.type === "CUSTOM" && (
<div className="space-y-2">
<Label htmlFor="customJarUrl">Custom Server Jar URL/Path</Label>
<Input
id="customJarUrl"
value={formData.customJarUrl || ""}
onChange={(e) => updateField("customJarUrl", e.target.value)}
placeholder="https://example.com/server.jar"
/>
<p className="text-sm text-muted-foreground">
Sets CUSTOM_SERVER for the itzg/minecraft-server image.
</p>
</div>
)}
<div className="space-y-2">
<Label htmlFor="motd">Message of the Day (MOTD)</Label>
<Textarea
id="motd"
value={formData.motd || ""}
onChange={(e) => updateField("motd", e.target.value)}
placeholder="A Minecraft Server"
rows={2}
/>
<p className="text-sm text-muted-foreground">
Supports formatting codes like §l for bold, §c for red
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="mode">Game Mode</Label>
<Select
value={formData.mode}
onValueChange={(value) => updateField("mode", toMode(value))}
>
<SelectTrigger id="mode">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="survival">Survival</SelectItem>
<SelectItem value="creative">Creative</SelectItem>
<SelectItem value="adventure">Adventure</SelectItem>
<SelectItem value="spectator">Spectator</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="difficulty">Difficulty</Label>
<Select
value={formData.difficulty}
onValueChange={(value) => updateField("difficulty", toDifficulty(value))}
>
<SelectTrigger id="difficulty">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="peaceful">Peaceful</SelectItem>
<SelectItem value="easy">Easy</SelectItem>
<SelectItem value="normal">Normal</SelectItem>
<SelectItem value="hard">Hard</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="maxPlayers">Max Players</Label>
<Input
id="maxPlayers"
type="number"
value={formData.maxPlayers}
onChange={(e) => updateField("maxPlayers", e.target.value)}
min="1"
max="1000"
/>
</div>
<div className="space-y-2">
<Label htmlFor="viewDistance">View Distance (chunks)</Label>
<Input
id="viewDistance"
type="number"
value={formData.viewDistance}
onChange={(e) => updateField("viewDistance", e.target.value)}
min="3"
max="32"
/>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="pvp"
checked={formData.pvp}
onCheckedChange={(checked) => updateField("pvp", checked as boolean)}
/>
<Label htmlFor="pvp">Enable PvP (Player vs Player)</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="onlineMode"
checked={formData.onlineMode}
onCheckedChange={(checked) => updateField("onlineMode", checked as boolean)}
/>
<Label htmlFor="onlineMode">
Online Mode (Requires authenticated Minecraft accounts)
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="allowFlight"
checked={formData.allowFlight}
onCheckedChange={(checked) => updateField("allowFlight", checked as boolean)}
/>
<Label htmlFor="allowFlight">Allow Flight</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="enableCommandBlock"
checked={formData.enableCommandBlock}
onCheckedChange={(checked) => updateField("enableCommandBlock", checked as boolean)}
/>
<Label htmlFor="enableCommandBlock">Enable Command Blocks</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="hardcore"
checked={formData.hardcore}
onCheckedChange={(checked) => updateField("hardcore", checked as boolean)}
/>
<Label htmlFor="hardcore">Hardcore Mode (Permanent Death)</Label>
</div>
</div>
</TabsContent>
{/* World Configuration */}
<TabsContent value="world" className="space-y-4 mt-4">
{formData.type === "CUSTOM" && (
<div className="rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
World options apply to standard server properties and may be ignored by custom jars.
</div>
)}
<div className="space-y-2">
<Label htmlFor="levelName">World Name</Label>
<Input
id="levelName"
value={formData.levelName}
onChange={(e) => updateField("levelName", e.target.value)}
placeholder="world"
/>
</div>
<div className="space-y-2">
<Label htmlFor="levelSeed">
World Seed
<InfoTooltip text="Leave empty for random generation. Can be numeric or text." />
</Label>
<Input
id="levelSeed"
value={formData.levelSeed || ""}
onChange={(e) => updateField("levelSeed", e.target.value)}
placeholder="Leave empty for random"
/>
</div>
<div className="space-y-2">
<Label htmlFor="levelType">Level Type</Label>
<Select
value={formData.levelType || "default"}
onValueChange={(value) => updateField("levelType", value === "default" ? "" : value)}
>
<SelectTrigger id="levelType">
<SelectValue placeholder="Default" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="flat">Flat/Superflat</SelectItem>
<SelectItem value="largeBiomes">Large Biomes</SelectItem>
<SelectItem value="amplified">Amplified</SelectItem>
<SelectItem value="buffet">Buffet (Single Biome)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="generatorSettings">
Generator Settings
<InfoTooltip text="JSON configuration for custom world generation. Advanced users only." />
</Label>
<Textarea
id="generatorSettings"
value={formData.generatorSettings || ""}
onChange={(e) => updateField("generatorSettings", e.target.value)}
placeholder='{"structures": {...}, "layers": [...]}'
rows={3}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="spawnProtection">
Spawn Protection (blocks)
<InfoTooltip text="Radius around spawn where only ops can build. Set to 0 to disable." />
</Label>
<Input
id="spawnProtection"
type="number"
value={formData.spawnProtection}
onChange={(e) => updateField("spawnProtection", e.target.value)}
min="0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="simulationDistance">
Simulation Distance
<InfoTooltip text="Distance in chunks where game logic runs (redstone, crops, etc.)" />
</Label>
<Input
id="simulationDistance"
type="number"
value={formData.simulationDistance}
onChange={(e) => updateField("simulationDistance", e.target.value)}
min="3"
max="32"
/>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="spawnAnimals"
checked={formData.spawnAnimals}
onCheckedChange={(checked) => updateField("spawnAnimals", checked as boolean)}
/>
<Label htmlFor="spawnAnimals">Spawn Animals</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="spawnMonsters"
checked={formData.spawnMonsters}
onCheckedChange={(checked) => updateField("spawnMonsters", checked as boolean)}
/>
<Label htmlFor="spawnMonsters">Spawn Monsters</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="spawnNpcs"
checked={formData.spawnNpcs}
onCheckedChange={(checked) => updateField("spawnNpcs", checked as boolean)}
/>
<Label htmlFor="spawnNpcs">Spawn NPCs (Villagers)</Label>
</div>
</div>
</TabsContent>
{/* Player Management */}
<TabsContent value="players" className="space-y-4 mt-4">
{formData.type === "CUSTOM" && (
<div className="rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
Some custom jars do not support managed whitelist/ops settings.
</div>
)}
<div className="flex items-center space-x-2">
<Checkbox
id="enableWhitelist"
checked={formData.enableWhitelist}
onCheckedChange={(checked) => updateField("enableWhitelist", checked as boolean)}
/>
<Label htmlFor="enableWhitelist">
Enable Whitelist
<InfoTooltip text="Only whitelisted players can join the server" />
</Label>
</div>
{formData.enableWhitelist && (
<>
<div className="space-y-2">
<Label htmlFor="whitelist">
Whitelisted Players
<InfoTooltip text="Comma-separated list of usernames or UUIDs" />
</Label>
<Textarea
id="whitelist"
value={formData.whitelist || ""}
onChange={(e) => updateField("whitelist", e.target.value)}
placeholder="Player1,Player2,UUID-here"
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="whitelistFile">
Whitelist File URL
<InfoTooltip text="URL or path to a whitelist.json file. Overrides the whitelist field." />
</Label>
<Input
id="whitelistFile"
value={formData.whitelistFile || ""}
onChange={(e) => updateField("whitelistFile", e.target.value)}
placeholder="https://example.com/whitelist.json"
/>
</div>
</>
)}
<div className="space-y-2">
<Label htmlFor="ops">
Operators (Ops)
<InfoTooltip text="Comma-separated list of usernames or UUIDs with full server permissions" />
</Label>
<Textarea
id="ops"
value={formData.ops || ""}
onChange={(e) => updateField("ops", e.target.value)}
placeholder="Admin1,Admin2"
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="opsFile">Ops File URL</Label>
<Input
id="opsFile"
value={formData.opsFile || ""}
onChange={(e) => updateField("opsFile", e.target.value)}
placeholder="https://example.com/ops.json"
/>
</div>
</TabsContent>
{/* Performance & JVM */}
<TabsContent value="performance" className="space-y-4 mt-4">
{formData.type === "CUSTOM" && (
<div className="rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
Custom jars may not honor built-in tuning flags. Use custom env vars if required.
</div>
)}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="useAikarFlags"
checked={formData.useAikarFlags}
onCheckedChange={(checked) => updateField("useAikarFlags", checked as boolean)}
/>
<Label htmlFor="useAikarFlags">
Use Aikar's Flags
<InfoTooltip text="Optimized G1GC garbage collection flags. Recommended for servers with 1GB+ RAM." />
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="useMeowiceFlags"
checked={formData.useMeowiceFlags}
onCheckedChange={(checked) => updateField("useMeowiceFlags", checked as boolean)}
/>
<Label htmlFor="useMeowiceFlags">
Use Meowice Flags
<InfoTooltip text="Modern optimization flags for Java 17+. Based on Aikar's flags with improvements." />
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="enableJmx"
checked={formData.enableJmx}
onCheckedChange={(checked) => updateField("enableJmx", checked as boolean)}
/>
<Label htmlFor="enableJmx">
Enable JMX
<InfoTooltip text="Enables remote JMX monitoring for advanced profiling" />
</Label>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="jvmOpts">
Custom JVM Options
<InfoTooltip text="Space-separated JVM arguments (e.g., -Xms2G -Xmx4G)" />
</Label>
<Textarea
id="jvmOpts"
value={formData.jvmOpts || ""}
onChange={(e) => updateField("jvmOpts", e.target.value)}
placeholder="-XX:+UseG1GC -XX:MaxGCPauseMillis=200"
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="jvmXxOpts">
JVM -XX Options
<InfoTooltip text="Space-separated -XX JVM flags for advanced tuning" />
</Label>
<Textarea
id="jvmXxOpts"
value={formData.jvmXxOpts || ""}
onChange={(e) => updateField("jvmXxOpts", e.target.value)}
placeholder="+UnlockExperimentalVMOptions +UseZGC"
rows={2}
/>
</div>
<div className="space-y-2">
<Label htmlFor="jvmDdOpts">
JVM -D System Properties
<InfoTooltip text="Comma-separated key=value pairs for system properties" />
</Label>
<Textarea
id="jvmDdOpts"
value={formData.jvmDdOpts || ""}
onChange={(e) => updateField("jvmDdOpts", e.target.value)}
placeholder="property1=value1,property2=value2"
rows={2}
/>
</div>
</TabsContent>
{/* Mods & Plugins */}
<TabsContent value="mods" className="space-y-4 mt-4">
{formData.type === "CUSTOM" && (
<div className="rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
Mods/plugins automation is intended for Vanilla/Paper workflows.
</div>
)}
<div className="space-y-2">
<Label htmlFor="plugins">
Plugins
<InfoTooltip text="Comma-separated list of plugin URLs or filenames" />
</Label>
<Textarea
id="plugins"
value={formData.plugins || ""}
onChange={(e) => updateField("plugins", e.target.value)}
placeholder="https://example.com/plugin1.jar,plugin2.jar"
rows={3}
/>
<p className="text-sm text-muted-foreground">
For Paper/Spigot/Bukkit servers. URLs or local filenames.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="spigetResources">
Spiget Resource IDs
<InfoTooltip text="Comma-separated Spiget resource IDs to auto-download from SpigotMC" />
</Label>
<Input
id="spigetResources"
value={formData.spigetResources || ""}
onChange={(e) => updateField("spigetResources", e.target.value)}
placeholder="1234,5678,9012"
/>
</div>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="removeOldPlugins"
checked={formData.removeOldPlugins}
onCheckedChange={(checked) => updateField("removeOldPlugins", checked as boolean)}
/>
<Label htmlFor="removeOldPlugins">
Remove Old Plugins
<InfoTooltip text="Automatically remove old versions of plugins when updating" />
</Label>
</div>
</div>
</TabsContent>
{/* Resources (Resource Pack, Icon) */}
<TabsContent value="resources" className="space-y-4 mt-4">
{formData.type === "CUSTOM" && (
<div className="rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
Resource pack settings may not apply to custom jars.
</div>
)}
<div className="space-y-2">
<Label htmlFor="resourcePack">
Resource Pack URL
<InfoTooltip text="URL or path to a resource pack ZIP file" />
</Label>
<Input
id="resourcePack"
value={formData.resourcePack || ""}
onChange={(e) => updateField("resourcePack", e.target.value)}
placeholder="https://example.com/resourcepack.zip"
/>
</div>
<div className="space-y-2">
<Label htmlFor="resourcePackSha1">
Resource Pack SHA1
<InfoTooltip text="SHA1 checksum of the resource pack for verification" />
</Label>
<Input
id="resourcePackSha1"
value={formData.resourcePackSha1 || ""}
onChange={(e) => updateField("resourcePackSha1", e.target.value)}
placeholder="a1b2c3d4e5f6..."
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="resourcePackEnforce"
checked={formData.resourcePackEnforce}
onCheckedChange={(checked) => updateField("resourcePackEnforce", checked as boolean)}
/>
<Label htmlFor="resourcePackEnforce">
Enforce Resource Pack
<InfoTooltip text="Require players to accept the resource pack to join" />
</Label>
</div>
<div className="space-y-2">
<Label htmlFor="serverIcon">
Server Icon URL
<InfoTooltip text="URL or path to a server icon image (PNG, 64x64 recommended)" />
</Label>
<Input
id="serverIcon"
value={formData.serverIcon || ""}
onChange={(e) => updateField("serverIcon", e.target.value)}
placeholder="https://example.com/icon.png"
/>
</div>
</TabsContent>
{/* Automation (Auto-Pause, Auto-Stop, RCON) */}
<TabsContent value="automation" className="space-y-4 mt-4">
{formData.type === "CUSTOM" && (
<div className="rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
Automation hooks depend on container scripts; custom jars may ignore them.
</div>
)}
<Accordion type="multiple" className="w-full">
<AccordionItem value="rcon">
<AccordionTrigger>RCON Configuration</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="enableRcon"
checked={formData.enableRcon}
onCheckedChange={(checked) => updateField("enableRcon", checked as boolean)}
/>
<Label htmlFor="enableRcon">
Enable RCON
<InfoTooltip text="Remote console for server management. Enabled by default for graceful shutdown." />
</Label>
</div>
{formData.enableRcon && (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="rconPassword">RCON Password</Label>
<Input
id="rconPassword"
type="password"
value={formData.rconPassword || ""}
onChange={(e) => updateField("rconPassword", e.target.value)}
placeholder="Auto-generated if empty"
/>
</div>
<div className="space-y-2">
<Label htmlFor="rconPort">RCON Port</Label>
<Input
id="rconPort"
type="number"
value={formData.rconPort}
onChange={(e) => updateField("rconPort", e.target.value)}
min="1"
max="65535"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="rconCmdsStartup">
RCON Commands on Startup
<InfoTooltip text="Semicolon-separated commands to run when server starts" />
</Label>
<Textarea
id="rconCmdsStartup"
value={formData.rconCmdsStartup || ""}
onChange={(e) => updateField("rconCmdsStartup", e.target.value)}
placeholder="say Server starting...;gamerule doDaylightCycle false"
rows={2}
/>
</div>
<div className="space-y-2">
<Label htmlFor="rconCmdsOnConnect">Commands on Player Connect</Label>
<Textarea
id="rconCmdsOnConnect"
value={formData.rconCmdsOnConnect || ""}
onChange={(e) => updateField("rconCmdsOnConnect", e.target.value)}
placeholder="tell {{player}} Welcome to the server!"
rows={2}
/>
</div>
</>
)}
</AccordionContent>
</AccordionItem>
<AccordionItem value="query">
<AccordionTrigger>Query Protocol</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="enableQuery"
checked={formData.enableQuery}
onCheckedChange={(checked) => updateField("enableQuery", checked as boolean)}
/>
<Label htmlFor="enableQuery">
Enable Query Protocol
<InfoTooltip text="Allows external tools to query server status and player list" />
</Label>
</div>
{formData.enableQuery && (
<div className="space-y-2">
<Label htmlFor="queryPort">Query Port</Label>
<Input
id="queryPort"
type="number"
value={formData.queryPort}
onChange={(e) => updateField("queryPort", e.target.value)}
min="1"
max="65535"
/>
</div>
)}
</AccordionContent>
</AccordionItem>
<AccordionItem value="autopause">
<AccordionTrigger>Auto-Pause</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="enableAutopause"
checked={formData.enableAutopause}
onCheckedChange={(checked) =>
updateField("enableAutopause", checked as boolean)
}
/>
<Label htmlFor="enableAutopause">
Enable Auto-Pause
<InfoTooltip text="Automatically pauses the server when no players are online to save resources" />
</Label>
</div>
{formData.enableAutopause && (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="autopauseTimeoutEst">
Timeout After Disconnect (seconds)
</Label>
<Input
id="autopauseTimeoutEst"
type="number"
value={formData.autopauseTimeoutEst}
onChange={(e) => updateField("autopauseTimeoutEst", e.target.value)}
min="0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="autopauseTimeoutInit">
Timeout After Startup (seconds)
</Label>
<Input
id="autopauseTimeoutInit"
type="number"
value={formData.autopauseTimeoutInit}
onChange={(e) => updateField("autopauseTimeoutInit", e.target.value)}
min="0"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="autopauseKnockInterface">Network Interface</Label>
<Input
id="autopauseKnockInterface"
value={formData.autopauseKnockInterface}
onChange={(e) => updateField("autopauseKnockInterface", e.target.value)}
placeholder="eth0"
/>
</div>
</>
)}
</AccordionContent>
</AccordionItem>
<AccordionItem value="autostop">
<AccordionTrigger>Auto-Stop</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<div className="flex items-center space-x-2">
<Checkbox
id="enableAutostop"
checked={formData.enableAutostop}
onCheckedChange={(checked) => updateField("enableAutostop", checked as boolean)}
/>
<Label htmlFor="enableAutostop">
Enable Auto-Stop
<InfoTooltip text="Automatically stops the server when no players are online" />
</Label>
</div>
{formData.enableAutostop && (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="autostopTimeoutEst">
Timeout After Player Departure (seconds)
</Label>
<Input
id="autostopTimeoutEst"
type="number"
value={formData.autostopTimeoutEst}
onChange={(e) => updateField("autostopTimeoutEst", e.target.value)}
min="0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="autostopTimeoutInit">Timeout After Launch (seconds)</Label>
<Input
id="autostopTimeoutInit"
type="number"
value={formData.autostopTimeoutInit}
onChange={(e) => updateField("autostopTimeoutInit", e.target.value)}
min="0"
/>
</div>
</div>
)}
</AccordionContent>
</AccordionItem>
</Accordion>
</TabsContent>
{/* Network Configuration */}
<TabsContent value="network" className="space-y-4 mt-4">
{formData.type === "CUSTOM" && (
<div className="rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
Network settings apply at the container level and still work with custom jars.
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="listenPort">Listen Port</Label>
<Input
id="listenPort"
type="number"
value={formData.listenPort}
onChange={(e) => updateField("listenPort", e.target.value)}
min="1"
max="65535"
/>
</div>
<div className="space-y-2">
<Label htmlFor="serviceType">
Service Type
<InfoTooltip text="How the server is exposed in Kubernetes" />
</Label>
<Select
value={formData.serviceType}
onValueChange={(value) => updateField("serviceType", value as ServiceType)}
>
<SelectTrigger id="serviceType">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="CLUSTER_IP">ClusterIP (Internal Only)</SelectItem>
<SelectItem value="NODE_PORT">NodePort (External Access)</SelectItem>
<SelectItem value="LOAD_BALANCER">LoadBalancer (Cloud/MetalLB)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{formData.serviceType === "NODE_PORT" && (
<div className="space-y-2">
<Label htmlFor="nodePort">
Node Port
<InfoTooltip text="Specific port on the node (30000-32767). Leave empty for auto-assignment." />
</Label>
<Input
id="nodePort"
type="number"
value={formData.nodePort || ""}
onChange={(e) => updateField("nodePort", e.target.value)}
placeholder="30000-32767"
min="30000"
max="32767"
/>
</div>
)}
</TabsContent>
{/* Advanced */}
<TabsContent value="advanced" className="space-y-4 mt-4">
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="timezone">
Timezone
<InfoTooltip text="Timezone for server logs and scheduling (e.g., America/New_York)" />
</Label>
<Input
id="timezone"
value={formData.timezone}
onChange={(e) => updateField("timezone", e.target.value)}
placeholder="UTC"
/>
</div>
<div className="space-y-2">
<Label htmlFor="uid">
User ID (UID)
<InfoTooltip text="Linux user ID for the Minecraft process" />
</Label>
<Input
id="uid"
value={formData.uid}
onChange={(e) => updateField("uid", e.target.value)}
placeholder="1000"
/>
</div>
<div className="space-y-2">
<Label htmlFor="gid">
Group ID (GID)
<InfoTooltip text="Linux group ID for the Minecraft process" />
</Label>
<Input
id="gid"
value={formData.gid}
onChange={(e) => updateField("gid", e.target.value)}
placeholder="1000"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="stopDuration">
Graceful Shutdown Timeout (seconds)
<InfoTooltip text="How long to wait for graceful shutdown before forcing stop" />
</Label>
<Input
id="stopDuration"
type="number"
value={formData.stopDuration}
onChange={(e) => updateField("stopDuration", e.target.value)}
min="1"
/>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label>
Custom Environment Variables
<InfoTooltip text="Additional environment variables to pass to the container" />
</Label>
<Button type="button" variant="outline" size="sm" onClick={addEnvVar}>
<Plus className="h-4 w-4 mr-2" />
Add Variable
</Button>
</div>
{formData.envVars.length > 0 && (
<div className="space-y-3">
{formData.envVars.map((envVar, index) => (
<div key={envVar.id} className="flex gap-2">
<Input
placeholder="KEY"
value={envVar.key}
onChange={(e) => updateEnvVar(index, "key", e.target.value)}
className="flex-1 font-mono"
/>
<Input
placeholder="value"
value={envVar.value}
onChange={(e) => updateEnvVar(index, "value", e.target.value)}
className="flex-1"
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => removeEnvVar(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</div>
</TabsContent>
</Tabs>
{error && (
<div className="bg-destructive/10 text-destructive px-4 py-3 rounded-lg">{error}</div>
)}
<div className="flex gap-3 pt-4 border-t">
<Button type="submit" disabled={loading}>
{loading ? "Saving..." : submitLabel}
</Button>
<Button type="button" variant="outline" onClick={onCancel} disabled={loading}>
Cancel
</Button>
</div>
</form>
);
}