Next.js 16 + Tailwind v4 + shadcn v4 dashboard for managing a modded Forge 1.20.1 server. Includes server controls, player management, mod manager with Modrinth search and dependency resolution, world backups, snapshots, analytics, logs, and chat bridge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
180 lines
6.9 KiB
TypeScript
180 lines
6.9 KiB
TypeScript
import { NextRequest } from "next/server";
|
|
import { auth } from "@/lib/auth";
|
|
import { downloadMod } from "@/lib/modrinth";
|
|
import type { ModSide } from "@/lib/modrinth";
|
|
import { createSnapshot, restoreSnapshot, listSnapshots } from "@/lib/snapshots";
|
|
import { waitForServerAdaptive, rebuildModpack, addModMetadata } from "@/lib/mods";
|
|
import { exec } from "child_process";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
type ModToInstall = {
|
|
projectId: string;
|
|
versionId: string;
|
|
filename: string;
|
|
url: string;
|
|
title: string;
|
|
side: ModSide;
|
|
};
|
|
|
|
function restartServer(): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
exec("sudo systemctl restart minecraft.service", (err) => {
|
|
if (err) reject(err);
|
|
else resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const session = await auth();
|
|
if (!session) {
|
|
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
status: 403,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
const { mods, snapshotName } = (await req.json()) as {
|
|
mods: ModToInstall[];
|
|
snapshotName: string;
|
|
};
|
|
|
|
if (!Array.isArray(mods) || mods.length === 0) {
|
|
return new Response(JSON.stringify({ error: "No mods to install" }), {
|
|
status: 400,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
const stream = new TransformStream();
|
|
const writer = stream.writable.getWriter();
|
|
const encoder = new TextEncoder();
|
|
|
|
async function send(event: string, data: object) {
|
|
try {
|
|
await writer.write(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
} catch {}
|
|
}
|
|
|
|
// Run the pipeline in the background, streaming events as we go
|
|
(async () => {
|
|
const name = snapshotName || `before-install-${Date.now()}`;
|
|
|
|
try {
|
|
// 1. Create snapshot
|
|
await send("step", { id: "snapshot", status: "active", message: "Creating safety snapshot..." });
|
|
try {
|
|
createSnapshot(name);
|
|
} catch (e) {
|
|
await send("step", { id: "snapshot", status: "error", message: (e as Error).message });
|
|
await send("done", { success: false, message: `Failed to create snapshot: ${(e as Error).message}` });
|
|
return;
|
|
}
|
|
await send("step", { id: "snapshot", status: "done", message: "Snapshot created" });
|
|
|
|
// 2. Download mods
|
|
await send("step", { id: "download", status: "active", message: `Downloading ${mods.length} mod(s)...` });
|
|
try {
|
|
for (const mod of mods) {
|
|
await downloadMod(mod.url, mod.filename, mod.side);
|
|
addModMetadata(mod.filename, { projectId: mod.projectId, side: mod.side });
|
|
}
|
|
} catch (e) {
|
|
await send("step", { id: "download", status: "error", message: (e as Error).message });
|
|
await send("step", { id: "rollback", status: "active", message: "Rolling back..." });
|
|
try {
|
|
const snaps = listSnapshots();
|
|
if (snaps.length > 0) restoreSnapshot(snaps[0].dirName);
|
|
} catch {}
|
|
await send("step", { id: "rollback", status: "done", message: "Rolled back to snapshot" });
|
|
await send("done", { success: false, message: `Download failed: ${(e as Error).message}`, rolledBack: true });
|
|
return;
|
|
}
|
|
await send("step", { id: "download", status: "done", message: "All mods downloaded" });
|
|
|
|
// 3. Check if server-side mods need a restart
|
|
const hasServerMods = mods.some((m) => m.side !== "client");
|
|
|
|
if (!hasServerMods) {
|
|
await send("step", { id: "modpack", status: "active", message: "Rebuilding modpack..." });
|
|
try { rebuildModpack(); } catch {}
|
|
await send("step", { id: "modpack", status: "done", message: "Modpack updated" });
|
|
await send("done", {
|
|
success: true,
|
|
message: `Installed ${mods.length} client-only mod(s). No server restart needed.`,
|
|
installed: mods.map((m) => m.filename),
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 4. Restart server
|
|
await send("step", { id: "restart", status: "active", message: "Restarting server..." });
|
|
try {
|
|
await restartServer();
|
|
} catch (e) {
|
|
await send("step", { id: "restart", status: "error", message: (e as Error).message });
|
|
await send("step", { id: "rollback", status: "active", message: "Rolling back..." });
|
|
try {
|
|
const snaps = listSnapshots();
|
|
if (snaps.length > 0) restoreSnapshot(snaps[0].dirName);
|
|
} catch {}
|
|
await send("step", { id: "rollback", status: "done", message: "Rolled back to snapshot" });
|
|
await send("done", { success: false, message: `Server restart failed: ${(e as Error).message}`, rolledBack: true });
|
|
return;
|
|
}
|
|
await send("step", { id: "restart", status: "done", message: "Restart command sent" });
|
|
|
|
// 5. Wait for server with progress reporting
|
|
await send("step", { id: "health", status: "active", message: "Waiting for server..." });
|
|
const online = await waitForServerAdaptive(async (progress) => {
|
|
await send("step", { id: "health", status: "active", message: progress.message });
|
|
});
|
|
|
|
if (online) {
|
|
await send("step", { id: "health", status: "done", message: "Server is online" });
|
|
await send("step", { id: "modpack", status: "active", message: "Rebuilding modpack..." });
|
|
try { rebuildModpack(); } catch {}
|
|
await send("step", { id: "modpack", status: "done", message: "Modpack updated" });
|
|
await send("done", {
|
|
success: true,
|
|
message: `Installed ${mods.length} mod(s). Server is online.`,
|
|
installed: mods.map((m) => m.filename),
|
|
});
|
|
} else {
|
|
await send("step", { id: "health", status: "error", message: "Server failed to start" });
|
|
await send("step", { id: "rollback", status: "active", message: "Rolling back to snapshot..." });
|
|
try {
|
|
const snaps = listSnapshots();
|
|
if (snaps.length > 0) {
|
|
restoreSnapshot(snaps[0].dirName);
|
|
await restartServer();
|
|
await waitForServerAdaptive(async (progress) => {
|
|
await send("step", { id: "rollback", status: "active", message: `Rollback: ${progress.message}` });
|
|
});
|
|
try { rebuildModpack(); } catch {}
|
|
}
|
|
} catch {}
|
|
await send("step", { id: "rollback", status: "done", message: "Rolled back and server restarted" });
|
|
await send("done", {
|
|
success: false,
|
|
message: `Server failed to start after installing ${mods.length} mod(s). Rolled back to snapshot.`,
|
|
rolledBack: true,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
await send("done", { success: false, message: (e as Error).message });
|
|
} finally {
|
|
try { writer.close(); } catch {}
|
|
}
|
|
})();
|
|
|
|
return new Response(stream.readable, {
|
|
headers: {
|
|
"Content-Type": "text/event-stream",
|
|
"Cache-Control": "no-cache",
|
|
Connection: "keep-alive",
|
|
"X-Accel-Buffering": "no",
|
|
},
|
|
});
|
|
}
|