my-homelab-configs/infra/rpi-services/seed-uptime-kuma.js

225 lines
6.3 KiB
JavaScript
Executable File

#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
function findModuleDirectory(root, moduleName, depth = 0) {
if (depth > 5) {
return "";
}
let entries;
try {
entries = fs.readdirSync(root, { withFileTypes: true });
} catch (_error) {
return "";
}
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const fullPath = path.join(root, entry.name);
if (entry.name === moduleName && fs.existsSync(path.join(fullPath, "package.json"))) {
return fullPath;
}
}
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const fullPath = path.join(root, entry.name);
const found = findModuleDirectory(fullPath, moduleName, depth + 1);
if (found) {
return found;
}
}
return "";
}
function loadDatabaseModule() {
const candidates = [
"better-sqlite3",
"/app/node_modules/better-sqlite3",
"/app/server/node_modules/better-sqlite3",
"/app/src/node_modules/better-sqlite3"
];
for (const root of ["/app/node_modules", "/app/server/node_modules", "/app/src/node_modules"]) {
const found = findModuleDirectory(root, "better-sqlite3");
if (found) {
candidates.push(found);
}
}
for (const moduleName of candidates) {
try {
return require(moduleName);
} catch (_error) {
// Try the next location used by the upstream Uptime Kuma image.
}
}
throw new Error("Could not load better-sqlite3 from the Uptime Kuma container");
}
function nowSql() {
return new Date().toISOString().replace("T", " ").slice(0, 19);
}
function defaultForColumn(column, monitor, userId) {
const accepted = monitor.accepted_statuscodes || ["200-399"];
const defaults = {
name: monitor.name,
type: monitor.type,
active: 1,
user_id: userId,
interval: monitor.interval || 60,
retry_interval: monitor.retry_interval || 60,
resend_interval: monitor.resend_interval || 0,
maxretries: monitor.maxretries || 3,
accepted_statuscodes: JSON.stringify(accepted),
url: monitor.url || "",
method: monitor.method || "GET",
hostname: monitor.hostname || "",
port: monitor.port || null,
dns_resolve_type: monitor.dns_resolve_type || "A",
dns_resolve_server: monitor.dns_resolve_server || "1.1.1.1",
upside_down: 0,
weight: monitor.weight || 2000,
maxredirects: monitor.maxredirects || 10,
timeout: monitor.timeout || 48,
expiryNotification: 0,
ignoreTls: monitor.ignoreTls ? 1 : 0,
created_date: nowSql(),
modified_date: nowSql(),
body: "",
headers: "",
basic_auth_user: "",
basic_auth_pass: "",
authMethod: "",
mqtt_success_message: "",
database_connection_string: "",
database_query: "",
packet_size: 56
};
return Object.prototype.hasOwnProperty.call(defaults, column.name)
? defaults[column.name]
: "";
}
function monitorValues(monitor, columns, userId, includeCreatedDate) {
const values = {};
const supported = new Set(columns.map((column) => column.name));
const requested = {
name: monitor.name,
type: monitor.type,
active: 1,
user_id: userId,
interval: monitor.interval || 60,
retry_interval: monitor.retry_interval || 60,
resend_interval: monitor.resend_interval || 0,
maxretries: monitor.maxretries || 3,
accepted_statuscodes: JSON.stringify(monitor.accepted_statuscodes || ["200-399"]),
method: monitor.method || "GET",
url: monitor.url || "",
hostname: monitor.hostname || "",
port: monitor.port || null,
dns_resolve_type: monitor.dns_resolve_type || "A",
dns_resolve_server: monitor.dns_resolve_server || "1.1.1.1",
maxredirects: monitor.maxredirects || 10,
timeout: monitor.timeout || 48,
ignoreTls: monitor.ignoreTls ? 1 : 0,
modified_date: nowSql()
};
if (includeCreatedDate) {
requested.created_date = nowSql();
}
for (const [key, value] of Object.entries(requested)) {
if (supported.has(key)) {
values[key] = value;
}
}
for (const column of columns) {
if (column.pk || Object.prototype.hasOwnProperty.call(values, column.name)) {
continue;
}
if (column.notnull && column.dflt_value === null) {
values[column.name] = defaultForColumn(column, monitor, userId);
}
}
return values;
}
function upsertMonitor(database, monitor, columns, userId) {
const existing = database
.prepare("select id from monitor where user_id = ? and name = ? limit 1")
.get(userId, monitor.name);
if (existing) {
const values = monitorValues(monitor, columns, userId, false);
delete values.name;
delete values.user_id;
const assignments = Object.keys(values).map((key) => `"${key}" = @${key}`);
database
.prepare(`update monitor set ${assignments.join(", ")} where id = @id`)
.run({ ...values, id: existing.id });
console.log(`updated ${monitor.name}`);
return;
}
const values = monitorValues(monitor, columns, userId, true);
const keys = Object.keys(values);
database
.prepare(
`insert into monitor (${keys.map((key) => `"${key}"`).join(", ")}) values (${keys
.map((key) => `@${key}`)
.join(", ")})`
)
.run(values);
console.log(`created ${monitor.name}`);
}
function main() {
const monitorFile = process.argv[2] || "/tmp/homelab-kuma-monitors.json";
const databasePath = process.argv[3] || "/app/data/kuma.db";
const monitors = JSON.parse(fs.readFileSync(monitorFile, "utf8"));
const Database = loadDatabaseModule();
const database = new Database(databasePath);
const userTable = database
.prepare("select name from sqlite_master where type = 'table' and name = 'user'")
.get();
if (!userTable) {
console.log("Uptime Kuma user table is missing; skipping monitor seed");
return;
}
const user = database.prepare("select id from user order by id limit 1").get();
if (!user) {
console.log("Uptime Kuma initial setup has not been completed; skipping monitor seed");
return;
}
const columns = database.prepare("pragma table_info(monitor)").all();
if (!columns.length) {
throw new Error("Uptime Kuma monitor table is missing");
}
const transaction = database.transaction(() => {
for (const monitor of monitors) {
upsertMonitor(database, monitor, columns, user.id);
}
});
transaction();
}
main();