mc-dashboard/app/api/mods/batch-install/route.ts
hurkicorgi dd69c17c3b Initial commit: Minecraft dashboard
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>
2026-04-13 00:46:58 -06:00

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",
},
});
}