Files
DenozordecandCursor 654b08c8e5
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / changes (push) Successful in 4s
quality / docker-check (push) Skipped
quality / web (push) Successful in 45s
quality / api (push) Successful in 36s
CD / quality (push) Successful in 1m30s
CD / publish (push) Successful in 1m51s
fix(ui): полный справочник стран и городов как в VPS Tracker
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 15:15:34 +07:00

741 lines
25 KiB
JavaScript

// src/schema.ts
import { sql } from "drizzle-orm";
import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
var appSettings = sqliteTable("app_settings", {
id: text("id").primaryKey(),
show_quick_actions: integer("show_quick_actions", { mode: "boolean" }).notNull().default(true),
default_ttl: integer("default_ttl").notNull().default(300),
naming_template: text("naming_template").notNull().default("{loc}-{role}{nn}.{zone}"),
proxied_lock: integer("proxied_lock", { mode: "boolean" }).notNull().default(true),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
});
var zones = sqliteTable("zones", {
id: text("id").primaryKey(),
cf_zone_id: text("cf_zone_id"),
name: text("name").notNull().unique(),
role: text("role").notNull().default("routing"),
naming_template: text("naming_template").notNull().default("{loc}-{role}{nn}.{zone}"),
default_ttl: integer("default_ttl").notNull().default(300),
last_sync_at: text("last_sync_at"),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
});
var locations = sqliteTable("locations", {
id: text("id").primaryKey(),
code: text("code").notNull().unique(),
name: text("name").notNull(),
country: text("country"),
sort_order: integer("sort_order").notNull().default(0),
created_at: text("created_at").notNull().default(sql`datetime('now')`)
});
var nodes = sqliteTable(
"nodes",
{
id: text("id").primaryKey(),
zone_id: text("zone_id").notNull().references(() => zones.id, { onDelete: "cascade" }),
location_id: text("location_id").notNull().references(() => locations.id),
hostname: text("hostname").notNull(),
role: text("role").notNull(),
index_num: integer("index_num").notNull().default(1),
provider_tag: text("provider_tag"),
notes: text("notes"),
sync_status: text("sync_status").notNull().default("pending"),
cf_a_record_id: text("cf_a_record_id"),
cf_aaaa_record_id: text("cf_aaaa_record_id"),
last_error: text("last_error"),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
},
(t) => [uniqueIndex("nodes_zone_hostname").on(t.zone_id, t.hostname)]
);
var nodeAddresses = sqliteTable(
"node_addresses",
{
id: text("id").primaryKey(),
node_id: text("node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }),
family: text("family").notNull(),
ip: text("ip").notNull()
},
(t) => [uniqueIndex("node_addresses_node_family").on(t.node_id, t.family)]
);
var aliases = sqliteTable(
"aliases",
{
id: text("id").primaryKey(),
zone_id: text("zone_id").notNull().references(() => zones.id, { onDelete: "cascade" }),
name: text("name").notNull(),
purpose: text("purpose").notNull().default("geo"),
mode: text("mode").notNull().default("primary"),
target_node_id: text("target_node_id").notNull().references(() => nodes.id, { onDelete: "restrict" }),
sync_status: text("sync_status").notNull().default("pending"),
cf_record_id: text("cf_record_id"),
last_error: text("last_error"),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
},
(t) => [uniqueIndex("aliases_zone_name").on(t.zone_id, t.name)]
);
var syncJobs = sqliteTable("sync_jobs", {
id: text("id").primaryKey(),
zone_id: text("zone_id").notNull().references(() => zones.id, { onDelete: "cascade" }),
status: text("status").notNull().default("pending"),
diff_json: text("diff_json"),
error: text("error"),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
finished_at: text("finished_at")
});
var syncEvents = sqliteTable("sync_events", {
id: text("id").primaryKey(),
job_id: text("job_id").notNull().references(() => syncJobs.id, { onDelete: "cascade" }),
kind: text("kind").notNull(),
record_name: text("record_name"),
record_type: text("record_type"),
detail: text("detail"),
created_at: text("created_at").notNull().default(sql`datetime('now')`)
});
var ignoredOrphans = sqliteTable(
"ignored_orphans",
{
id: text("id").primaryKey(),
zone_id: text("zone_id").notNull().references(() => zones.id, { onDelete: "cascade" }),
record_name: text("record_name").notNull(),
record_type: text("record_type").notNull(),
created_at: text("created_at").notNull().default(sql`datetime('now')`)
},
(t) => [
uniqueIndex("ignored_orphans_unique").on(
t.zone_id,
t.record_name,
t.record_type
)
]
);
var schema = {
appSettings,
zones,
locations,
nodes,
nodeAddresses,
aliases,
syncJobs,
syncEvents,
ignoredOrphans
};
// src/client.ts
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { readFileSync, readdirSync } from "fs";
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
var __dirname = dirname(fileURLToPath(import.meta.url));
function resolveDatabasePath(databaseUrl) {
const url = databaseUrl.startsWith("sqlite:") ? databaseUrl.slice("sqlite:".length) : databaseUrl;
return url;
}
function createDb(databaseUrl) {
const path = resolveDatabasePath(databaseUrl);
const sqlite = new Database(path);
sqlite.pragma("journal_mode = WAL");
sqlite.pragma("synchronous = NORMAL");
sqlite.pragma("foreign_keys = ON");
const db = drizzle(sqlite, { schema });
return { db, sqlite };
}
function createMemoryDb() {
const sqlite = new Database(":memory:");
sqlite.pragma("foreign_keys = ON");
const db = drizzle(sqlite, { schema });
return { db, sqlite };
}
function runMigrations(sqlite) {
const migrationsDir = join(__dirname, "..", "migrations");
const files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
sqlite.exec(
`CREATE TABLE IF NOT EXISTS _migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
)`
);
for (const file of files) {
const applied = sqlite.prepare("SELECT 1 FROM _migrations WHERE name = ?").get(file);
if (applied) continue;
const sql3 = readFileSync(join(migrationsDir, file), "utf-8");
sqlite.exec(sql3);
sqlite.prepare("INSERT INTO _migrations (name) VALUES (?)").run(file);
}
}
function healthCheck(sqlite) {
sqlite.prepare("SELECT 1").get();
}
// src/errors.ts
var NotFoundError = class extends Error {
constructor(message) {
super(message);
this.name = "NotFoundError";
}
};
var ConflictError = class extends Error {
constructor(message) {
super(message);
this.name = "ConflictError";
}
};
// src/settings-repo.ts
import { eq } from "drizzle-orm";
var SETTINGS_ID = "settings-main";
function toDto(row) {
return {
id: row.id,
showQuickActions: row.show_quick_actions == null ? true : Boolean(row.show_quick_actions),
defaultTtl: row.default_ttl ?? 300,
namingTemplate: row.naming_template ?? "{loc}-{role}{nn}.{zone}",
proxiedLock: row.proxied_lock == null ? true : Boolean(row.proxied_lock)
};
}
function ensureRow(db) {
const existing = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get();
if (existing) return existing;
db.insert(appSettings).values({
id: SETTINGS_ID,
show_quick_actions: true,
default_ttl: 300,
naming_template: "{loc}-{role}{nn}.{zone}",
proxied_lock: true
}).run();
return db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get();
}
function getAppSettings(db) {
return toDto(ensureRow(db));
}
function updateAppSettings(db, patch) {
ensureRow(db);
const updates = {
updated_at: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
};
if (patch.showQuickActions !== void 0) {
updates.show_quick_actions = patch.showQuickActions;
}
if (patch.defaultTtl !== void 0) {
updates.default_ttl = patch.defaultTtl;
}
if (patch.namingTemplate !== void 0) {
updates.naming_template = patch.namingTemplate;
}
if (patch.proxiedLock !== void 0) {
updates.proxied_lock = patch.proxiedLock;
}
db.update(appSettings).set(updates).where(eq(appSettings.id, SETTINGS_ID)).run();
return getAppSettings(db);
}
// src/fleet-repo.ts
import { randomUUID } from "crypto";
import { and, asc, count, eq as eq2, sql as sql2 } from "drizzle-orm";
import { catalogCitiesWithLocCodes } from "@cdnmanager/shared";
function now() {
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
}
function id(prefix) {
return `${prefix}-${randomUUID().slice(0, 8)}`;
}
function mapLocation(row) {
return {
id: row.id,
code: row.code,
name: row.name,
country: row.country,
sortOrder: row.sort_order
};
}
function mapZone(row) {
return {
id: row.id,
cfZoneId: row.cf_zone_id,
name: row.name,
role: row.role,
namingTemplate: row.naming_template,
defaultTtl: row.default_ttl,
lastSyncAt: row.last_sync_at,
createdAt: row.created_at,
updatedAt: row.updated_at
};
}
function listLocations(db) {
return db.select().from(locations).orderBy(asc(locations.sort_order), asc(locations.code)).all().map(mapLocation);
}
function getLocation(db, locationId) {
const row = db.select().from(locations).where(eq2(locations.id, locationId)).get();
if (!row) throw new NotFoundError(`location ${locationId}`);
return mapLocation(row);
}
function ensureCatalogLocations(db) {
const existing = db.select().from(locations).all();
const byCode = new Map(existing.map((r) => [r.code.toLowerCase(), r]));
const byNameCountry = new Map(
existing.map((r) => [
`${(r.name || "").trim().toLowerCase()}|${(r.country || "").toUpperCase()}`,
r
])
);
let inserted = 0;
let sortOrder = 100;
for (const city of catalogCitiesWithLocCodes()) {
const nameKey = `${city.name.trim().toLowerCase()}|${city.countryCode}`;
if (byNameCountry.has(nameKey)) continue;
if (byCode.has(city.locCode.toLowerCase())) {
const alt = `${city.locCode}${city.countryCode.toLowerCase()}`;
if (byCode.has(alt) || byNameCountry.has(nameKey)) continue;
db.insert(locations).values({
id: `loc-${alt}`,
code: alt,
name: city.name,
country: city.countryCode,
sort_order: sortOrder++
}).run();
byCode.set(alt, { id: `loc-${alt}` });
byNameCountry.set(nameKey, { id: `loc-${alt}` });
inserted += 1;
continue;
}
db.insert(locations).values({
id: `loc-${city.locCode}`,
code: city.locCode,
name: city.name,
country: city.countryCode,
sort_order: sortOrder++
}).run();
byCode.set(city.locCode.toLowerCase(), {
id: `loc-${city.locCode}`
});
byNameCountry.set(nameKey, {
id: `loc-${city.locCode}`
});
inserted += 1;
}
return inserted;
}
function findLocationByCity(db, cityName, countryCode) {
const q = cityName.trim().toLowerCase();
if (!q) return null;
const rows = listLocations(db);
const match = rows.find((l) => {
if (l.name.trim().toLowerCase() !== q) return false;
if (countryCode && l.country && l.country.toUpperCase() !== countryCode.toUpperCase()) {
return false;
}
return true;
});
return match ?? null;
}
function listZones(db) {
return db.select().from(zones).orderBy(asc(zones.name)).all().map(mapZone);
}
function getZone(db, zoneId) {
const row = db.select().from(zones).where(eq2(zones.id, zoneId)).get();
if (!row) throw new NotFoundError(`zone ${zoneId}`);
return mapZone(row);
}
function createZone(db, input) {
const existing = db.select().from(zones).where(eq2(zones.name, input.name)).get();
if (existing) throw new ConflictError(`zone ${input.name} already exists`);
const zoneId = id("zone");
const ts = now();
db.insert(zones).values({
id: zoneId,
name: input.name,
cf_zone_id: input.cfZoneId ?? null,
role: input.role ?? "routing",
naming_template: input.namingTemplate ?? "{loc}-{role}{nn}.{zone}",
default_ttl: input.defaultTtl ?? 300,
created_at: ts,
updated_at: ts
}).run();
return getZone(db, zoneId);
}
function updateZone(db, zoneId, patch) {
getZone(db, zoneId);
const updates = {
updated_at: now()
};
if (patch.name !== void 0) updates.name = patch.name;
if (patch.cfZoneId !== void 0) updates.cf_zone_id = patch.cfZoneId;
if (patch.role !== void 0) updates.role = patch.role;
if (patch.namingTemplate !== void 0)
updates.naming_template = patch.namingTemplate;
if (patch.defaultTtl !== void 0) updates.default_ttl = patch.defaultTtl;
if (patch.lastSyncAt !== void 0) updates.last_sync_at = patch.lastSyncAt;
db.update(zones).set(updates).where(eq2(zones.id, zoneId)).run();
return getZone(db, zoneId);
}
function deleteZone(db, zoneId) {
getZone(db, zoneId);
db.delete(zones).where(eq2(zones.id, zoneId)).run();
}
function loadAddresses(db, nodeId) {
return db.select().from(nodeAddresses).where(eq2(nodeAddresses.node_id, nodeId)).all().map((r) => ({
id: r.id,
family: r.family,
ip: r.ip
}));
}
function mapNode(db, row, loc) {
const aliasCount = db.select({ c: count() }).from(aliases).where(eq2(aliases.target_node_id, row.id)).get()?.c;
return {
id: row.id,
zoneId: row.zone_id,
locationId: row.location_id,
locationCode: loc?.code,
locationName: loc?.name,
hostname: row.hostname,
role: row.role,
indexNum: row.index_num,
providerTag: row.provider_tag,
notes: row.notes,
syncStatus: row.sync_status,
cfARecordId: row.cf_a_record_id,
cfAaaaRecordId: row.cf_aaaa_record_id,
lastError: row.last_error,
addresses: loadAddresses(db, row.id),
aliasCount: Number(aliasCount ?? 0),
createdAt: row.created_at,
updatedAt: row.updated_at
};
}
function listNodes(db, filter = {}) {
let rows = db.select().from(nodes).all();
if (filter.zoneId) rows = rows.filter((r) => r.zone_id === filter.zoneId);
if (filter.locationId)
rows = rows.filter((r) => r.location_id === filter.locationId);
if (filter.role) rows = rows.filter((r) => r.role === filter.role);
if (filter.syncStatus)
rows = rows.filter((r) => r.sync_status === filter.syncStatus);
if (filter.q) {
const q = filter.q.toLowerCase();
rows = rows.filter(
(r) => r.hostname.toLowerCase().includes(q) || (r.provider_tag ?? "").toLowerCase().includes(q)
);
}
const locMap = new Map(
listLocations(db).map((l) => [l.id, { code: l.code, name: l.name }])
);
return rows.map((r) => mapNode(db, r, locMap.get(r.location_id))).sort((a, b) => a.hostname.localeCompare(b.hostname));
}
function getNode(db, nodeId) {
const row = db.select().from(nodes).where(eq2(nodes.id, nodeId)).get();
if (!row) throw new NotFoundError(`node ${nodeId}`);
const loc = getLocation(db, row.location_id);
return mapNode(db, row, { code: loc.code, name: loc.name });
}
function setAddresses(db, nodeId, ipv4, ipv6) {
db.delete(nodeAddresses).where(eq2(nodeAddresses.node_id, nodeId)).run();
db.insert(nodeAddresses).values({ id: id("addr"), node_id: nodeId, family: "v4", ip: ipv4 }).run();
if (ipv6) {
db.insert(nodeAddresses).values({ id: id("addr"), node_id: nodeId, family: "v6", ip: ipv6 }).run();
}
}
function createNode(db, input) {
getZone(db, input.zoneId);
getLocation(db, input.locationId);
const dup = db.select().from(nodes).where(
and(eq2(nodes.zone_id, input.zoneId), eq2(nodes.hostname, input.hostname))
).get();
if (dup) throw new ConflictError(`node ${input.hostname} already exists`);
const nodeId = id("node");
const ts = now();
db.insert(nodes).values({
id: nodeId,
zone_id: input.zoneId,
location_id: input.locationId,
hostname: input.hostname,
role: input.role,
index_num: input.indexNum,
provider_tag: input.providerTag ?? null,
notes: input.notes ?? null,
sync_status: "pending",
created_at: ts,
updated_at: ts
}).run();
setAddresses(db, nodeId, input.ipv4, input.ipv6);
return getNode(db, nodeId);
}
function updateNode(db, nodeId, patch) {
const current = getNode(db, nodeId);
if (patch.locationId) getLocation(db, patch.locationId);
const updates = {
updated_at: now()
};
if (patch.locationId !== void 0) updates.location_id = patch.locationId;
if (patch.hostname !== void 0) updates.hostname = patch.hostname;
if (patch.role !== void 0) updates.role = patch.role;
if (patch.indexNum !== void 0) updates.index_num = patch.indexNum;
if (patch.providerTag !== void 0) updates.provider_tag = patch.providerTag;
if (patch.notes !== void 0) updates.notes = patch.notes;
if (patch.syncStatus !== void 0) updates.sync_status = patch.syncStatus;
if (patch.cfARecordId !== void 0)
updates.cf_a_record_id = patch.cfARecordId;
if (patch.cfAaaaRecordId !== void 0)
updates.cf_aaaa_record_id = patch.cfAaaaRecordId;
if (patch.lastError !== void 0) updates.last_error = patch.lastError;
db.update(nodes).set(updates).where(eq2(nodes.id, nodeId)).run();
if (patch.ipv4 !== void 0) {
const v6 = patch.ipv6 !== void 0 ? patch.ipv6 : current.addresses.find((a) => a.family === "v6")?.ip ?? null;
setAddresses(db, nodeId, patch.ipv4, v6);
} else if (patch.ipv6 !== void 0) {
const v4 = current.addresses.find((a) => a.family === "v4")?.ip;
if (!v4) throw new ConflictError("node has no IPv4");
setAddresses(db, nodeId, v4, patch.ipv6);
}
return getNode(db, nodeId);
}
function deleteNode(db, nodeId) {
getNode(db, nodeId);
const linked = db.select({ c: count() }).from(aliases).where(eq2(aliases.target_node_id, nodeId)).get()?.c;
if (Number(linked ?? 0) > 0) {
throw new ConflictError("node has aliases; retarget or delete them first");
}
db.delete(nodes).where(eq2(nodes.id, nodeId)).run();
}
function mapAlias(row, targetHostname) {
return {
id: row.id,
zoneId: row.zone_id,
name: row.name,
purpose: row.purpose,
mode: row.mode,
targetNodeId: row.target_node_id,
targetHostname,
syncStatus: row.sync_status,
cfRecordId: row.cf_record_id,
lastError: row.last_error,
createdAt: row.created_at,
updatedAt: row.updated_at
};
}
function listAliases(db, filter = {}) {
let rows = db.select().from(aliases).all();
if (filter.zoneId) rows = rows.filter((r) => r.zone_id === filter.zoneId);
if (filter.purpose) rows = rows.filter((r) => r.purpose === filter.purpose);
if (filter.syncStatus)
rows = rows.filter((r) => r.sync_status === filter.syncStatus);
if (filter.q) {
const q = filter.q.toLowerCase();
rows = rows.filter((r) => r.name.toLowerCase().includes(q));
}
const nodeHost = new Map(
db.select().from(nodes).all().map((n) => [n.id, n.hostname])
);
return rows.map((r) => mapAlias(r, nodeHost.get(r.target_node_id))).sort((a, b) => a.name.localeCompare(b.name));
}
function getAlias(db, aliasId) {
const row = db.select().from(aliases).where(eq2(aliases.id, aliasId)).get();
if (!row) throw new NotFoundError(`alias ${aliasId}`);
const target = db.select().from(nodes).where(eq2(nodes.id, row.target_node_id)).get();
return mapAlias(row, target?.hostname);
}
function createAlias(db, input) {
getZone(db, input.zoneId);
getNode(db, input.targetNodeId);
const dup = db.select().from(aliases).where(and(eq2(aliases.zone_id, input.zoneId), eq2(aliases.name, input.name))).get();
if (dup) throw new ConflictError(`alias ${input.name} already exists`);
const aliasId = id("alias");
const ts = now();
db.insert(aliases).values({
id: aliasId,
zone_id: input.zoneId,
name: input.name,
purpose: input.purpose,
mode: input.mode,
target_node_id: input.targetNodeId,
sync_status: "pending",
created_at: ts,
updated_at: ts
}).run();
return getAlias(db, aliasId);
}
function updateAlias(db, aliasId, patch) {
getAlias(db, aliasId);
if (patch.targetNodeId) getNode(db, patch.targetNodeId);
const updates = {
updated_at: now()
};
if (patch.name !== void 0) updates.name = patch.name;
if (patch.purpose !== void 0) updates.purpose = patch.purpose;
if (patch.mode !== void 0) updates.mode = patch.mode;
if (patch.targetNodeId !== void 0)
updates.target_node_id = patch.targetNodeId;
if (patch.syncStatus !== void 0) updates.sync_status = patch.syncStatus;
if (patch.cfRecordId !== void 0) updates.cf_record_id = patch.cfRecordId;
if (patch.lastError !== void 0) updates.last_error = patch.lastError;
db.update(aliases).set(updates).where(eq2(aliases.id, aliasId)).run();
return getAlias(db, aliasId);
}
function deleteAlias(db, aliasId) {
getAlias(db, aliasId);
db.delete(aliases).where(eq2(aliases.id, aliasId)).run();
}
function createSyncJob(db, zoneId) {
getZone(db, zoneId);
const jobId = id("sync");
db.insert(syncJobs).values({
id: jobId,
zone_id: zoneId,
status: "pending",
created_at: now()
}).run();
return jobId;
}
function updateSyncJob(db, jobId, patch) {
const updates = {};
if (patch.status !== void 0) updates.status = patch.status;
if (patch.diffJson !== void 0) updates.diff_json = patch.diffJson;
if (patch.error !== void 0) updates.error = patch.error;
if (patch.finishedAt !== void 0) updates.finished_at = patch.finishedAt;
db.update(syncJobs).set(updates).where(eq2(syncJobs.id, jobId)).run();
}
function getSyncJob(db, jobId) {
const row = db.select().from(syncJobs).where(eq2(syncJobs.id, jobId)).get();
if (!row) throw new NotFoundError(`sync job ${jobId}`);
return {
id: row.id,
zoneId: row.zone_id,
status: row.status,
diff: row.diff_json ? JSON.parse(row.diff_json) : [],
error: row.error,
createdAt: row.created_at,
finishedAt: row.finished_at
};
}
function listSyncJobs(db, zoneId) {
return db.select().from(syncJobs).where(eq2(syncJobs.zone_id, zoneId)).orderBy(sql2`${syncJobs.created_at} DESC`).all().slice(0, 20).map((row) => ({
id: row.id,
zoneId: row.zone_id,
status: row.status,
diff: row.diff_json ? JSON.parse(row.diff_json) : [],
error: row.error,
createdAt: row.created_at,
finishedAt: row.finished_at
}));
}
function addSyncEvent(db, jobId, event) {
db.insert(syncEvents).values({
id: id("evt"),
job_id: jobId,
kind: event.kind,
record_name: event.recordName ?? null,
record_type: event.recordType ?? null,
detail: event.detail ?? null,
created_at: now()
}).run();
}
function listIgnoredOrphans(db, zoneId) {
return db.select().from(ignoredOrphans).where(eq2(ignoredOrphans.zone_id, zoneId)).all().map((r) => ({
id: r.id,
recordName: r.record_name,
recordType: r.record_type
}));
}
function ignoreOrphan(db, zoneId, recordName, recordType) {
getZone(db, zoneId);
db.insert(ignoredOrphans).values({
id: id("ign"),
zone_id: zoneId,
record_name: recordName,
record_type: recordType,
created_at: now()
}).onConflictDoNothing().run();
}
function unignoreOrphan(db, zoneId, recordName, recordType) {
db.delete(ignoredOrphans).where(
and(
eq2(ignoredOrphans.zone_id, zoneId),
eq2(ignoredOrphans.record_name, recordName),
eq2(ignoredOrphans.record_type, recordType)
)
).run();
}
function dashboardCounts(db) {
const nodeRows = db.select().from(nodes).all();
const aliasRows = db.select().from(aliases).all();
const zoneRows = db.select().from(zones).all();
let syncOk = 0;
let drift = 0;
let nodesWithoutIp = 0;
for (const n of nodeRows) {
if (n.sync_status === "ok") syncOk += 1;
if (n.sync_status === "drift" || n.sync_status === "missing") drift += 1;
const addrs = loadAddresses(db, n.id);
if (!addrs.some((a) => a.family === "v4")) nodesWithoutIp += 1;
}
let brokenAliases = 0;
for (const a of aliasRows) {
if (a.sync_status === "error" || a.sync_status === "missing")
brokenAliases += 1;
if (a.sync_status === "drift") drift += 1;
if (a.sync_status === "ok") syncOk += 1;
}
const lastSyncAt = zoneRows.map((z) => z.last_sync_at).filter(Boolean).sort().at(-1) ?? null;
return {
nodes: nodeRows.length,
aliases: aliasRows.length,
syncOk,
drift,
nodesWithoutIp,
brokenAliases,
lastSyncAt
};
}
export {
ConflictError,
NotFoundError,
addSyncEvent,
aliases,
appSettings,
createAlias,
createDb,
createMemoryDb,
createNode,
createSyncJob,
createZone,
dashboardCounts,
deleteAlias,
deleteNode,
deleteZone,
ensureCatalogLocations,
findLocationByCity,
getAlias,
getAppSettings,
getLocation,
getNode,
getSyncJob,
getZone,
healthCheck,
ignoreOrphan,
ignoredOrphans,
listAliases,
listIgnoredOrphans,
listLocations,
listNodes,
listSyncJobs,
listZones,
locations,
nodeAddresses,
nodes,
resolveDatabasePath,
runMigrations,
schema,
syncEvents,
syncJobs,
unignoreOrphan,
updateAlias,
updateAppSettings,
updateNode,
updateSyncJob,
updateZone,
zones
};