Restore source files from Google Drive backup

This commit is contained in:
Aryan Singh committed 2026-10-27 12:34:01 -05:00
1 parent dff2b5a9c4
commit 328525b37f
27 files changed
+975 -106

No files matched your search

+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env node
// @ts-check
/**
* Feed exporter for the CraftDeck public read-only mirror.
*
* Reads the live panel database (READ-ONLY) and writes a small, sanitised
* SQLite "feed" file that the PUBLIC_READONLY dashboard serves from. The feed
* contains ONLY:
*
* metrics – the sampler time-series (last 7 days)
* activity – join/leave events only, as (id, ts, type, player); the
* free-text `detail` column (which can hold IPs on connect/
* blocked rows) is deliberately dropped
* feed_meta – one small status JSON blob (running, online names, counts)
*
* It NEVER copies panel_users, approved_players, settings, notifications,
* schedules or ping_samples, so none of those can leak to the public app.
*
* The file is written to a temp path and atomically renamed into place, so
* readers never observe a half-written feed. No external dependencies — only
* the built-in node:sqlite module.
*
* Env:
* SOURCE_DB path to the live panel DB (default: dev-data/app/craftdeck.db)
* FEED_PATH path to write the feed to (default: dev-data/feed/feed.db)
* MC_VERSION optional version string for the status blob
* SERVER_TYPE optional server type string for the status blob
*/
import { DatabaseSync } from "node:sqlite";
import fs from "node:fs";
import path from "node:path";
const SOURCE_DB = process.env.SOURCE_DB || "dev-data/app/craftdeck.db";
const FEED_PATH = process.env.FEED_PATH || "dev-data/feed/feed.db";
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
function log(msg) {
console.log(`[export-feed] ${msg}`);
}
if (!fs.existsSync(SOURCE_DB)) {
console.error(`[export-feed] source DB not found: ${SOURCE_DB}`);
process.exit(1);
}
const src = new DatabaseSync(SOURCE_DB, { readOnly: true });
// ── derive current status from the panel DB (no Docker/RCON) ──────────────
const now = Date.now();
const latest =
/** @type {any} */ (src.prepare("SELECT * FROM metrics ORDER BY ts DESC LIMIT 1").get()) || null;
// The sampler writes a row every 10s; a fresh row with cpu present means the
// mc container was up at sample time.
const fresh = latest && now - Number(latest.ts) < 60_000;
const running = Boolean(fresh && latest.cpu != null);
// Reconstruct who is currently online by replaying join/leave since the most
// recent server start (a restart clears everyone).
let onlineNames = [];
if (running) {
const lastStart = /** @type {any} */ (
src
.prepare("SELECT MAX(ts) AS ts FROM activity WHERE type IN ('server_online')")
.get()
);
const sinceTs = lastStart && lastStart.ts ? Number(lastStart.ts) : now - 24 * 60 * 60 * 1000;
const events = /** @type {any[]} */ (
src
.prepare(
"SELECT ts, type, player FROM activity WHERE ts >= ? AND type IN ('join','leave') ORDER BY ts ASC"
)
.all(sinceTs)
);
const set = new Set();
for (const e of events) {
if (!e.player) continue;
if (e.type === "join") set.add(e.player);
else set.delete(e.player);
}
onlineNames = [...set];
}
const playersOnline = running ? Number(latest?.players ?? onlineNames.length) || onlineNames.length : 0;
const status = {
generatedAt: now,
running,
playersOnline,
playersMax: 0,
playerNames: onlineNames,
version: process.env.MC_VERSION || null,
serverType: process.env.SERVER_TYPE || null,
};
// ── build the feed in a temp file, then atomically swap it in ─────────────
fs.mkdirSync(path.dirname(FEED_PATH), { recursive: true });
const tmp = `${FEED_PATH}.tmp-${process.pid}`;
for (const p of [tmp, `${tmp}-wal`, `${tmp}-shm`, `${tmp}-journal`]) {
try {
fs.rmSync(p);
} catch {}
}
const out = new DatabaseSync(tmp);
out.exec("PRAGMA journal_mode = DELETE");
out.exec(`
CREATE TABLE 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 activity (
id INTEGER PRIMARY KEY,
ts INTEGER NOT NULL,
type TEXT NOT NULL,
player TEXT
);
CREATE INDEX idx_activity_ts ON activity(ts DESC);
CREATE TABLE feed_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
`);
const since = now - WEEK_MS;
const metricRows = /** @type {any[]} */ (
src.prepare("SELECT * FROM metrics WHERE ts > ? ORDER BY ts ASC").all(since)
);
const insMetric = out.prepare(
`INSERT INTO metrics(ts, cpu, mem_used, mem_limit, net_rx, net_tx, tps, mspt, players, disk_used, disk_free)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
);
for (const m of metricRows) {
insMetric.run(
m.ts, m.cpu, m.mem_used, m.mem_limit, m.net_rx, m.net_tx,
m.tps, m.mspt, m.players, m.disk_used, m.disk_free
);
}
// Only join/leave — the `detail` column is intentionally not carried over.
const activityRows = /** @type {any[]} */ (
src
.prepare(
"SELECT id, ts, type, player FROM activity WHERE ts > ? AND type IN ('join','leave') ORDER BY ts ASC"
)
.all(since)
);
const insActivity = out.prepare("INSERT INTO activity(id, ts, type, player) VALUES(?, ?, ?, ?)");
for (const a of activityRows) insActivity.run(a.id, a.ts, a.type, a.player);
out.prepare("INSERT INTO feed_meta(key, value) VALUES('status', ?)").run(JSON.stringify(status));
out.close();
src.close();
// Atomic replace (same filesystem). Clean up any leftover side files.
for (const p of [`${FEED_PATH}-wal`, `${FEED_PATH}-shm`, `${FEED_PATH}-journal`]) {
try {
fs.rmSync(p);
} catch {}
}
fs.renameSync(tmp, FEED_PATH);
log(
`wrote ${FEED_PATH}: ${metricRows.length} metrics, ${activityRows.length} join/leave, ` +
`running=${running}, online=${playersOnline}`
);