"use client"; import { useState } from "react"; import { Package, Search, Trash2, Power, Download, RefreshCw, Lock } from "lucide-react"; import { api, useApi, toast } from "@/lib/api"; import { Card, Button, Badge, Input, EmptyState, Modal } from "@/components/ui"; import { fmtBytes } from "@/lib/format"; type ModsData = { installed: Array<{ file: string; name: string; id: string | null; version: string | null; disabled: boolean; size: number; }>; managed: string[]; mcVersion: string; }; type SearchHit = { slug: string; title: string; description: string; downloads: number; icon_url: string | null; server_side: string; client_side: string; }; const PROTECTED = new Set(["fabric-api", "easyauth"]); // Managed entries may carry a version qualifier ("easyauth:beta") — compare // by the base slug. const baseSlug = (entry: string) => entry.split(":")[0].toLowerCase(); // Server-side-only picks that make an offline friends server nicer — no // client installs needed. All verified compatible with Fabric 26.2. const FEATURED = [ { slug: "bluemap", emoji: "🗺", title: "BlueMap", description: "Explorable 3D web map of your world, like Google Maps for the server. After installing, the map appears on the Overview page.", }, { slug: "skinrestorer", emoji: "🎭", title: "SkinRestorer", description: "Offline-mode servers show everyone as Steve — this restores real skins by username, TLauncher players included.", }, { slug: "chunky", emoji: "⚡", title: "Chunky", description: "Pre-generates the world around spawn so nobody lags exploring new chunks. Run `chunky radius 2000` then `chunky start` in Console.", }, { slug: "proxy-protocol-support", emoji: "🛡", title: "Proxy Protocol Support", description: "Using TCPShield (or another proxy)? This restores players' real IPs so the Security page and IP bans work. Config is auto-enforced — see docs/tcpshield.md.", }, ]; export default function ModsPage() { const { data, refresh } = useApi("/api/mods", 0); const [query, setQuery] = useState(""); const [hits, setHits] = useState(null); const [searching, setSearching] = useState(false); const [busy, setBusy] = useState(null); const [applying, setApplying] = useState(false); const [needsApply, setNeedsApply] = useState(false); const [confirmApply, setConfirmApply] = useState(false); const search = async () => { if (!query.trim()) return; setSearching(true); try { setHits(await api(`/api/mods/search?q=${encodeURIComponent(query.trim())}`)); } catch (err) { toast((err as Error).message, "error"); } finally { setSearching(false); } }; const manage = async (label: string, body: Record, message: string) => { setBusy(label); try { const res = await api<{ needsApply?: boolean; needsRestart?: boolean }>("/api/mods/manage", { body }); toast(message, "success"); if (res.needsApply || res.needsRestart) setNeedsApply(true); void refresh(); } catch (err) { toast((err as Error).message, "error"); } finally { setBusy(null); } }; const apply = async () => { setConfirmApply(false); setApplying(true); try { await api("/api/mods/apply", { method: "POST" }); toast("Server recreated — mods re-resolved for the current version", "success"); setNeedsApply(false); void refresh(); } catch (err) { toast((err as Error).message, "error"); } finally { setApplying(false); } }; const managedSet = new Set((data?.managed ?? []).map(baseSlug)); return (

Mods

Managed mods auto-update to the newest build for Minecraft {data?.mcVersion ?? "…"} on every apply

{/* featured picks */}
{FEATURED.map((f) => { const already = managedSet.has(f.slug); return (
{f.emoji} {f.title}

{f.description}

); })}
{/* Modrinth search */}
setQuery(e.target.value)} onKeyDown={(e) => e.key === "Enter" && void search()} placeholder={`Search Fabric mods for ${data?.mcVersion ?? "26.2"}…`} className="w-full" />
{hits === null && (

Search Modrinth — results are pre-filtered to Fabric + your server version.

)} {hits?.length === 0 &&

No compatible mods found.

} {hits?.map((h) => { const already = managedSet.has(baseSlug(h.slug)); return (
{h.icon_url ? ( // eslint-disable-next-line @next/next/no-img-element ) : (
)}
{h.title} {h.client_side === "required" && ( needs client install )}
{h.description}
); })}
{/* managed list */}

These Modrinth projects are pinned in .env (MODRINTH_PROJECTS). The server resolves the newest compatible build on every start, so updating Minecraft also updates these automatically.

    {(data?.managed ?? []).map((slug) => (
  • {slug} {PROTECTED.has(baseSlug(slug)) && ( required )} {!PROTECTED.has(baseSlug(slug)) && ( )}
  • ))}
{/* installed jars */} {!data || data.installed.length === 0 ? ( } title="No mod jars found" hint="Jars appear here once the server has started and downloaded its mods. You can also upload jars via the Files page (mods/ folder)." /> ) : ( {data.installed.map((m) => ( ))}
Mod Version File Size Actions
{m.name} {m.disabled && disabled} {m.version ?? "—"} {m.file} {fmtBytes(m.size)}
)}
setConfirmApply(false)} title="Apply mod changes?">

This recreates the server container: online players are disconnected for a minute or two while the server restarts and re-resolves every managed mod against Minecraft {data?.mcVersion}.

); }