diff --git a/README.md b/README.md index 6801a1f..7a42111 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ A production-quality, self-hosted Minecraft **Java 26.3** server for friends: - **CraftDeck**, a full web management panel: live console, players & stats, lag diagnostics, performance graphs, mod manager with Modrinth search, backups & restore, file browser/editor, config editor, scheduled tasks, Discord alerts +- **Worlds & Network**: isolated concurrent server profiles, Velocity lobby routing, + Paper/Folia and modded runtimes, live map integrations, resource pack settings, + per-server consoles/files and verified backup/restore — [guide](docs/worlds-and-network.md) - **Automatic backups** with retention, **one-command deployment**, everything in Docker | | | @@ -67,6 +70,9 @@ they're ready. Full details: [docs/auth-flow.md](docs/auth-flow.md). ## Documentation +- [docs/worlds-and-network.md](docs/worlds-and-network.md) — multiple servers, lobby routing, maps and resource packs +- [docs/apple-container.md](docs/apple-container.md) — native Mac tests and x86_64 image builds with Apple’s container CLI + - [docs/auth-flow.md](docs/auth-flow.md) — how the approval/registration system works - [docs/mods-and-modpacks.md](docs/mods-and-modpacks.md) — adding mods, switching to modpacks or other loaders - [docs/updating.md](docs/updating.md) — upgrading Minecraft, mods, and the panel diff --git a/dashboard/.dockerignore b/dashboard/.dockerignore index daf787e..ca90523 100644 --- a/dashboard/.dockerignore +++ b/dashboard/.dockerignore @@ -2,3 +2,5 @@ node_modules .next Dockerfile .dockerignore +dev-data +*.tsbuildinfo diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile index c39e329..0760fab 100644 --- a/dashboard/Dockerfile +++ b/dashboard/Dockerfile @@ -13,7 +13,7 @@ ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build FROM node:26-slim@sha256:ec7758ee051e457b468b32bde57b0879010b325bb9862718e9615225ce4aaae1 AS runtime -ARG TARGETARCH=amd64 +ARG TARGETARCH ARG DOCKER_CLI_VERSION=27.5.1 ARG COMPOSE_VERSION=v2.32.4 diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index dc2aba0..6c88987 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -16,7 +16,8 @@ "next": "16.3.7", "node-cron": "4.6.0", "react": "19.2.7", - "react-dom": "19.2.7" + "react-dom": "19.2.7", + "yaml": "2.9.1" }, "devDependencies": { "@tailwindcss/postcss": "4.3.2", @@ -3111,6 +3112,21 @@ "engines": { "node": ">=20.11" } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } } } } diff --git a/dashboard/package.json b/dashboard/package.json index d319c59..14eabc7 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -19,7 +19,8 @@ "next": "16.3.7", "node-cron": "4.6.0", "react": "19.2.7", - "react-dom": "19.2.7" + "react-dom": "19.2.7", + "yaml": "2.9.1" }, "devDependencies": { "@tailwindcss/postcss": "4.3.2", diff --git a/dashboard/src/app/(panel)/features/page.tsx b/dashboard/src/app/(panel)/features/page.tsx new file mode 100644 index 0000000..298b78a --- /dev/null +++ b/dashboard/src/app/(panel)/features/page.tsx @@ -0,0 +1,262 @@ +import Link from "next/link"; +import { + ArrowUpRight, + Boxes, + Cpu, + Globe2, + Shield, + Sparkles, + Wrench, +} from "lucide-react"; +import { Badge, Card } from "@/components/ui"; +const groups = [ + { + title: "Worlds & networks", + icon: Globe2, + items: [ + [ + "Independent world servers", + "Built in", + "Concurrent profiles with separate seeds, versions, Java runtimes, mods, configs, player data, and backups.", + ], + [ + "Lobby & server switching", + "Built in", + "Velocity gateway, default lobby, fallback routes, and hostname routing for Paper/Folia. Use /server to switch compatible servers.", + ], + [ + "Several worlds inside one server", + "Addon", + "Multiverse-style plugins or modded dimensions. These share one runtime, so use separate profiles for incompatible modpacks.", + ], + [ + "Cross-server inventories & chat", + "Addon", + "A compatible sync plugin and shared database can connect inventories, economies, permissions, and chat. Decide what each game mode should share.", + ], + ], + }, + { + title: "Performance & scale", + icon: Cpu, + items: [ + [ + "Region-based ticking", + "Runtime option", + "Folia runs independent regions in parallel. It needs suitable hardware and Folia-aware plugins; crowded regions can still lag.", + ], + [ + "Fabric / Forge / NeoForge / Quilt", + "Built in", + "Dedicated runtime profiles preserve mod compatibility. Match the Java release and client modpack to the selected server version.", + ], + [ + "Profiling & tuning", + "Primary panel", + "Performance charts and spark diagnostics remain in the primary server panel. Tune view/simulation distances per network profile.", + ], + [ + "Chunk pregeneration", + "Addon", + "Pregenerate a bounded play area with a compatible tool to reduce exploration spikes. Budget disk space and schedule it outside peak hours.", + ], + ], + }, + { + title: "Maps & client experience", + icon: Sparkles, + items: [ + [ + "BlueMap / Dynmap", + "Integration", + "Compatible release resolution, isolated renderer ports, and an embedded viewer for your HTTPS map URL. Rendering runs on the server.", + ], + [ + "Resource pack downloads", + "Built in", + "Per-server ZIP URL, SHA-1, download prompt, and required/optional mode. Textures, models, sounds, and fonts; client mods are separate.", + ], + [ + "Claims, towns & economies", + "Addon", + "Install compatible protection, town, economy, shop, and map-marker plugins. Plugin compatibility depends on the runtime and version.", + ], + [ + "Bedrock crossplay", + "Addon", + "Geyser and optionally Floodgate can bridge supported clients. Configure identity handling and a separate UDP port; this panel does not deploy it automatically.", + ], + ], + }, + { + title: "Safety & operations", + icon: Shield, + items: [ + [ + "Backups & recovery", + "Built in", + "Per-server backup sidecars, retention policies, verified cold snapshots, and staged restores that retain previous data.", + ], + [ + "Access & isolation", + "Built in", + "New profiles require verified accounts and a whitelist. Proxy backends are private and use a forwarding secret. Primary offline authentication remains separate.", + ], + [ + "Console & file management", + "Built in", + "Profile-specific commands, log tails, stopped-server config editing, mod/plugin uploads, and datapack uploads.", + ], + [ + "Remote backups & disaster recovery", + "External setup", + "Replicate archives to another host or object store and test restores. Local backups do not survive loss of the host disk.", + ], + ], + }, + { + title: "Gameplay extensions", + icon: Boxes, + items: [ + [ + "Custom terrain & dimensions", + "Addon", + "World-generation datapacks and mods, structure packs, biomes, and dimensions. Install generation content before exploring new terrain.", + ], + [ + "Minigames & events", + "Addon", + "Create separate arena profiles for PvP, parkour, creative, adventure, or seasonal worlds and route through the gateway.", + ], + [ + "Voice chat & Discord", + "Addon", + "Compatible voice mods need an extra UDP port. The primary panel already supports Discord operational alerts; in-game bridges need plugins.", + ], + [ + "Permissions, moderation & rollback", + "Addon", + "Use compatible permissions, audit, anti-cheat, and rollback tools. Folia requires explicit support; a Paper plugin label is insufficient.", + ], + ], + }, + { + title: "Advanced infrastructure", + icon: Wrench, + items: [ + [ + "Custom domains & ingress", + "External setup", + "DNS/SRV records, HTTPS map reverse proxies, and Minecraft-aware TCP protection can provide a consistent entry point.", + ], + [ + "Version translation", + "Addon", + "ViaVersion-family plugins translate supported protocol differences. They cannot make incompatible client modpacks interchangeable.", + ], + [ + "Multiple physical hosts", + "External setup", + "A private network and per-host orchestration can spread servers across machines. This implementation manages one Docker host.", + ], + [ + "Maintenance & automation", + "Primary panel", + "The existing scheduler, metrics, notifications, and access flows target the primary server. Network profiles have independent automatic backups.", + ], + ], + }, +]; +export default function FeaturesPage() { + return ( +
+
+
+ CraftDeck / Possibilities +
+

