"use client"; import { useEffect, useState, type ReactNode } from "react"; import Link from "next/link"; import { Globe2, Plus, Network, Server, Play, Square, RotateCw, Terminal, Archive, Settings2, Copy, ArrowRight, Layers, Check, FolderOpen, RefreshCw, } from "lucide-react"; import { api, useApi, toast } from "@/lib/api"; import { Badge, Button, Card, EmptyState, Input, Modal, Select, Tabs, Toggle, } from "@/components/ui"; import { defaultProfile, LOADERS, proxyCompatible, type WorldProfile, type NetworkView, type ProxySettings, } from "@/lib/network"; type Draft = Omit; type Action = { op: string; id?: string; name: string; backup?: string }; const fieldStyle = "flex flex-col gap-1.5 text-xs text-ink2"; export default function NetworkPage() { const { data, error, refresh, loading } = useApi( "/api/network", 5000, ); const [editor, setEditor] = useState<{ draft: Draft; id?: string } | null>( null, ); const [selected, setSelected] = useState(null); const [action, setAction] = useState(null); const [confirmation, setConfirmation] = useState(""); const [pending, setPending] = useState(false); const [proxy, setProxy] = useState(null); const [proxyDirty, setProxyDirty] = useState(false); useEffect(() => { if (data && !proxyDirty) setProxy(data.proxy); }, [data, proxyDirty]); const locked = !data || data.managed || data.busy || pending; async function saveProfile(draft: Draft, id?: string) { setPending(true); try { await api("/api/network", { body: { op: "save", profile: draft, id } }); setEditor(null); toast("Profile saved. Deploy when ready.", "success"); await refresh(); } catch (e) { toast((e as Error).message, "error"); } finally { setPending(false); } } async function act() { if (!action) return; setPending(true); try { await api("/api/network", { body: { ...action, confirm: confirmation } }); setAction(null); setConfirmation(""); await refresh(); toast("Operation queued; progress appears below"); } catch (e) { toast((e as Error).message, "error"); } finally { setPending(false); } } const ask = (next: Action) => { setConfirmation(""); setAction(next); }; const running = data?.profiles.filter((p) => data.states[p.id]?.running).length ?? 0; const routes = data?.profiles.filter((p) => p.route === "proxy") ?? []; return (
CraftDeck / Infrastructure

Your worlds. One network.

Run independent servers, build a lobby, and give every world room to grow.

{error && (
{error}
)} {data?.managed && (
This installation is managed by Komodo. Deploy network changes through homelab-infra; the panel keeps these controls read-only.
)}
{[ { label: "WORLD SERVERS", value: `${running} / ${data?.profiles.length ?? 0}`, hint: "running / configured", icon: Layers, }, { label: "PUBLIC ENTRY", value: `:${data?.proxy.port ?? "—"}`, hint: data?.states.proxy?.running ? "Velocity is running" : "Velocity is not running", icon: Network, }, { label: "ISOLATED BY DESIGN", value: `${data?.profiles.reduce((n, p) => n + p.memory, 0) ?? 0} GiB`, hint: "configured Java heap across profiles", icon: Server, }, ].map((s) => (
{s.label}
{s.value}
{s.hint}
))}
Network gateway } actions={ {data?.states.proxy?.health ?? data?.states.proxy?.status ?? "Loading"} } >
Players Velocity :{proxy?.port}
{routes.length ? ( routes.map((p) => (
{p.name} {p.id === proxy?.lobby && ( Lobby )} {p.loader}
)) ) : (

Create a Paper or Folia server to add your first route.

)}

Players join the lobby, then use{" "} /server <server-id>. Hostname routes can send them straight to a world. Each server has separate inventories unless you install a compatible sync plugin.

{proxy && (

The primary server keeps port {data?.primaryPort ?? 25565}. Choose a free gateway port or forward your router’s public 25565 to this gateway. Applying routing reconnects players.

)}

World servers{" "} {data?.profiles.length ?? 0}

Open live maps →
{loading &&

Loading your network…

} {data && !data.profiles.length && ( } title="A new world starts here" hint="Create a lobby, a survival world, or a modded adventure. Profiles are saved first; you decide when to start them." />
)}
{data?.profiles.map((p) => { const s = data.states[p.id]; return (

{p.name}

{p.id}

{s?.health ?? s?.status ?? "Unknown"}
{p.loader} {p.version} {p.memory} GiB {p.route === "proxy" ? "Proxy route" : `Direct :${p.port}`} {p.map !== "none" && {p.map}}
World seed
{p.seed || "Random on first start"}
Game mode
{p.mode} · {p.difficulty}
Backups
Every {p.backupHours}h · {p.retentionDays} days
Resource pack
{p.resourcePack ? p.resourcePackRequired ? "Required download" : "Optional download" : "None"}
{s?.error && (

Container runtime unavailable; status could not be checked.

)}
); })}
{!!data?.jobs.length && (
{data.jobs.slice(0, 6).map((j) => (
{j.state === "succeeded" ? ( ) : ( )}
{j.action} {data.profiles.find((p) => p.id === j.profileId)?.name ?? "Gateway"} {j.state}

{j.message}

))}
)}
Runtime: {data?.runtime === "apple" ? "Apple container" : "Docker"}. New network profiles use verified Minecraft accounts. The primary server retains its existing authentication. Explore server capabilities →
!pending && setEditor(null)} title={editor?.id ? "Edit server profile" : "Create a world server"} wide > {editor && ( saveProfile(draft, editor.id)} /> )} !pending && setAction(null)} title={ action?.op === "proxy-apply" ? "Apply gateway routing" : `${action?.op ?? ""} · ${action?.name ?? ""}` } >

