"use client"; import { useEffect, useState } from "react"; import { KeyRound, Webhook, Globe, AlertTriangle } from "lucide-react"; import { api, useApi, toast } from "@/lib/api"; import { Card, Button, Input, Toggle, Modal } from "@/components/ui"; import { fmtBytes } from "@/lib/format"; type Settings = { registrationWindowMinutes: number; notifyJoins: boolean; notifyBlocked: boolean; discordEnabled: boolean; discordJoins: boolean; discordBlocked: boolean; discordWebhookUrl: string; }; type WorldInfo = { seed: string | null; sizeBytes: number | null; dimensions: string[]; levelName: string | null }; export default function SettingsPage() { const { data, refresh } = useApi("/api/settings", 0); const { data: me, refresh: refreshMe } = useApi<{ username: string; mustChange: boolean }>("/api/auth/me"); const { data: world } = useApi("/api/world", 0); const [values, setValues] = useState(null); const [saving, setSaving] = useState(false); const [currentPw, setCurrentPw] = useState(""); const [newPw, setNewPw] = useState(""); const [newPw2, setNewPw2] = useState(""); const [changing, setChanging] = useState(false); const [resetOpen, setResetOpen] = useState(false); const [resetConfirm, setResetConfirm] = useState(""); const [resetting, setResetting] = useState(false); useEffect(() => { if (data) setValues(data); }, [data]); const save = async () => { if (!values) return; setSaving(true); try { await api("/api/settings", { body: values }); toast("Settings saved", "success"); void refresh(); } catch (err) { toast((err as Error).message, "error"); } finally { setSaving(false); } }; const changePassword = async () => { if (newPw !== newPw2) { toast("New passwords don't match", "error"); return; } setChanging(true); try { await api("/api/auth/password", { body: { current: currentPw, next: newPw } }); toast("Panel password changed", "success"); setCurrentPw(""); setNewPw(""); setNewPw2(""); void refreshMe(); } catch (err) { toast((err as Error).message, "error"); } finally { setChanging(false); } }; const resetWorld = async () => { setResetting(true); try { const res = await api<{ archived: string }>("/api/world", { body: { op: "reset", confirm: resetConfirm }, }); toast(`World reset — old world saved as ${res.archived}`, "warn"); setResetOpen(false); setResetConfirm(""); } catch (err) { toast((err as Error).message, "error"); } finally { setResetting(false); } }; return (

Settings

Panel account, authentication window, notifications, danger zone

{me?.mustChange && (
You're still on the initial admin password — change it below.
)}
{/* password */}
setCurrentPw(e.target.value)} className="w-full" />
setNewPw(e.target.value)} className="w-full" />
setNewPw2(e.target.value)} className="w-full" />

At least 8 characters.

{/* auth window */}
setValues((v) => (v ? { ...v, registrationWindowMinutes: Number(e.target.value) } : v)) } className="w-32 font-mono" />

After you approve a username, the player has this long to join and /register. When it lapses the name is automatically removed from the whitelist again.

setValues((s) => (s ? { ...s, notifyJoins: v } : s))} label="Notify on player joins/leaves" /> setValues((s) => (s ? { ...s, notifyBlocked: v } : s))} label="Notify on blocked join attempts (bots/strangers)" />
{/* discord */}
setValues((v) => (v ? { ...v, discordWebhookUrl: e.target.value } : v))} placeholder="https://discord.com/api/webhooks/…" className="w-full font-mono text-xs" />

Discord → Server settings → Integrations → Webhooks → New webhook → Copy URL

setValues((s) => (s ? { ...s, discordEnabled: v } : s))} label="Send alerts to Discord" /> setValues((s) => (s ? { ...s, discordJoins: v } : s))} label="Include joins/leaves" /> setValues((s) => (s ? { ...s, discordBlocked: v } : s))} label="Include blocked join attempts" />
{/* danger zone */}
Reset world
Current world: {world?.levelName ?? "world"} ({fmtBytes(world?.sizeBytes)}) — archived, then a fresh world generates. A safety backup is taken first.
setResetOpen(false)} title="Reset the world?">

The server stops, the current world folder is archived inside the data directory, and a brand-new world generates on next start. A backup is taken first, but treat this as destructive.

Type reset the world to confirm:

setResetConfirm(e.target.value)} placeholder="reset the world" className="w-full font-mono" />
); }