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,120 @@
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { env } from "./env";
|
||||
|
||||
/**
|
||||
* Panel database (node:sqlite — no native deps). Single connection reused
|
||||
* across HMR reloads via globalThis.
|
||||
*/
|
||||
const g = globalThis as unknown as { __craftdeckDb?: DatabaseSync };
|
||||
|
||||
function open(): DatabaseSync {
|
||||
fs.mkdirSync(env.appDataDir, { recursive: true });
|
||||
const db = new DatabaseSync(path.join(env.appDataDir, "craftdeck.db"));
|
||||
db.exec("PRAGMA journal_mode = WAL");
|
||||
db.exec("PRAGMA busy_timeout = 5000");
|
||||
migrate(db);
|
||||
return db;
|
||||
}
|
||||
|
||||
function migrate(db: DatabaseSync) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS panel_users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
pass_hash TEXT NOT NULL,
|
||||
must_change INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS approved_players (
|
||||
id INTEGER PRIMARY KEY,
|
||||
username TEXT UNIQUE COLLATE NOCASE NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending', -- pending | registered | expired | revoked
|
||||
approved_at INTEGER,
|
||||
window_expires_at INTEGER,
|
||||
registered_at INTEGER,
|
||||
last_seen_at INTEGER,
|
||||
note TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS activity (
|
||||
id INTEGER PRIMARY KEY,
|
||||
ts INTEGER NOT NULL,
|
||||
type TEXT NOT NULL, -- join | leave | register | approve | expire | revoke | server_online | server_offline | backup | chat_command | crash
|
||||
player TEXT,
|
||||
detail TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_activity_ts ON activity(ts DESC);
|
||||
CREATE TABLE IF NOT EXISTS metrics (
|
||||
ts INTEGER PRIMARY KEY,
|
||||
cpu REAL, mem_used INTEGER, mem_limit INTEGER,
|
||||
net_rx INTEGER, net_tx INTEGER,
|
||||
tps REAL, mspt REAL, players INTEGER,
|
||||
disk_used INTEGER, disk_free INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS ping_samples (
|
||||
id INTEGER PRIMARY KEY,
|
||||
ts INTEGER NOT NULL,
|
||||
player TEXT NOT NULL,
|
||||
ping INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ping_ts ON ping_samples(ts DESC);
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id INTEGER PRIMARY KEY,
|
||||
ts INTEGER NOT NULL,
|
||||
level TEXT NOT NULL, -- info | success | warn | error
|
||||
title TEXT NOT NULL,
|
||||
body TEXT,
|
||||
read INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS schedules (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
cron TEXT NOT NULL,
|
||||
action TEXT NOT NULL, -- restart | backup | command | announce
|
||||
payload TEXT,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
last_run_at INTEGER,
|
||||
last_result TEXT,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
export function db(): DatabaseSync {
|
||||
if (!g.__craftdeckDb) g.__craftdeckDb = open();
|
||||
return g.__craftdeckDb;
|
||||
}
|
||||
|
||||
// ── settings helpers ────────────────────────────────────────────────────
|
||||
|
||||
export function getSetting(key: string, fallback: string): string {
|
||||
const row = db().prepare("SELECT value FROM settings WHERE key = ?").get(key) as
|
||||
| { value: string }
|
||||
| undefined;
|
||||
return row?.value ?? fallback;
|
||||
}
|
||||
|
||||
export function setSetting(key: string, value: string) {
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO settings(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
|
||||
)
|
||||
.run(key, value);
|
||||
}
|
||||
|
||||
export function getBoolSetting(key: string, fallback: boolean): boolean {
|
||||
return getSetting(key, fallback ? "1" : "0") === "1";
|
||||
}
|
||||
|
||||
// ── activity helpers ────────────────────────────────────────────────────
|
||||
|
||||
export function logActivity(type: string, player?: string | null, detail?: string | null) {
|
||||
db()
|
||||
.prepare("INSERT INTO activity(ts, type, player, detail) VALUES(?, ?, ?, ?)")
|
||||
.run(Date.now(), type, player ?? null, detail ?? null);
|
||||
}
|
||||
Reference in new issue
Block a user