{action?.op === "proxy-apply" || action?.op === "proxy-stop" ? "Changing the gateway disconnects all players using it. World servers keep running." : action?.op === "restore" ? "Restore replaces this server’s active files and leaves it stopped. Previous files are retained for recovery." : action?.op === "deploy" ? "Deploy downloads the selected runtime and starts this server. Existing worlds receive a verified backup before changes. By starting a Minecraft server you agree to the Minecraft EULA." : "This operation may briefly disconnect players on the affected server. Other world servers keep running."}

{data?.runtime === "apple" && (

On this Mac, world operations refresh changing container addresses and may restart the gateway. All connected players will need to reconnect. If no routed world is ready, the gateway stops; apply routing again after starting a world.

)}
setSelected(null)} title={selected?.name ?? "Server"} wide > {selected && ( { setSelected(null); ask(a); }} /> )}
); } function Group({ title, children, open = false, }: { title: string; children: ReactNode; open?: boolean; }) { return (
{title}
{children}
); } function ProfileForm({ draft, busy, onSave, }: { draft: Draft; busy: boolean; onSave: (p: Draft) => void; }) { const [p, setP] = useState(draft); function field( key: keyof Draft, label: string, type = "text", hint?: string, ) { return ( ); } function select(key: keyof Draft, label: string, values: readonly string[]) { return ( {field( "version", "Exact Minecraft version", "text", "Choose a version supported by the runtime, mods, and map.", )} {select("java", "Java runtime", ["8", "17", "21", "25"])} {field("loaderVersion", "Loader version / Paper build (optional)")} {field( "memory", "Java heap (GiB)", "number", "Each server also reserves 1 GiB container headroom.", )} {p.loader === "FOLIA" && (

Folia ticks independent regions in parallel. Use only plugins explicitly supporting Folia. CPU contention, dense player clusters, and disk limits can still cause lag.

)} {field( "seed", "World seed", "text", "Blank generates a random seed. New profiles have separate terrain and player data.", )} {select("mode", "Game mode", [ "survival", "creative", "adventure", "spectator", ])} {select("difficulty", "Difficulty", [ "peaceful", "easy", "normal", "hard", ])} {field("maxPlayers", "Player slots", "number")} {field("viewDistance", "View distance (chunks)", "number")} {field( "simulationDistance", "Simulation distance (chunks)", "number", )} setP({ ...p, pvp: v })} label="Player versus player" /> setP({ ...p, hardcore: v })} label="Hardcore" />