initialize generic rms-software repository

Add the reusable RMS core application (server, web UI, plugins, tests, tools) with generic defaults, GPL licensing, and maintainer context documentation so deployments can consume this repo as software source independent of station-specific overlays.
This commit is contained in:
2026-03-16 03:31:08 +01:00
commit e1a4ce0b8b
58 changed files with 20611 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
const fs = require("fs");
const fsp = require("fs/promises");
function createJsonStorage(options) {
const dataDir = options.dataDir;
return {
id: "json",
async init() {
await fsp.mkdir(dataDir, { recursive: true });
},
async exists(key) {
return fs.existsSync(key);
},
async readJson(key, fallback = null) {
try {
const raw = await fsp.readFile(key, "utf8");
const parsed = parseJsonWithTrailingRecovery(raw);
if (parsed.ok) {
if (parsed.repaired) {
await backupCorruptJsonFile(key, raw, "trailing-garbage");
await fsp.writeFile(key, `${JSON.stringify(parsed.value, null, 2)}\n`, "utf8");
console.warn(`[json-storage] repaired trailing JSON data in ${key}`);
}
return parsed.value;
}
if (fallback !== undefined) {
await backupCorruptJsonFile(key, raw, "invalid-json");
await fsp.writeFile(key, `${JSON.stringify(fallback, null, 2)}\n`, "utf8");
console.warn(`[json-storage] invalid JSON in ${key}, restored fallback`);
return fallback;
}
throw parsed.error;
} catch (error) {
if (error.code === "ENOENT") {
return fallback;
}
throw error;
}
},
async writeJson(key, value) {
await fsp.writeFile(key, `${JSON.stringify(value, null, 2)}\n`, "utf8");
},
async appendText(key, text) {
await fsp.appendFile(key, text, "utf8");
},
async readText(key, fallback = "") {
try {
return await fsp.readFile(key, "utf8");
} catch (error) {
if (error.code === "ENOENT") {
return fallback;
}
throw error;
}
},
async writeText(key, text) {
await fsp.writeFile(key, text, "utf8");
}
};
}
function parseJsonWithTrailingRecovery(raw) {
try {
return { ok: true, repaired: false, value: JSON.parse(raw) };
} catch (error) {
const message = String(error && error.message ? error.message : "");
const match = message.match(/position\s+(\d+)/i);
if (!match) {
return { ok: false, error };
}
const position = Number(match[1]);
if (!Number.isFinite(position) || position <= 0 || position >= raw.length) {
return { ok: false, error };
}
const prefix = raw.slice(0, position).trimEnd();
if (!prefix) {
return { ok: false, error };
}
try {
return { ok: true, repaired: true, value: JSON.parse(prefix) };
} catch {
return { ok: false, error };
}
}
}
async function backupCorruptJsonFile(key, raw, reason) {
try {
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const suffix = String(reason || "corrupt").replace(/[^a-z0-9-]/gi, "-").toLowerCase();
const backupPath = `${key}.${suffix}.${stamp}.bak`;
await fsp.writeFile(backupPath, raw, "utf8");
} catch {
// best effort backup only
}
}
module.exports = {
createJsonStorage
};

View File

@@ -0,0 +1,66 @@
const fs = require("fs");
const path = require("path");
function createSqliteStorage(options) {
const sqlitePath = options.sqlitePath;
let db = null;
function requireDriver() {
try {
return require("node:sqlite").DatabaseSync;
} catch {
throw new Error("SQLite storage requires Node.js with built-in 'node:sqlite' (Node 22+)");
}
}
return {
id: "sqlite",
async init() {
const Database = requireDriver();
fs.mkdirSync(path.dirname(sqlitePath), { recursive: true });
db = new Database(sqlitePath);
db.exec("PRAGMA journal_mode = WAL");
db.exec("CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL)");
db.exec("CREATE TABLE IF NOT EXISTS logs (k TEXT NOT NULL, ts INTEGER NOT NULL, line TEXT NOT NULL)");
},
async exists(key) {
const row = db.prepare("SELECT 1 AS ok FROM kv WHERE k = ? LIMIT 1").get(key);
return Boolean(row);
},
async readJson(key, fallback = null) {
const row = db.prepare("SELECT v FROM kv WHERE k = ? LIMIT 1").get(key);
if (!row) {
return fallback;
}
try {
return JSON.parse(row.v);
} catch {
return fallback;
}
},
async writeJson(key, value) {
const raw = JSON.stringify(value);
db.prepare("INSERT INTO kv(k, v) VALUES(?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v").run(key, raw);
},
async appendText(key, text) {
db.prepare("INSERT INTO logs(k, ts, line) VALUES(?, ?, ?)").run(key, Date.now(), text);
},
async readText(key, fallback = "") {
const rows = db.prepare("SELECT line FROM logs WHERE k = ? ORDER BY ts ASC").all(key);
if (!rows.length) {
return fallback;
}
return rows.map((row) => row.line).join("");
},
async writeText(key, text) {
db.prepare("DELETE FROM logs WHERE k = ?").run(key);
if (text) {
db.prepare("INSERT INTO logs(k, ts, line) VALUES(?, ?, ?)").run(key, Date.now(), text);
}
}
};
}
module.exports = {
createSqliteStorage
};