Minecraft 26.2 Fabric server stack with CraftDeck management panel
- docker-compose: itzg/minecraft-server (Fabric, offline mode, whitelist enforced, Modrinth-managed mods), itzg/mc-backup sidecar, custom dashboard - Gated auth for offline mode: approve username -> whitelisted with a short registration window -> EasyAuth /register + /login; auto-revoke on expiry - CraftDeck panel (Next.js 16, zero native deps): live console (SSE), players & stats, lag diagnostics (server TPS vs player ping via spark), performance graphs, Modrinth mod search/manager, backups & restore, sandboxed file browser/editor, .env config editor with compose apply, cron schedules, Discord alerts, scrypt-hashed panel login - scripts/setup.sh generates secrets; docs/ covers auth, mods, updating, backups, networking (Tailscale), troubleshooting Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
279d137ce5
98 files changed
+9925
No files matched your search
@@ -0,0 +1,97 @@
|
||||
import cron, { type ScheduledTask } from "node-cron";
|
||||
import { db } from "./db";
|
||||
import { notify } from "./notify";
|
||||
import { powerContainer, execInContainer } from "./docker";
|
||||
import { rcon } from "./rcon";
|
||||
import { say } from "./mc";
|
||||
import { env } from "./env";
|
||||
|
||||
/**
|
||||
* Scheduled tasks (cron-based): restarts, backups, announcements, arbitrary
|
||||
* console commands. Definitions live in the panel DB; jobs are (re)loaded
|
||||
* whenever they change.
|
||||
*/
|
||||
|
||||
export type Schedule = {
|
||||
id: number;
|
||||
name: string;
|
||||
cron: string;
|
||||
action: "restart" | "backup" | "command" | "announce";
|
||||
payload: string | null;
|
||||
enabled: number;
|
||||
last_run_at: number | null;
|
||||
last_result: string | null;
|
||||
};
|
||||
|
||||
const g = globalThis as unknown as { __craftdeckJobs?: Map<number, ScheduledTask> };
|
||||
|
||||
function jobs(): Map<number, ScheduledTask> {
|
||||
if (!g.__craftdeckJobs) g.__craftdeckJobs = new Map();
|
||||
return g.__craftdeckJobs;
|
||||
}
|
||||
|
||||
export function validateCron(expr: string): boolean {
|
||||
return cron.validate(expr);
|
||||
}
|
||||
|
||||
export async function runScheduleAction(action: Schedule["action"], payload: string | null): Promise<string> {
|
||||
switch (action) {
|
||||
case "restart": {
|
||||
try {
|
||||
await say("Server restarting in 30 seconds!");
|
||||
await new Promise((r) => setTimeout(r, 30_000));
|
||||
} catch {
|
||||
// server not reachable over RCON — restart anyway
|
||||
}
|
||||
await powerContainer(env.mcContainer, "restart");
|
||||
return "Restarted";
|
||||
}
|
||||
case "backup": {
|
||||
const res = await execInContainer(env.backupContainer, ["backup", "now"]);
|
||||
if (res.exitCode !== 0) throw new Error(res.output.slice(-500) || "backup failed");
|
||||
return "Backup completed";
|
||||
}
|
||||
case "command": {
|
||||
if (!payload) throw new Error("No command configured");
|
||||
return (await rcon().exec(payload)).slice(0, 500);
|
||||
}
|
||||
case "announce": {
|
||||
if (!payload) throw new Error("No message configured");
|
||||
await say(payload);
|
||||
return "Announced";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function execute(schedule: Schedule) {
|
||||
let result: string;
|
||||
try {
|
||||
result = await runScheduleAction(schedule.action, schedule.payload);
|
||||
if (schedule.action !== "announce") {
|
||||
notify("success", `Schedule "${schedule.name}" ran`, result, { discord: false });
|
||||
}
|
||||
} catch (err) {
|
||||
result = `ERROR: ${(err as Error).message}`;
|
||||
notify("error", `Schedule "${schedule.name}" failed`, (err as Error).message);
|
||||
}
|
||||
db()
|
||||
.prepare("UPDATE schedules SET last_run_at = ?, last_result = ? WHERE id = ?")
|
||||
.run(Date.now(), result.slice(0, 1000), schedule.id);
|
||||
}
|
||||
|
||||
export function reloadSchedules() {
|
||||
for (const task of jobs().values()) task.stop();
|
||||
jobs().clear();
|
||||
const rows = db().prepare("SELECT * FROM schedules WHERE enabled = 1").all() as unknown as Schedule[];
|
||||
for (const row of rows) {
|
||||
if (!cron.validate(row.cron)) continue;
|
||||
jobs().set(
|
||||
row.id,
|
||||
cron.schedule(row.cron, () => void execute(row))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function startScheduler() {
|
||||
reloadSchedules();
|
||||
}
|
||||
Reference in new issue
Block a user