feat(health): добавить Globalping и мультивыбор источников проб
CD / update-wiki (push) Successful in 8s
quality / commitlint (push) Skipped
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 54s
quality / api (push) Successful in 46s
CD / quality (push) Successful in 1m49s
CD / publish (push) Successful in 1m40s

Несколько источников проб сразу и правило агрегации на сервисе вместо XOR Local/Cloudflare.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-19 18:32:18 +07:00
co-authored by Cursor
parent 4c4908558b
commit b9bea44dce
31 changed files with 2671 additions and 271 deletions
+278 -3
View File
@@ -1,7 +1,7 @@
import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
import Database from 'better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import { AuditSourceApp, AuditSeverity, AuditTargetType, AuditLogEntry, HealthWorkerStatus, LbMode, HealthCheckType, HealthCheckProvider, IpHealthState, HealthCheckScope, ServiceBinding, Domain, Group, OriginHealthCheck, ServiceNode, Service, ServiceGroup, Subdomain, DnsRecord, ServiceBindingView, Certificate, GroupWithStats, IpHealthStatus, SyncJob, DomainListItem, HealthCheckTarget } from '@cfdm/shared';
import { AuditSourceApp, AuditSeverity, AuditTargetType, AuditLogEntry, HealthWorkerStatus, LbMode, HealthCheckType, HealthCheckProvider, HealthCheckAggregate, IpHealthState, HealthStatusProvider, HealthCheckScope, ServiceBinding, Domain, Group, OriginHealthCheck, ServiceNode, Service, ServiceGroup, Subdomain, DnsRecord, ServiceBindingView, Certificate, GroupWithStats, IpHealthStatus, SyncJob, DomainListItem, HealthCheckTarget } from '@cfdm/shared';
declare const groups: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
name: "groups";
@@ -599,6 +599,44 @@ declare const serviceGroups: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
}, {}, {
length: number | undefined;
}>;
health_check_providers: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "health_check_providers";
tableName: "service_groups";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
health_check_aggregate: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "health_check_aggregate";
tableName: "service_groups";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
created_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "created_at";
tableName: "service_groups";
@@ -1537,6 +1575,44 @@ declare const serviceBindings: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
}, {}, {
length: number | undefined;
}>;
health_check_providers: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "health_check_providers";
tableName: "service_bindings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
health_check_aggregate: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "health_check_aggregate";
tableName: "service_bindings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
routing_strategy: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "routing_strategy";
tableName: "service_bindings";
@@ -3460,6 +3536,61 @@ declare const appSettings: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
}, {}, {
length: number | undefined;
}>;
globalping_token: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "globalping_token";
tableName: "app_settings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
globalping_locations: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "globalping_locations";
tableName: "app_settings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
globalping_limit: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "globalping_limit";
tableName: "app_settings";
dataType: "number";
columnType: "SQLiteInteger";
data: number;
driverParam: number;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
created_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "created_at";
tableName: "app_settings";
@@ -5151,6 +5282,44 @@ declare const schema: {
}, {}, {
length: number | undefined;
}>;
health_check_providers: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "health_check_providers";
tableName: "service_groups";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
health_check_aggregate: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "health_check_aggregate";
tableName: "service_groups";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
created_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "created_at";
tableName: "service_groups";
@@ -6089,6 +6258,44 @@ declare const schema: {
}, {}, {
length: number | undefined;
}>;
health_check_providers: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "health_check_providers";
tableName: "service_bindings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
health_check_aggregate: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "health_check_aggregate";
tableName: "service_bindings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
routing_strategy: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "routing_strategy";
tableName: "service_bindings";
@@ -8012,6 +8219,61 @@ declare const schema: {
}, {}, {
length: number | undefined;
}>;
globalping_token: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "globalping_token";
tableName: "app_settings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
globalping_locations: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "globalping_locations";
tableName: "app_settings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
globalping_limit: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "globalping_limit";
tableName: "app_settings";
dataType: "number";
columnType: "SQLiteInteger";
data: number;
driverParam: number;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
created_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "created_at";
tableName: "app_settings";
@@ -9176,6 +9438,9 @@ type AppSettingsDto = {
healthWorkerError: string | null;
healthWorkerDeployedAt: string | null;
healthWorkerLastIngestAt: string | null;
globalpingTokenSet: boolean;
globalpingLocations: string;
globalpingLimit: number;
} & HealthEngineSettings;
type AppSettingsPatch = {
vpsTrackerUrl?: string;
@@ -9194,6 +9459,9 @@ type AppSettingsPatch = {
healthWorkerError?: string | null;
healthWorkerDeployedAt?: string | null;
healthWorkerLastIngestAt?: string | null;
globalpingToken?: string;
globalpingLocations?: string;
globalpingLimit?: number;
};
type HealthEngineFallbacks = HealthEngineSettings & {
healthWorkerUrl: string;
@@ -9206,6 +9474,9 @@ declare function getAppSettingsSecrets(db: Db): {
vpsTrackerSyncEnabled: boolean;
healthWorkerUrl: string;
healthWorkerToken: string;
globalpingToken: string;
globalpingLocations: string;
globalpingLimit: number;
};
declare function updateAppSettings(db: Db, patch: AppSettingsPatch, fallbacks?: HealthEngineFallbacks): AppSettingsDto;
declare function touchVpsTrackerSync(db: Db): void;
@@ -9294,6 +9565,8 @@ interface ServiceGroupLbPatch {
health_check_timeout_ms?: number;
health_check_verify_tls?: boolean;
health_check_provider?: HealthCheckProvider;
health_check_providers?: HealthCheckProvider[];
health_check_aggregate?: HealthCheckAggregate;
}
declare function createServiceGroup(db: Db, name: string, groupType: string, icon: string | null, domain: string | null, lbPatch?: ServiceGroupLbPatch): ServiceGroup;
declare function updateServiceGroup(db: Db, id: number, name: string, groupType: string, icon: string | null, domain: string | null, lbPatch?: ServiceGroupLbPatch): ServiceGroup;
@@ -9402,6 +9675,8 @@ interface BindingLbPatch {
health_check_timeout_ms?: number;
health_check_verify_tls?: boolean;
health_check_provider?: HealthCheckProvider;
health_check_providers?: HealthCheckProvider[];
health_check_aggregate?: HealthCheckAggregate;
}
declare function updateBindingLbConfig(db: Db, bindingId: number, patch: BindingLbPatch): void;
declare function setBindingCnameTarget(db: Db, bindingId: number, target: string | null): void;
@@ -9453,7 +9728,7 @@ type ServiceIpHealthRow = {
latency_ms: number | null;
last_checked_at: string | null;
last_error: string | null;
provider: HealthCheckProvider;
provider: HealthStatusProvider;
colo: string | null;
};
/** Per-IP binding-scope health, worst status if the same IP is on several bindings. */
@@ -9462,7 +9737,7 @@ declare function mergeHealthAggregates(parts: Array<HealthAggregate | undefined
declare function getIpHealthStatusRow(db: Db, scope: HealthCheckScope, refId: number, ip: string): IpHealthStatus | null;
declare function upsertIpHealthStatus(db: Db, scope: HealthCheckScope, refId: number, ip: string, status: string, latencyMs: number | null, consecutiveFailures: number, lastError: string | null, consecutiveSuccesses?: number, extras?: {
colo?: string | null;
provider?: HealthCheckProvider;
provider?: HealthStatusProvider;
}): void;
declare function deleteIpHealthStatusForRef(db: Db, scope: HealthCheckScope, refId: number): void;
declare function deleteIpHealthStatusForIp(db: Db, scope: HealthCheckScope, refId: number, ip: string): void;
+126 -22
View File
@@ -53,6 +53,8 @@ var serviceGroups = sqliteTable("service_groups", {
health_check_timeout_ms: integer("health_check_timeout_ms").notNull().default(3e3),
health_check_verify_tls: integer("health_check_verify_tls", { mode: "boolean" }).notNull().default(false),
health_check_provider: text("health_check_provider").notNull().default("local"),
health_check_providers: text("health_check_providers").notNull().default('["local"]'),
health_check_aggregate: text("health_check_aggregate").notNull().default("majority"),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
});
@@ -119,6 +121,8 @@ var serviceBindings = sqliteTable(
mode: "boolean"
}).notNull().default(false),
health_check_provider: text("health_check_provider").notNull().default("local"),
health_check_providers: text("health_check_providers").notNull().default('["local"]'),
health_check_aggregate: text("health_check_aggregate").notNull().default("majority"),
routing_strategy: text("routing_strategy").notNull().default("round_robin"),
operation_version: integer("operation_version").notNull().default(0),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
@@ -286,6 +290,9 @@ var appSettings = sqliteTable("app_settings", {
health_worker_error: text("health_worker_error"),
health_worker_deployed_at: text("health_worker_deployed_at"),
health_worker_last_ingest_at: text("health_worker_last_ingest_at"),
globalping_token: text("globalping_token"),
globalping_locations: text("globalping_locations"),
globalping_limit: integer("globalping_limit"),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
});
@@ -577,7 +584,10 @@ function toDto(row, fallbacks) {
healthWorkerError: row.health_worker_error?.trim() || null,
healthWorkerDeployedAt: row.health_worker_deployed_at ?? null,
healthWorkerLastIngestAt: row.health_worker_last_ingest_at ?? null,
healthWorkerStatus: workerStatus(row, env.healthWorkerUrl)
healthWorkerStatus: workerStatus(row, env.healthWorkerUrl),
globalpingTokenSet: Boolean(row.globalping_token?.trim()),
globalpingLocations: row.globalping_locations?.trim() || "World",
globalpingLimit: row.globalping_limit == null || Number.isNaN(row.globalping_limit) || row.globalping_limit < 1 ? 3 : Math.min(10, row.globalping_limit)
};
}
function getAppSettings(db, fallbacks) {
@@ -593,12 +603,16 @@ function getAppSettings(db, fallbacks) {
}
function getAppSettingsSecrets(db) {
const row = db.select().from(appSettings).where(eq2(appSettings.id, SETTINGS_ID)).get();
const limit = row?.globalping_limit;
return {
vpsTrackerUrl: row?.vps_tracker_url?.trim() ?? "",
vpsTrackerIntegrationToken: row?.vps_tracker_integration_token?.trim() ?? "",
vpsTrackerSyncEnabled: Boolean(row?.vps_tracker_sync_enabled),
healthWorkerUrl: row?.health_worker_url?.trim() ?? "",
healthWorkerToken: row?.health_worker_token?.trim() ?? ""
healthWorkerToken: row?.health_worker_token?.trim() ?? "",
globalpingToken: row?.globalping_token?.trim() ?? "",
globalpingLocations: row?.globalping_locations?.trim() || "World",
globalpingLimit: limit == null || Number.isNaN(limit) || limit < 1 ? 3 : Math.min(10, limit)
};
}
function updateAppSettings(db, patch, fallbacks) {
@@ -624,6 +638,9 @@ function updateAppSettings(db, patch, fallbacks) {
health_worker_error: patch.healthWorkerError !== void 0 ? patch.healthWorkerError?.trim() || null : current.health_worker_error,
health_worker_deployed_at: patch.healthWorkerDeployedAt !== void 0 ? patch.healthWorkerDeployedAt : current.health_worker_deployed_at,
health_worker_last_ingest_at: patch.healthWorkerLastIngestAt !== void 0 ? patch.healthWorkerLastIngestAt : current.health_worker_last_ingest_at,
globalping_token: patch.globalpingToken !== void 0 && patch.globalpingToken.trim() !== "" ? patch.globalpingToken : current.globalping_token,
globalping_locations: patch.globalpingLocations !== void 0 ? patch.globalpingLocations.trim() || "World" : current.globalping_locations,
globalping_limit: patch.globalpingLimit !== void 0 ? Math.min(10, Math.max(1, patch.globalpingLimit)) : current.globalping_limit,
updated_at: (/* @__PURE__ */ new Date()).toISOString()
}).where(eq2(appSettings.id, SETTINGS_ID)).run();
return getAppSettings(db, fallbacks);
@@ -771,7 +788,15 @@ __export(repos_exports, {
upsertIpHealthStatus: () => upsertIpHealthStatus,
upsertSubdomain: () => upsertSubdomain
});
import { dnsRecordNamesMatch, isIpLiteral } from "@cfdm/shared";
import {
derivePrimaryProvider,
dnsRecordNamesMatch,
isIpLiteral,
parseHealthAggregate,
parseHealthProviders,
serializeHealthProviders,
normalizeStatusProvider
} from "@cfdm/shared";
import { and as and2, asc, count, eq as eq3, isNull, like, notInArray, or as or2, sql as sql2 } from "drizzle-orm";
function listGroups(db) {
return db.select().from(groups).orderBy(asc(groups.name)).all();
@@ -1158,8 +1183,58 @@ function deleteService(db, id) {
const result = db.delete(services).where(eq3(services.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`service ${id}`);
}
function normalizeHealthProvider(value) {
return value === "cloudflare" ? "cloudflare" : "local";
function healthProviderColumns(patch) {
const out = {};
if (patch.health_check_providers !== void 0) {
const list = parseHealthProviders(patch.health_check_providers);
out.health_check_providers = serializeHealthProviders(list);
out.health_check_provider = derivePrimaryProvider(list);
} else if (patch.health_check_provider !== void 0) {
const list = parseHealthProviders(null, patch.health_check_provider);
out.health_check_providers = serializeHealthProviders(list);
out.health_check_provider = derivePrimaryProvider(list);
}
if (patch.health_check_aggregate !== void 0) {
out.health_check_aggregate = parseHealthAggregate(
patch.health_check_aggregate
);
}
return out;
}
function mapHealthFields(row) {
const providers = parseHealthProviders(
row.health_check_providers,
row.health_check_provider
);
return {
health_check_providers: providers,
health_check_provider: derivePrimaryProvider(providers),
health_check_aggregate: parseHealthAggregate(row.health_check_aggregate)
};
}
function mapServiceBinding(row) {
return {
id: row.id,
domain_id: row.domain_id,
service_id: row.service_id,
hostname: row.hostname,
cname_target: row.cname_target,
dns_record_id: row.dns_record_id,
lb_mode: row.lb_mode,
health_check_enabled: row.health_check_enabled,
health_check_type: row.health_check_type,
health_check_port: row.health_check_port,
health_check_path: row.health_check_path,
health_check_expected_status: row.health_check_expected_status,
health_check_interval_sec: row.health_check_interval_sec,
health_check_timeout_ms: row.health_check_timeout_ms,
health_check_verify_tls: row.health_check_verify_tls,
...mapHealthFields(row),
routing_strategy: row.routing_strategy,
operation_version: row.operation_version,
created_at: row.created_at,
updated_at: row.updated_at
};
}
function mapServiceGroup(row) {
return {
@@ -1178,7 +1253,7 @@ function mapServiceGroup(row) {
health_check_interval_sec: row.health_check_interval_sec,
health_check_timeout_ms: row.health_check_timeout_ms,
health_check_verify_tls: row.health_check_verify_tls,
health_check_provider: normalizeHealthProvider(row.health_check_provider),
...mapHealthFields(row),
created_at: row.created_at,
updated_at: row.updated_at
};
@@ -1206,7 +1281,11 @@ function createServiceGroup(db, name, groupType, icon, domain, lbPatch) {
health_check_interval_sec: lbPatch?.health_check_interval_sec ?? 30,
health_check_timeout_ms: lbPatch?.health_check_timeout_ms ?? 3e3,
health_check_verify_tls: lbPatch?.health_check_verify_tls ?? false,
health_check_provider: lbPatch?.health_check_provider ?? "local"
...healthProviderColumns({
health_check_provider: lbPatch?.health_check_provider ?? "local",
health_check_providers: lbPatch?.health_check_providers,
health_check_aggregate: lbPatch?.health_check_aggregate ?? "majority"
})
}).returning({ id: serviceGroups.id }).get().id;
return getServiceGroup(db, id);
}
@@ -1236,8 +1315,7 @@ function updateServiceGroup(db, id, name, groupType, icon, domain, lbPatch) {
update.health_check_timeout_ms = lbPatch.health_check_timeout_ms;
if (lbPatch.health_check_verify_tls !== void 0)
update.health_check_verify_tls = lbPatch.health_check_verify_tls;
if (lbPatch.health_check_provider !== void 0)
update.health_check_provider = lbPatch.health_check_provider;
Object.assign(update, healthProviderColumns(lbPatch));
}
const result = db.update(serviceGroups).set(update).where(eq3(serviceGroups.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`service group ${id}`);
@@ -1547,8 +1625,7 @@ function updateBindingLbConfig(db, bindingId, patch) {
update.health_check_timeout_ms = patch.health_check_timeout_ms;
if (patch.health_check_verify_tls !== void 0)
update.health_check_verify_tls = patch.health_check_verify_tls;
if (patch.health_check_provider !== void 0)
update.health_check_provider = patch.health_check_provider;
Object.assign(update, healthProviderColumns(patch));
db.update(serviceBindings).set(update).where(eq3(serviceBindings.id, bindingId)).run();
}
function setBindingCnameTarget(db, bindingId, target) {
@@ -1607,7 +1684,8 @@ function dnsRecordMatchesHostname(recordName, hostname, zoneName) {
var SERVICE_BINDING_SELECT_COLUMNS = `sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
sb.lb_mode, sb.health_check_enabled, sb.health_check_type, sb.health_check_port,
sb.health_check_path, sb.health_check_expected_status, sb.health_check_interval_sec,
sb.health_check_timeout_ms, sb.health_check_verify_tls, sb.health_check_provider, sb.cname_target,
sb.health_check_timeout_ms, sb.health_check_verify_tls, sb.health_check_provider,
sb.health_check_providers, sb.health_check_aggregate, sb.cname_target,
d.zone_name, d.group_id, g.name AS group_name,
s.name AS service_name, s.slug AS service_slug,
dr.content AS target_ip, dr.sync_status,
@@ -1655,6 +1733,7 @@ function enrichServiceBindingView(db, row) {
return {
...row,
cname_target: row.cname_target ?? null,
...mapHealthFields(row),
target_ips,
target_ip: target_ips[0] ?? null,
target_ip_weights,
@@ -1695,7 +1774,7 @@ function listBindingsByService(db, serviceId) {
function getBinding(db, id) {
const row = db.select().from(serviceBindings).where(eq3(serviceBindings.id, id)).get();
if (!row) throw new NotFoundError(`service binding ${id}`);
return row;
return mapServiceBinding(row);
}
function getBindingView(db, id) {
const rows = db.all(sql2`
@@ -1718,7 +1797,8 @@ function findBinding(db, serviceId, domainId, hostname) {
eq3(serviceBindings.hostname, hostname)
)
).get();
return row ?? null;
if (!row) return null;
return mapServiceBinding(row);
}
function insertBinding(db, domainId, serviceId, hostname, dnsRecordId) {
const id = db.insert(serviceBindings).values({
@@ -1744,7 +1824,7 @@ function setBindingDnsRecordId(db, bindingId, dnsRecordId) {
}).where(eq3(serviceBindings.id, bindingId)).run();
}
function bindingsToRemove(db, serviceId, keepIds) {
const all = db.select().from(serviceBindings).where(eq3(serviceBindings.service_id, serviceId)).all();
const all = db.select().from(serviceBindings).where(eq3(serviceBindings.service_id, serviceId)).all().map(mapServiceBinding);
return all.filter((b) => !keepIds.includes(b.id));
}
function deleteBindingsExcept(db, serviceId, keepIds) {
@@ -1975,7 +2055,7 @@ function listIpHealthByServiceIds(db, serviceIds) {
latency_ms: parsed.health_latency_ms,
last_checked_at: row.last_checked_at,
last_error: row.last_error,
provider: normalizeHealthProvider(row.provider),
provider: normalizeStatusProvider(row.provider),
colo: row.colo
});
result.set(row.service_id, list);
@@ -2101,6 +2181,8 @@ function listHealthCheckTargets(db) {
sb.health_check_expected_status AS expected_status,
sb.health_check_timeout_ms AS timeout_ms,
sb.health_check_verify_tls AS verify_tls,
sb.health_check_providers AS providers_json,
COALESCE(sb.health_check_aggregate, 'majority') AS aggregate,
COALESCE(sb.health_check_provider, 'local') AS provider
FROM service_binding_ips sbi
JOIN service_bindings sb ON sb.id = sbi.binding_id
@@ -2116,6 +2198,8 @@ function listHealthCheckTargets(db) {
sg.health_check_expected_status AS expected_status,
sg.health_check_timeout_ms AS timeout_ms,
sg.health_check_verify_tls AS verify_tls,
sg.health_check_providers AS providers_json,
COALESCE(sg.health_check_aggregate, 'majority') AS aggregate,
COALESCE(sg.health_check_provider, 'local') AS provider
FROM service_binding_ips sbi
JOIN service_bindings sb ON sb.id = sbi.binding_id
@@ -2137,6 +2221,8 @@ function listHealthCheckTargets(db) {
sg.health_check_expected_status AS expected_status,
sg.health_check_timeout_ms AS timeout_ms,
sg.health_check_verify_tls AS verify_tls,
sg.health_check_providers AS providers_json,
COALESCE(sg.health_check_aggregate, 'majority') AS aggregate,
COALESCE(sg.health_check_provider, 'local') AS provider
FROM service_binding_ips sbi
JOIN service_bindings sb ON sb.id = sbi.binding_id
@@ -2158,6 +2244,8 @@ function listHealthCheckTargets(db) {
sb.health_check_expected_status AS expected_status,
sb.health_check_timeout_ms AS timeout_ms,
sb.health_check_verify_tls AS verify_tls,
sb.health_check_providers AS providers_json,
COALESCE(sb.health_check_aggregate, 'majority') AS aggregate,
COALESCE(sb.health_check_provider, 'local') AS provider
FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id
@@ -2176,6 +2264,8 @@ function listHealthCheckTargets(db) {
sg.health_check_expected_status AS expected_status,
sg.health_check_timeout_ms AS timeout_ms,
sg.health_check_verify_tls AS verify_tls,
sg.health_check_providers AS providers_json,
COALESCE(sg.health_check_aggregate, 'majority') AS aggregate,
COALESCE(sg.health_check_provider, 'local') AS provider
FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id
@@ -2195,11 +2285,25 @@ function listHealthCheckTargets(db) {
...groupInheritedBindingTargets,
...cnameBindingTargets,
...groupInheritedCnameBindingTargets
].map((t) => ({
...t,
verify_tls: Boolean(t.verify_tls),
provider: normalizeHealthProvider(t.provider)
}));
].map((t) => {
const row = t;
const providers = parseHealthProviders(row.providers_json, row.provider);
return {
scope: row.scope,
ref_id: row.ref_id,
ip: row.ip,
hostname: row.hostname,
type: row.type,
port: row.port,
path: row.path,
expected_status: row.expected_status,
timeout_ms: row.timeout_ms,
verify_tls: Boolean(row.verify_tls),
providers,
aggregate: parseHealthAggregate(row.aggregate),
provider: derivePrimaryProvider(providers)
};
});
}
function listDomainTags(db, domainId) {
return db.select({ tag: domainTags.tag }).from(domainTags).where(eq3(domainTags.domain_id, domainId)).all().map((r) => r.tag);
@@ -2329,7 +2433,7 @@ function listHealthProbeLogForService(db, serviceId, limit = 50) {
`);
return rows.map((row) => ({
...row,
provider: normalizeHealthProvider(row.provider),
provider: parseHealthProviders(null, row.provider)[0] ?? "local",
ok: Boolean(row.ok)
}));
}
@@ -0,0 +1,21 @@
ALTER TABLE service_bindings ADD COLUMN health_check_providers TEXT;
ALTER TABLE service_bindings ADD COLUMN health_check_aggregate TEXT NOT NULL DEFAULT 'majority';
UPDATE service_bindings
SET health_check_providers = CASE
WHEN health_check_provider = 'cloudflare' THEN '["cloudflare"]'
ELSE '["local"]'
END
WHERE health_check_providers IS NULL;
ALTER TABLE service_groups ADD COLUMN health_check_providers TEXT;
ALTER TABLE service_groups ADD COLUMN health_check_aggregate TEXT NOT NULL DEFAULT 'majority';
UPDATE service_groups
SET health_check_providers = CASE
WHEN health_check_provider = 'cloudflare' THEN '["cloudflare"]'
ELSE '["local"]'
END
WHERE health_check_providers IS NULL;
ALTER TABLE app_settings ADD COLUMN globalping_token TEXT;
ALTER TABLE app_settings ADD COLUMN globalping_locations TEXT;
ALTER TABLE app_settings ADD COLUMN globalping_limit INTEGER;
+143 -22
View File
@@ -5,10 +5,12 @@ import type {
DomainListItem,
Group,
GroupWithStats,
HealthCheckAggregate,
HealthCheckProvider,
HealthCheckScope,
HealthCheckTarget,
HealthCheckType,
HealthStatusProvider,
IpHealthState,
IpHealthStatus,
LbMode,
@@ -21,7 +23,15 @@ import type {
Subdomain,
SyncJob,
} from "@cfdm/shared";
import { dnsRecordNamesMatch, isIpLiteral } from "@cfdm/shared";
import {
derivePrimaryProvider,
dnsRecordNamesMatch,
isIpLiteral,
parseHealthAggregate,
parseHealthProviders,
serializeHealthProviders,
normalizeStatusProvider,
} from "@cfdm/shared";
import { and, asc, count, eq, isNull, like, notInArray, or, sql } from "drizzle-orm";
import type { Db } from "./client.js";
import { ConflictError, NotFoundError } from "./errors.js";
@@ -770,8 +780,82 @@ export function deleteService(db: Db, id: number): void {
// --- Service Groups ---
function normalizeHealthProvider(value: unknown): HealthCheckProvider {
return value === "cloudflare" ? "cloudflare" : "local";
function healthProviderColumns(patch: {
health_check_provider?: HealthCheckProvider;
health_check_providers?: HealthCheckProvider[];
health_check_aggregate?: HealthCheckAggregate;
}): {
health_check_provider?: string;
health_check_providers?: string;
health_check_aggregate?: string;
} {
const out: {
health_check_provider?: string;
health_check_providers?: string;
health_check_aggregate?: string;
} = {};
if (patch.health_check_providers !== undefined) {
const list = parseHealthProviders(patch.health_check_providers);
out.health_check_providers = serializeHealthProviders(list);
out.health_check_provider = derivePrimaryProvider(list);
} else if (patch.health_check_provider !== undefined) {
const list = parseHealthProviders(null, patch.health_check_provider);
out.health_check_providers = serializeHealthProviders(list);
out.health_check_provider = derivePrimaryProvider(list);
}
if (patch.health_check_aggregate !== undefined) {
out.health_check_aggregate = parseHealthAggregate(
patch.health_check_aggregate,
);
}
return out;
}
function mapHealthFields(row: {
health_check_provider?: unknown;
health_check_providers?: unknown;
health_check_aggregate?: unknown;
}): {
health_check_provider: HealthCheckProvider;
health_check_providers: HealthCheckProvider[];
health_check_aggregate: HealthCheckAggregate;
} {
const providers = parseHealthProviders(
row.health_check_providers,
row.health_check_provider,
);
return {
health_check_providers: providers,
health_check_provider: derivePrimaryProvider(providers),
health_check_aggregate: parseHealthAggregate(row.health_check_aggregate),
};
}
function mapServiceBinding(
row: typeof serviceBindings.$inferSelect,
): ServiceBinding {
return {
id: row.id,
domain_id: row.domain_id,
service_id: row.service_id,
hostname: row.hostname,
cname_target: row.cname_target,
dns_record_id: row.dns_record_id,
lb_mode: row.lb_mode as LbMode,
health_check_enabled: row.health_check_enabled,
health_check_type: row.health_check_type as HealthCheckType,
health_check_port: row.health_check_port,
health_check_path: row.health_check_path,
health_check_expected_status: row.health_check_expected_status,
health_check_interval_sec: row.health_check_interval_sec,
health_check_timeout_ms: row.health_check_timeout_ms,
health_check_verify_tls: row.health_check_verify_tls,
...mapHealthFields(row),
routing_strategy: row.routing_strategy as LbMode,
operation_version: row.operation_version,
created_at: row.created_at,
updated_at: row.updated_at,
};
}
function mapServiceGroup(row: typeof serviceGroups.$inferSelect): ServiceGroup {
@@ -791,7 +875,7 @@ function mapServiceGroup(row: typeof serviceGroups.$inferSelect): ServiceGroup {
health_check_interval_sec: row.health_check_interval_sec,
health_check_timeout_ms: row.health_check_timeout_ms,
health_check_verify_tls: row.health_check_verify_tls,
health_check_provider: normalizeHealthProvider(row.health_check_provider),
...mapHealthFields(row),
created_at: row.created_at,
updated_at: row.updated_at,
};
@@ -827,6 +911,8 @@ export interface ServiceGroupLbPatch {
health_check_timeout_ms?: number;
health_check_verify_tls?: boolean;
health_check_provider?: HealthCheckProvider;
health_check_providers?: HealthCheckProvider[];
health_check_aggregate?: HealthCheckAggregate;
}
export function createServiceGroup(
@@ -853,7 +939,11 @@ export function createServiceGroup(
health_check_interval_sec: lbPatch?.health_check_interval_sec ?? 30,
health_check_timeout_ms: lbPatch?.health_check_timeout_ms ?? 3000,
health_check_verify_tls: lbPatch?.health_check_verify_tls ?? false,
health_check_provider: lbPatch?.health_check_provider ?? "local",
...healthProviderColumns({
health_check_provider: lbPatch?.health_check_provider ?? "local",
health_check_providers: lbPatch?.health_check_providers,
health_check_aggregate: lbPatch?.health_check_aggregate ?? "majority",
}),
})
.returning({ id: serviceGroups.id })
.get()!.id;
@@ -894,8 +984,7 @@ export function updateServiceGroup(
update.health_check_timeout_ms = lbPatch.health_check_timeout_ms;
if (lbPatch.health_check_verify_tls !== undefined)
update.health_check_verify_tls = lbPatch.health_check_verify_tls;
if (lbPatch.health_check_provider !== undefined)
update.health_check_provider = lbPatch.health_check_provider;
Object.assign(update, healthProviderColumns(lbPatch));
}
const result = db
.update(serviceGroups)
@@ -1439,6 +1528,8 @@ export interface BindingLbPatch {
health_check_timeout_ms?: number;
health_check_verify_tls?: boolean;
health_check_provider?: HealthCheckProvider;
health_check_providers?: HealthCheckProvider[];
health_check_aggregate?: HealthCheckAggregate;
}
export function updateBindingLbConfig(
@@ -1469,8 +1560,7 @@ export function updateBindingLbConfig(
update.health_check_timeout_ms = patch.health_check_timeout_ms;
if (patch.health_check_verify_tls !== undefined)
update.health_check_verify_tls = patch.health_check_verify_tls;
if (patch.health_check_provider !== undefined)
update.health_check_provider = patch.health_check_provider;
Object.assign(update, healthProviderColumns(patch));
db.update(serviceBindings)
.set(update)
.where(eq(serviceBindings.id, bindingId))
@@ -1578,7 +1668,8 @@ function dnsRecordMatchesHostname(
const SERVICE_BINDING_SELECT_COLUMNS = `sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
sb.lb_mode, sb.health_check_enabled, sb.health_check_type, sb.health_check_port,
sb.health_check_path, sb.health_check_expected_status, sb.health_check_interval_sec,
sb.health_check_timeout_ms, sb.health_check_verify_tls, sb.health_check_provider, sb.cname_target,
sb.health_check_timeout_ms, sb.health_check_verify_tls, sb.health_check_provider,
sb.health_check_providers, sb.health_check_aggregate, sb.cname_target,
d.zone_name, d.group_id, g.name AS group_name,
s.name AS service_name, s.slug AS service_slug,
dr.content AS target_ip, dr.sync_status,
@@ -1642,6 +1733,7 @@ function enrichServiceBindingView(
return {
...row,
cname_target: row.cname_target ?? null,
...mapHealthFields(row),
target_ips,
target_ip: target_ips[0] ?? null,
target_ip_weights,
@@ -1694,7 +1786,7 @@ export function getBinding(db: Db, id: number): ServiceBinding {
.where(eq(serviceBindings.id, id))
.get();
if (!row) throw new NotFoundError(`service binding ${id}`);
return row as ServiceBinding;
return mapServiceBinding(row);
}
export function getBindingView(db: Db, id: number): ServiceBindingView {
@@ -1728,7 +1820,8 @@ export function findBinding(
),
)
.get();
return (row as ServiceBinding) ?? null;
if (!row) return null;
return mapServiceBinding(row);
}
export function insertBinding(
@@ -1792,7 +1885,8 @@ export function bindingsToRemove(
.select()
.from(serviceBindings)
.where(eq(serviceBindings.service_id, serviceId))
.all() as ServiceBinding[];
.all()
.map(mapServiceBinding);
return all.filter((b) => !keepIds.includes(b.id));
}
@@ -2124,7 +2218,7 @@ export type ServiceIpHealthRow = {
latency_ms: number | null;
last_checked_at: string | null;
last_error: string | null;
provider: HealthCheckProvider;
provider: HealthStatusProvider;
colo: string | null;
};
@@ -2175,7 +2269,7 @@ export function listIpHealthByServiceIds(
latency_ms: parsed.health_latency_ms,
last_checked_at: row.last_checked_at,
last_error: row.last_error,
provider: normalizeHealthProvider(row.provider),
provider: normalizeStatusProvider(row.provider),
colo: row.colo,
});
result.set(row.service_id, list);
@@ -2243,7 +2337,7 @@ export function upsertIpHealthStatus(
consecutiveFailures: number,
lastError: string | null,
consecutiveSuccesses = 0,
extras?: { colo?: string | null; provider?: HealthCheckProvider },
extras?: { colo?: string | null; provider?: HealthStatusProvider },
): void {
const colo = extras?.colo ?? null;
const provider = extras?.provider ?? "local";
@@ -2359,6 +2453,8 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
sb.health_check_expected_status AS expected_status,
sb.health_check_timeout_ms AS timeout_ms,
sb.health_check_verify_tls AS verify_tls,
sb.health_check_providers AS providers_json,
COALESCE(sb.health_check_aggregate, 'majority') AS aggregate,
COALESCE(sb.health_check_provider, 'local') AS provider
FROM service_binding_ips sbi
JOIN service_bindings sb ON sb.id = sbi.binding_id
@@ -2378,6 +2474,8 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
sg.health_check_expected_status AS expected_status,
sg.health_check_timeout_ms AS timeout_ms,
sg.health_check_verify_tls AS verify_tls,
sg.health_check_providers AS providers_json,
COALESCE(sg.health_check_aggregate, 'majority') AS aggregate,
COALESCE(sg.health_check_provider, 'local') AS provider
FROM service_binding_ips sbi
JOIN service_bindings sb ON sb.id = sbi.binding_id
@@ -2404,6 +2502,8 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
sg.health_check_expected_status AS expected_status,
sg.health_check_timeout_ms AS timeout_ms,
sg.health_check_verify_tls AS verify_tls,
sg.health_check_providers AS providers_json,
COALESCE(sg.health_check_aggregate, 'majority') AS aggregate,
COALESCE(sg.health_check_provider, 'local') AS provider
FROM service_binding_ips sbi
JOIN service_bindings sb ON sb.id = sbi.binding_id
@@ -2427,6 +2527,8 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
sb.health_check_expected_status AS expected_status,
sb.health_check_timeout_ms AS timeout_ms,
sb.health_check_verify_tls AS verify_tls,
sb.health_check_providers AS providers_json,
COALESCE(sb.health_check_aggregate, 'majority') AS aggregate,
COALESCE(sb.health_check_provider, 'local') AS provider
FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id
@@ -2448,6 +2550,8 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
sg.health_check_expected_status AS expected_status,
sg.health_check_timeout_ms AS timeout_ms,
sg.health_check_verify_tls AS verify_tls,
sg.health_check_providers AS providers_json,
COALESCE(sg.health_check_aggregate, 'majority') AS aggregate,
COALESCE(sg.health_check_provider, 'local') AS provider
FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id
@@ -2468,11 +2572,28 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
...groupInheritedBindingTargets,
...cnameBindingTargets,
...groupInheritedCnameBindingTargets,
].map((t) => ({
...t,
verify_tls: Boolean(t.verify_tls),
provider: normalizeHealthProvider(t.provider),
}));
].map((t) => {
const row = t as HealthCheckTarget & {
providers_json?: string | null;
aggregate?: string | null;
};
const providers = parseHealthProviders(row.providers_json, row.provider);
return {
scope: row.scope,
ref_id: row.ref_id,
ip: row.ip,
hostname: row.hostname,
type: row.type,
port: row.port,
path: row.path,
expected_status: row.expected_status,
timeout_ms: row.timeout_ms,
verify_tls: Boolean(row.verify_tls),
providers,
aggregate: parseHealthAggregate(row.aggregate),
provider: derivePrimaryProvider(providers),
};
});
}
// --- Domain tags ---
@@ -2759,7 +2880,7 @@ export function listHealthProbeLogForService(
`);
return rows.map((row) => ({
...row,
provider: normalizeHealthProvider(row.provider),
provider: parseHealthProviders(null, row.provider)[0] ?? "local",
ok: Boolean(row.ok),
}));
}
+11
View File
@@ -65,6 +65,8 @@ export const serviceGroups = sqliteTable("service_groups", {
.notNull()
.default(false),
health_check_provider: text("health_check_provider").notNull().default("local"),
health_check_providers: text("health_check_providers").notNull().default('["local"]'),
health_check_aggregate: text("health_check_aggregate").notNull().default("majority"),
created_at: text("created_at")
.notNull()
.default(sql`datetime('now')`),
@@ -169,6 +171,12 @@ export const serviceBindings = sqliteTable(
health_check_provider: text("health_check_provider")
.notNull()
.default("local"),
health_check_providers: text("health_check_providers")
.notNull()
.default('["local"]'),
health_check_aggregate: text("health_check_aggregate")
.notNull()
.default("majority"),
routing_strategy: text("routing_strategy").notNull().default("round_robin"),
operation_version: integer("operation_version").notNull().default(0),
created_at: text("created_at")
@@ -398,6 +406,9 @@ export const appSettings = sqliteTable("app_settings", {
health_worker_error: text("health_worker_error"),
health_worker_deployed_at: text("health_worker_deployed_at"),
health_worker_last_ingest_at: text("health_worker_last_ingest_at"),
globalping_token: text("globalping_token"),
globalping_locations: text("globalping_locations"),
globalping_limit: integer("globalping_limit"),
created_at: text("created_at")
.notNull()
.default(sql`datetime('now')`),
+37
View File
@@ -28,6 +28,9 @@ export type AppSettingsDto = {
healthWorkerError: string | null;
healthWorkerDeployedAt: string | null;
healthWorkerLastIngestAt: string | null;
globalpingTokenSet: boolean;
globalpingLocations: string;
globalpingLimit: number;
} & HealthEngineSettings;
export type AppSettingsPatch = {
@@ -47,6 +50,9 @@ export type AppSettingsPatch = {
healthWorkerError?: string | null;
healthWorkerDeployedAt?: string | null;
healthWorkerLastIngestAt?: string | null;
globalpingToken?: string;
globalpingLocations?: string;
globalpingLimit?: number;
};
export type HealthEngineFallbacks = HealthEngineSettings & {
@@ -118,6 +124,14 @@ function toDto(
healthWorkerDeployedAt: row.health_worker_deployed_at ?? null,
healthWorkerLastIngestAt: row.health_worker_last_ingest_at ?? null,
healthWorkerStatus: workerStatus(row, env.healthWorkerUrl),
globalpingTokenSet: Boolean(row.globalping_token?.trim()),
globalpingLocations: row.globalping_locations?.trim() || "World",
globalpingLimit:
row.globalping_limit == null ||
Number.isNaN(row.globalping_limit) ||
row.globalping_limit < 1
? 3
: Math.min(10, row.globalping_limit),
};
}
@@ -146,12 +160,16 @@ export function getAppSettingsSecrets(db: Db): {
vpsTrackerSyncEnabled: boolean;
healthWorkerUrl: string;
healthWorkerToken: string;
globalpingToken: string;
globalpingLocations: string;
globalpingLimit: number;
} {
const row = db
.select()
.from(appSettings)
.where(eq(appSettings.id, SETTINGS_ID))
.get();
const limit = row?.globalping_limit;
return {
vpsTrackerUrl: row?.vps_tracker_url?.trim() ?? "",
vpsTrackerIntegrationToken:
@@ -159,6 +177,12 @@ export function getAppSettingsSecrets(db: Db): {
vpsTrackerSyncEnabled: Boolean(row?.vps_tracker_sync_enabled),
healthWorkerUrl: row?.health_worker_url?.trim() ?? "",
healthWorkerToken: row?.health_worker_token?.trim() ?? "",
globalpingToken: row?.globalping_token?.trim() ?? "",
globalpingLocations: row?.globalping_locations?.trim() || "World",
globalpingLimit:
limit == null || Number.isNaN(limit) || limit < 1
? 3
: Math.min(10, limit),
};
}
@@ -249,6 +273,19 @@ export function updateAppSettings(
patch.healthWorkerLastIngestAt !== undefined
? patch.healthWorkerLastIngestAt
: current.health_worker_last_ingest_at,
globalping_token:
patch.globalpingToken !== undefined &&
patch.globalpingToken.trim() !== ""
? patch.globalpingToken
: current.globalping_token,
globalping_locations:
patch.globalpingLocations !== undefined
? patch.globalpingLocations.trim() || "World"
: current.globalping_locations,
globalping_limit:
patch.globalpingLimit !== undefined
? Math.min(10, Math.max(1, patch.globalpingLimit))
: current.globalping_limit,
updated_at: new Date().toISOString(),
})
.where(eq(appSettings.id, SETTINGS_ID))