feat(api): enhance health check and DNS management
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 7s
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m9s
quality / api (push) Successful in 40s
CD / quality (push) Successful in 2m0s
CD / publish (push) Successful in 2m38s

- Added batch endpoint for health status queries to reduce the number of requests.
- Improved DNS record management with caching and invalidation mechanisms.
- Updated health check service to handle new configurations and improve performance.
- Refactored certificate service to utilize concurrency for checks, enhancing efficiency.
- Removed unused dependencies and optimized package configurations.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-09-03 16:16:41 +07:00
co-authored by Cursor
parent de3dbe8521
commit 1e9cc04a59
42 changed files with 6036 additions and 2836 deletions
+50 -2
View File
File diff suppressed because one or more lines are too long
+314 -1
View File
@@ -425,6 +425,9 @@ function createDb(databaseUrl) {
sqlite.pragma("journal_mode = WAL");
sqlite.pragma("synchronous = NORMAL");
sqlite.pragma("foreign_keys = ON");
sqlite.pragma("busy_timeout = 5000");
sqlite.pragma("cache_size = -16000");
sqlite.pragma("wal_autocheckpoint = 1000");
const db = drizzle(sqlite, { schema });
return { db, sqlite };
}
@@ -455,6 +458,9 @@ function runMigrations(sqlite) {
function healthCheck(sqlite) {
sqlite.prepare("SELECT 1").get();
}
function walCheckpointTruncate(sqlite) {
sqlite.pragma("wal_checkpoint(TRUNCATE)");
}
// src/errors.ts
var NotFoundError = class extends Error {
@@ -669,6 +675,7 @@ function touchVpsTrackerSync(db) {
// src/repos.ts
var repos_exports = {};
__export(repos_exports, {
LOG_RETENTION_LIMITS: () => LOG_RETENTION_LIMITS,
addDomainTags: () => addDomainTags,
aggregateGroupScopeHealthByIds: () => aggregateGroupScopeHealthByIds,
aggregateIpHealthByRefs: () => aggregateIpHealthByRefs,
@@ -723,6 +730,7 @@ __export(repos_exports, {
getServiceGroup: () => getServiceGroup,
getSubdomain: () => getSubdomain,
getSyncJob: () => getSyncJob,
hasWeightedBindings: () => hasWeightedBindings,
insertBinding: () => insertBinding,
insertDnsRecord: () => insertDnsRecord,
insertFailoverLog: () => insertFailoverLog,
@@ -734,11 +742,14 @@ __export(repos_exports, {
listAllDomains: () => listAllDomains,
listAllNodes: () => listAllNodes,
listAllSubdomains: () => listAllSubdomains,
listBindingIpHealthByBindingIds: () => listBindingIpHealthByBindingIds,
listBindingIps: () => listBindingIps,
listBindingIpsWithMeta: () => listBindingIpsWithMeta,
listBindingIpsWithMetaByBindingIds: () => listBindingIpsWithMetaByBindingIds,
listBindingNodes: () => listBindingNodes,
listBindingsByDomain: () => listBindingsByDomain,
listBindingsByService: () => listBindingsByService,
listBindingsByServiceIds: () => listBindingsByServiceIds,
listCertificates: () => listCertificates,
listDnsByDomain: () => listDnsByDomain,
listDnsRecords: () => listDnsRecords,
@@ -757,19 +768,25 @@ __export(repos_exports, {
listHealthProbeLogForService: () => listHealthProbeLogForService,
listIpHealthByServiceIds: () => listIpHealthByServiceIds,
listIpHealthStatus: () => listIpHealthStatus,
listIpHealthStatusByBindingIds: () => listIpHealthStatusByBindingIds,
listLatestLiveHealthByServiceIds: () => listLatestLiveHealthByServiceIds,
listNodes: () => listNodes,
listNotificationLog: () => listNotificationLog,
listOriginIpsForFqdn: () => listOriginIpsForFqdn,
listRecordsByBindingIds: () => listRecordsByBindingIds,
listRecordsForBinding: () => listRecordsForBinding,
listServiceGroups: () => listServiceGroups,
listServiceIpRows: () => listServiceIpRows,
listServiceIpRowsByServiceIds: () => listServiceIpRowsByServiceIds,
listServiceIps: () => listServiceIps,
listServices: () => listServices,
listServicesByGroup: () => listServicesByGroup,
listServicesByGroupIds: () => listServicesByGroupIds,
listSubdomainsByDomain: () => listSubdomainsByDomain,
listUngroupedServices: () => listUngroupedServices,
markDnsPendingDelete: () => markDnsPendingDelete,
mergeHealthAggregates: () => mergeHealthAggregates,
pruneLogs: () => pruneLogs,
pruneStaleIpHealthStatus: () => pruneStaleIpHealthStatus,
reorderServices: () => reorderServices,
replaceBindingIps: () => replaceBindingIps,
@@ -1135,6 +1152,29 @@ function listServices(db) {
function listServicesByGroup(db, groupId) {
return db.select().from(services).where(eq3(services.service_group_id, groupId)).orderBy(asc(services.sort_order), asc(services.name)).all();
}
function listServicesByGroupIds(db, groupIds) {
const result = /* @__PURE__ */ new Map();
if (groupIds.length === 0) return result;
const idList = sql2.join(
groupIds.map((id) => sql2`${id}`),
sql2`, `
);
const rows = db.all(sql2`
SELECT * FROM services
WHERE service_group_id IN (${idList})
ORDER BY service_group_id, sort_order, name
`);
for (const row of rows) {
const groupId = row.service_group_id;
let list = result.get(groupId);
if (!list) {
list = [];
result.set(groupId, list);
}
list.push(row);
}
return result;
}
function listUngroupedServices(db) {
return db.select().from(services).where(isNull(services.service_group_id)).orderBy(asc(services.sort_order), asc(services.name)).all();
}
@@ -1772,6 +1812,16 @@ function listAllBindings(db) {
ORDER BY d.zone_name, s.name
`).map((row) => enrichServiceBindingView(db, row));
}
function hasWeightedBindings(db) {
const row = db.get(sql2`
SELECT (EXISTS (
SELECT 1 FROM service_bindings WHERE lb_mode = 'weighted'
) OR EXISTS (
SELECT 1 FROM service_groups WHERE lb_mode = 'weighted'
)) AS n
`);
return Boolean(row?.n);
}
function listBindingsByDomain(db, domainId) {
return db.all(sql2`
SELECT ${sql2.raw(SERVICE_BINDING_SELECT_COLUMNS)}
@@ -1800,6 +1850,123 @@ function getBinding(db, id) {
if (!row) throw new NotFoundError(`service binding ${id}`);
return mapServiceBinding(row);
}
function listBindingsByServiceIds(db, serviceIds) {
const result = /* @__PURE__ */ new Map();
if (serviceIds.length === 0) return result;
const idList = sql2.join(
serviceIds.map((id) => sql2`${id}`),
sql2`, `
);
const rows = db.all(sql2`
SELECT sb.*, d.zone_name FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id
WHERE sb.service_id IN (${idList})
ORDER BY sb.id
`);
for (const row of rows) {
const { service_id, ...binding } = row;
let list = result.get(service_id);
if (!list) {
list = [];
result.set(service_id, list);
}
list.push({
...mapServiceBinding({ ...binding, service_id }),
zone_name: row.zone_name
});
}
return result;
}
function listBindingIpsWithMetaByBindingIds(db, bindingIds) {
const result = /* @__PURE__ */ new Map();
if (bindingIds.length === 0) return result;
const idList = sql2.join(
bindingIds.map((id) => sql2`${id}`),
sql2`, `
);
const rows = db.all(sql2`
SELECT binding_id, ip, weight, priority FROM service_binding_ips
WHERE binding_id IN (${idList})
ORDER BY binding_id, rowid
`);
for (const row of rows) {
let list = result.get(row.binding_id);
if (!list) {
list = [];
result.set(row.binding_id, list);
}
list.push({ ip: row.ip, weight: row.weight, priority: row.priority });
}
return result;
}
function listRecordsByBindingIds(db, bindingIds) {
const result = /* @__PURE__ */ new Map();
if (bindingIds.length === 0) return result;
const idList = sql2.join(
bindingIds.map((id) => sql2`${id}`),
sql2`, `
);
const rows = db.all(sql2`
SELECT dr.*, sbr.binding_id AS binding_id FROM dns_records dr
INNER JOIN service_binding_records sbr ON sbr.dns_record_id = dr.id
WHERE sbr.binding_id IN (${idList})
ORDER BY sbr.binding_id, dr.id
`);
for (const row of rows) {
const { binding_id, ...record } = row;
let list = result.get(binding_id);
if (!list) {
list = [];
result.set(binding_id, list);
}
list.push(record);
}
return result;
}
function listBindingIpHealthByBindingIds(db, bindingIds) {
const result = /* @__PURE__ */ new Map();
if (bindingIds.length === 0) return result;
const idList = sql2.join(
bindingIds.map((id) => sql2`${id}`),
sql2`, `
);
const rows = db.all(sql2`
SELECT ref_id, ip, status, latency_ms
FROM ip_health_status
WHERE scope = 'binding' AND ref_id IN (${idList})
`);
for (const row of rows) {
let byIp = result.get(row.ref_id);
if (!byIp) {
byIp = /* @__PURE__ */ new Map();
result.set(row.ref_id, byIp);
}
byIp.set(row.ip, { status: row.status, latency_ms: row.latency_ms });
}
return result;
}
function listServiceIpRowsByServiceIds(db, serviceIds) {
const result = /* @__PURE__ */ new Map();
if (serviceIds.length === 0) return result;
const idList = sql2.join(
serviceIds.map((id) => sql2`${id}`),
sql2`, `
);
const rows = db.all(sql2`
SELECT service_id, ip, enabled FROM service_ips
WHERE service_id IN (${idList})
ORDER BY service_id, rowid
`);
for (const row of rows) {
let list = result.get(row.service_id);
if (!list) {
list = [];
result.set(row.service_id, list);
}
list.push({ ip: row.ip, enabled: Boolean(row.enabled) });
}
return result;
}
function getBindingView(db, id) {
const rows = db.all(sql2`
SELECT ${sql2.raw(SERVICE_BINDING_SELECT_COLUMNS)}
@@ -1972,6 +2139,29 @@ function listIpHealthStatus(db, scope, refId) {
WHERE scope = ${scope} AND ref_id = ${refId}
`);
}
function listIpHealthStatusByBindingIds(db, bindingIds) {
const result = /* @__PURE__ */ new Map();
if (bindingIds.length === 0) return result;
const idList = sql2.join(
bindingIds.map((id) => sql2`${id}`),
sql2`, `
);
const rows = db.all(sql2`
SELECT scope, ref_id, ip, status, latency_ms, consecutive_failures,
last_checked_at, last_error, colo, provider
FROM ip_health_status
WHERE scope = 'binding' AND ref_id IN (${idList})
`);
for (const row of rows) {
let list = result.get(row.ref_id);
if (!list) {
list = [];
result.set(row.ref_id, list);
}
list.push(row);
}
return result;
}
var UNKNOWN_HEALTH = {
health_status: "unknown",
health_latency_ms: null
@@ -2127,6 +2317,83 @@ function listIpHealthByServiceIds(db, serviceIds) {
}
return result;
}
function parseIpHealthState(status) {
if (status === "up" || status === "down" || status === "degraded" || status === "unknown") {
return status;
}
return void 0;
}
function bestAliveIpState(statuses) {
if (statuses.some((status) => status === "up")) return "up";
if (statuses.some((status) => status === "degraded")) return "degraded";
if (statuses.some((status) => status === "down")) return "down";
return "unknown";
}
function listLatestLiveHealthByServiceIds(db, serviceIds) {
const result = /* @__PURE__ */ new Map();
if (serviceIds.length === 0) return result;
const idList = sql2.join(
serviceIds.map((id) => sql2`${id}`),
sql2`, `
);
const rows = db.all(sql2`
SELECT sb.service_id AS service_id,
l.ip AS ip,
l.provider AS provider,
l.status AS status,
l.latency_ms AS latency_ms,
l.error AS last_error,
l.colo AS colo,
l.checked_at AS checked_at
FROM health_probe_log l
INNER JOIN service_bindings sb
ON l.scope = 'binding' AND l.ref_id = sb.id
INNER JOIN (
SELECT sb2.service_id AS service_id,
l2.ip AS ip,
l2.provider AS provider,
MAX(l2.id) AS max_id
FROM health_probe_log l2
INNER JOIN service_bindings sb2
ON l2.scope = 'binding' AND l2.ref_id = sb2.id
WHERE sb2.service_id IN (${idList})
GROUP BY sb2.service_id, l2.ip, l2.provider
) latest
ON latest.max_id = l.id
`);
const byServiceIp = /* @__PURE__ */ new Map();
for (const row of rows) {
const status = parseIpHealthState(row.status);
if (!status) continue;
const key = `${row.service_id}\0${row.ip}`;
const probe = {
ip: row.ip,
status,
latency_ms: row.latency_ms,
last_checked_at: row.checked_at,
last_error: row.last_error,
provider: normalizeStatusProvider(row.provider),
colo: row.colo
};
const bucket = byServiceIp.get(key);
if (bucket) bucket.probes.push(probe);
else {
byServiceIp.set(key, {
serviceId: row.service_id,
ip: row.ip,
probes: [probe]
});
}
}
for (const { serviceId, ip, probes } of byServiceIp.values()) {
const status = bestAliveIpState(probes.map((probe) => probe.status));
const preferred = probes.find((probe) => probe.status === status) ?? probes[0];
const list = result.get(serviceId) ?? [];
list.push({ ...preferred, ip, status });
result.set(serviceId, list);
}
return result;
}
function mergeHealthAggregates(parts) {
const rank = {
unknown: 0,
@@ -2563,6 +2830,51 @@ function listNotificationLog(db, limit = 50) {
LIMIT ${limit}
`);
}
var LOG_RETENTION_LIMITS = {
notification_log: 500,
failover_log: 1e3,
audit_log: 2e3,
sync_jobs: 500,
health_probe_log: 2e3
};
function pruneLogs(db) {
let deleted = 0;
const byId = [
{ table: "notification_log", limit: LOG_RETENTION_LIMITS.notification_log },
{ table: "failover_log", limit: LOG_RETENTION_LIMITS.failover_log },
{ table: "health_probe_log", limit: LOG_RETENTION_LIMITS.health_probe_log }
];
for (const { table, limit } of byId) {
const res = db.run(sql2`
DELETE FROM ${sql2.identifier(table)}
WHERE id NOT IN (
SELECT id FROM ${sql2.identifier(table)}
ORDER BY id DESC
LIMIT ${limit}
)
`);
deleted += res.changes;
}
const audit = db.run(sql2`
DELETE FROM audit_log
WHERE id NOT IN (
SELECT id FROM audit_log
ORDER BY created_at DESC, id DESC
LIMIT ${LOG_RETENTION_LIMITS.audit_log}
)
`);
deleted += audit.changes;
const jobs = db.run(sql2`
DELETE FROM sync_jobs
WHERE id NOT IN (
SELECT id FROM sync_jobs
ORDER BY created_at DESC, id DESC
LIMIT ${LOG_RETENTION_LIMITS.sync_jobs}
)
`);
deleted += jobs.changes;
return deleted;
}
function insertFailoverLog(db, input) {
for (const entry of input.entries) {
db.insert(failoverLog).values({
@@ -2623,5 +2935,6 @@ export {
subdomains,
syncJobs,
touchVpsTrackerSync,
updateAppSettings
updateAppSettings,
walCheckpointTruncate
};
+8
View File
@@ -23,6 +23,9 @@ export function createDb(databaseUrl: string): { db: Db; sqlite: Sqlite } {
sqlite.pragma("journal_mode = WAL");
sqlite.pragma("synchronous = NORMAL");
sqlite.pragma("foreign_keys = ON");
sqlite.pragma("busy_timeout = 5000");
sqlite.pragma("cache_size = -16000");
sqlite.pragma("wal_autocheckpoint = 1000");
const db = drizzle(sqlite, { schema });
return { db, sqlite };
}
@@ -63,3 +66,8 @@ export function runMigrations(sqlite: Sqlite): void {
export function healthCheck(sqlite: Sqlite): void {
sqlite.prepare("SELECT 1").get();
}
/** Truncate the WAL back into the main DB file; safe to run periodically. */
export function walCheckpointTruncate(sqlite: Sqlite): void {
sqlite.pragma("wal_checkpoint(TRUNCATE)");
}
+278
View File
@@ -648,6 +648,34 @@ export function listServicesByGroup(db: Db, groupId: number): Service[] {
.all() as Service[];
}
/** Batch: services for many groups in one query (hot path: /service-groups). */
export function listServicesByGroupIds(
db: Db,
groupIds: number[],
): Map<number, Service[]> {
const result = new Map<number, Service[]>();
if (groupIds.length === 0) return result;
const idList = sql.join(
groupIds.map((id) => sql`${id}`),
sql`, `,
);
const rows = db.all<Service & { service_group_id: number }>(sql`
SELECT * FROM services
WHERE service_group_id IN (${idList})
ORDER BY service_group_id, sort_order, name
`);
for (const row of rows) {
const groupId = row.service_group_id;
let list = result.get(groupId);
if (!list) {
list = [];
result.set(groupId, list);
}
list.push(row);
}
return result;
}
export function listUngroupedServices(db: Db): Service[] {
return db
.select()
@@ -1762,6 +1790,18 @@ export function listAllBindings(db: Db): ServiceBindingView[] {
.map((row) => enrichServiceBindingView(db, row));
}
/** Cheap predicate for the weighted-dns scheduler: any weighted row at all? */
export function hasWeightedBindings(db: Db): boolean {
const row = db.get<{ n: number }>(sql`
SELECT (EXISTS (
SELECT 1 FROM service_bindings WHERE lb_mode = 'weighted'
) OR EXISTS (
SELECT 1 FROM service_groups WHERE lb_mode = 'weighted'
)) AS n
`);
return Boolean(row?.n);
}
export function listBindingsByDomain(db: Db, domainId: number): ServiceBindingView[] {
return db
.all<Omit<ServiceBindingView, "target_ips" | "target_ip_weights" | "target_ip_priorities">>(sql`
@@ -1799,6 +1839,163 @@ export function getBinding(db: Db, id: number): ServiceBinding {
return mapServiceBinding(row);
}
/** Batch: bindings for many services in one query (hot path: /service-groups). */
export function listBindingsByServiceIds(
db: Db,
serviceIds: number[],
): Map<number, Array<ServiceBinding & { zone_name: string }>> {
const result = new Map<
number,
Array<ServiceBinding & { zone_name: string }>
>();
if (serviceIds.length === 0) return result;
const idList = sql.join(
serviceIds.map((id) => sql`${id}`),
sql`, `,
);
const rows = db.all<
typeof serviceBindings.$inferSelect & { service_id: number; zone_name: string }
>(sql`
SELECT sb.*, d.zone_name FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id
WHERE sb.service_id IN (${idList})
ORDER BY sb.id
`);
for (const row of rows) {
const { service_id, ...binding } = row;
let list = result.get(service_id);
if (!list) {
list = [];
result.set(service_id, list);
}
list.push({
...mapServiceBinding({ ...binding, service_id }),
zone_name: row.zone_name,
});
}
return result;
}
/** Batch: binding IPs with metadata for many bindings in one query. */
export function listBindingIpsWithMetaByBindingIds(
db: Db,
bindingIds: number[],
): Map<number, BindingIpMeta[]> {
const result = new Map<number, BindingIpMeta[]>();
if (bindingIds.length === 0) return result;
const idList = sql.join(
bindingIds.map((id) => sql`${id}`),
sql`, `,
);
const rows = db.all<{
binding_id: number;
ip: string;
weight: number;
priority: number;
}>(sql`
SELECT binding_id, ip, weight, priority FROM service_binding_ips
WHERE binding_id IN (${idList})
ORDER BY binding_id, rowid
`);
for (const row of rows) {
let list = result.get(row.binding_id);
if (!list) {
list = [];
result.set(row.binding_id, list);
}
list.push({ ip: row.ip, weight: row.weight, priority: row.priority });
}
return result;
}
/** Batch: dns records linked to many bindings in one query. */
export function listRecordsByBindingIds(
db: Db,
bindingIds: number[],
): Map<number, DnsRecord[]> {
const result = new Map<number, DnsRecord[]>();
if (bindingIds.length === 0) return result;
const idList = sql.join(
bindingIds.map((id) => sql`${id}`),
sql`, `,
);
const rows = db.all<DnsRecord & { binding_id: number }>(sql`
SELECT dr.*, sbr.binding_id AS binding_id FROM dns_records dr
INNER JOIN service_binding_records sbr ON sbr.dns_record_id = dr.id
WHERE sbr.binding_id IN (${idList})
ORDER BY sbr.binding_id, dr.id
`);
for (const row of rows) {
const { binding_id, ...record } = row;
let list = result.get(binding_id);
if (!list) {
list = [];
result.set(binding_id, list);
}
list.push(record);
}
return result;
}
/** Batch: binding-scope IP health status for many bindings in one query. */
export function listBindingIpHealthByBindingIds(
db: Db,
bindingIds: number[],
): Map<number, Map<string, { status: string; latency_ms: number | null }>> {
const result = new Map<number, Map<string, { status: string; latency_ms: number | null }>>();
if (bindingIds.length === 0) return result;
const idList = sql.join(
bindingIds.map((id) => sql`${id}`),
sql`, `,
);
const rows = db.all<{
ref_id: number;
ip: string;
status: string;
latency_ms: number | null;
}>(sql`
SELECT ref_id, ip, status, latency_ms
FROM ip_health_status
WHERE scope = 'binding' AND ref_id IN (${idList})
`);
for (const row of rows) {
let byIp = result.get(row.ref_id);
if (!byIp) {
byIp = new Map();
result.set(row.ref_id, byIp);
}
byIp.set(row.ip, { status: row.status, latency_ms: row.latency_ms });
}
return result;
}
/** Batch: service IP rows for many services in one query. */
export function listServiceIpRowsByServiceIds(
db: Db,
serviceIds: number[],
): Map<number, ServiceIpRow[]> {
const result = new Map<number, ServiceIpRow[]>();
if (serviceIds.length === 0) return result;
const idList = sql.join(
serviceIds.map((id) => sql`${id}`),
sql`, `,
);
const rows = db.all<{ service_id: number; ip: string; enabled: number }>(sql`
SELECT service_id, ip, enabled FROM service_ips
WHERE service_id IN (${idList})
ORDER BY service_id, rowid
`);
for (const row of rows) {
let list = result.get(row.service_id);
if (!list) {
list = [];
result.set(row.service_id, list);
}
list.push({ ip: row.ip, enabled: Boolean(row.enabled) });
}
return result;
}
export function getBindingView(db: Db, id: number): ServiceBindingView {
const rows = db.all<Omit<ServiceBindingView, "target_ips" | "target_ip_weights" | "target_ip_priorities">>(sql`
SELECT ${sql.raw(SERVICE_BINDING_SELECT_COLUMNS)}
@@ -2113,6 +2310,34 @@ export function listIpHealthStatus(
`);
}
/** Batch: latest IP health rows for many bindings in one query. */
export function listIpHealthStatusByBindingIds(
db: Db,
bindingIds: number[],
): Map<number, IpHealthStatus[]> {
const result = new Map<number, IpHealthStatus[]>();
if (bindingIds.length === 0) return result;
const idList = sql.join(
bindingIds.map((id) => sql`${id}`),
sql`, `,
);
const rows = db.all<IpHealthStatus>(sql`
SELECT scope, ref_id, ip, status, latency_ms, consecutive_failures,
last_checked_at, last_error, colo, provider
FROM ip_health_status
WHERE scope = 'binding' AND ref_id IN (${idList})
`);
for (const row of rows) {
let list = result.get(row.ref_id);
if (!list) {
list = [];
result.set(row.ref_id, list);
}
list.push(row);
}
return result;
}
export type HealthAggregate = {
health_status: IpHealthState;
health_latency_ms: number | null;
@@ -3171,6 +3396,59 @@ export function listNotificationLog(
`);
}
// --- Log retention ---
export const LOG_RETENTION_LIMITS = {
notification_log: 500,
failover_log: 1000,
audit_log: 2000,
sync_jobs: 500,
health_probe_log: 2000,
} as const;
/** Keep only the newest N rows per log table; returns total deleted rows. */
export function pruneLogs(db: Db): number {
let deleted = 0;
// Integer autoincrement PKs — order by id.
const byId: Array<{ table: string; limit: number }> = [
{ table: "notification_log", limit: LOG_RETENTION_LIMITS.notification_log },
{ table: "failover_log", limit: LOG_RETENTION_LIMITS.failover_log },
{ table: "health_probe_log", limit: LOG_RETENTION_LIMITS.health_probe_log },
];
for (const { table, limit } of byId) {
const res = db.run(sql`
DELETE FROM ${sql.identifier(table)}
WHERE id NOT IN (
SELECT id FROM ${sql.identifier(table)}
ORDER BY id DESC
LIMIT ${limit}
)
`);
deleted += res.changes;
}
// audit_log has TEXT (uuid) PK — order by created_at, id.
const audit = db.run(sql`
DELETE FROM audit_log
WHERE id NOT IN (
SELECT id FROM audit_log
ORDER BY created_at DESC, id DESC
LIMIT ${LOG_RETENTION_LIMITS.audit_log}
)
`);
deleted += audit.changes;
// sync_jobs has TEXT PK — order by created_at, id.
const jobs = db.run(sql`
DELETE FROM sync_jobs
WHERE id NOT IN (
SELECT id FROM sync_jobs
ORDER BY created_at DESC, id DESC
LIMIT ${LOG_RETENTION_LIMITS.sync_jobs}
)
`);
deleted += jobs.changes;
return deleted;
}
export type FailoverLogAction = "added" | "removed";
export type FailoverLogRow = {