Build the server you want.

+

+ A practical capability guide, from a small survival world to a network + of game modes. The labels show what this panel manages and what needs + additional software or infrastructure. +

+ + Open your network + + +
+
+ {groups.map((g) => ( + + + {g.title} + + } + > +
+ {g.items.map(([title, status, body]) => ( +
+
+

{title}

+ + {status} + +
+

{body}

+
+ ))} +
+
+ ))} +
+

+ Compatibility changes by Minecraft release. Verify the exact mod/plugin + versions and test changes on a fresh profile before moving players. +

+
+ + Folia documentation ↗ + + + Velocity compatibility ↗ + + + BlueMap documentation ↗ + + + Runtime documentation ↗ + +
+
+ ); +} diff --git a/dashboard/src/app/(panel)/maps/page.tsx b/dashboard/src/app/(panel)/maps/page.tsx new file mode 100644 index 0000000..167ea61 --- /dev/null +++ b/dashboard/src/app/(panel)/maps/page.tsx @@ -0,0 +1,131 @@ +"use client"; +import { useState } from "react"; +import Link from "next/link"; +import { Map, ExternalLink, RefreshCw, Layers } from "lucide-react"; +import { useApi } from "@/lib/api"; +import { Badge, Button, Card, EmptyState, Select } from "@/components/ui"; +import type { NetworkView } from "@/lib/network"; +export default function MapsPage() { + const { data, error, loading } = useApi("/api/network", 15000); + const [id, setId] = useState(""); + const [reload, setReload] = useState(0); + const maps = data?.profiles.filter((p) => p.map !== "none") ?? []; + const selected = maps.find((p) => p.id === id) ?? maps[0]; + return ( +
+
+
+
+ CraftDeck / Atlas +
+

Live world maps

+

+ Explore terrain, follow players, and view the overlays supplied by + your map renderer. +

+
+ + Configure maps → + +
+ {error && ( +

+ {error} +

+ )} + {loading &&

Loading maps…

} + {!!maps.length && ( +
+ + { + setProxy({ ...proxy, port: +e.target.value }); + setProxyDirty(true); + }} + /> + + + +
+

+ 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" + /> + + +