feat(api, web): enhance health check and domain management features
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m3s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m48s
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 6s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped

- Integrated domain monitoring routes and bulk update functionality for domains in the API.
- Improved health check service to include domain monitoring and logging of health status changes.
- Updated web components to reflect health status with new HealthCheckBadge and enhanced domain filtering options.
- Refactored domain service to support bulk updates and improved domain management capabilities.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-17 02:25:12 +07:00
co-authored by Cursor
parent dd167a4fec
commit 64585ccd47
38 changed files with 4199 additions and 677 deletions
+1266 -3
View File
File diff suppressed because it is too large Load Diff
+232 -8
View File
@@ -62,6 +62,7 @@ var domains = sqliteTable("domains", {
cf_zone_id: text("cf_zone_id").notNull(),
status: text("status").notNull().default("active"),
cert_monitoring: text("cert_monitoring").notNull().default("auto"),
environment: text("environment"),
last_synced_at: text("last_synced_at"),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
@@ -196,6 +197,48 @@ var appSettings = sqliteTable("app_settings", {
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
});
var domainTags = sqliteTable(
"domain_tags",
{
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
tag: text("tag").notNull()
},
(t) => [primaryKey({ columns: [t.domain_id, t.tag] })]
);
var domainMonitors = sqliteTable("domain_monitors", {
id: integer("id").primaryKey({ autoIncrement: true }),
domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
hostname: text("hostname").notNull(),
type: text("type").notNull().default("http"),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
interval_sec: integer("interval_sec").notNull().default(60),
timeout_ms: integer("timeout_ms").notNull().default(5e3),
path: text("path"),
expected_status: integer("expected_status"),
last_status: text("last_status").notNull().default("unknown"),
last_latency_ms: integer("last_latency_ms"),
last_checked_at: text("last_checked_at"),
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')`)
});
var domainMonitorResults = sqliteTable("domain_monitor_results", {
id: integer("id").primaryKey({ autoIncrement: true }),
monitor_id: integer("monitor_id").notNull().references(() => domainMonitors.id, { onDelete: "cascade" }),
status: text("status").notNull(),
latency_ms: integer("latency_ms"),
error: text("error"),
checked_at: text("checked_at").notNull().default(sql`datetime('now')`)
});
var notificationLog = sqliteTable("notification_log", {
id: integer("id").primaryKey({ autoIncrement: true }),
kind: text("kind").notNull(),
ref_type: text("ref_type").notNull(),
ref_id: integer("ref_id"),
title: text("title").notNull(),
message: text("message").notNull(),
created_at: text("created_at").notNull().default(sql`datetime('now')`)
});
var schema = {
groups,
services,
@@ -211,7 +254,11 @@ var schema = {
certificates,
syncJobs,
ipHealthStatus,
appSettings
appSettings,
domainTags,
domainMonitors,
domainMonitorResults,
notificationLog
};
// src/client.ts
@@ -367,9 +414,11 @@ function getAppSwitcher(db) {
// src/repos.ts
var repos_exports = {};
__export(repos_exports, {
addDomainTags: () => addDomainTags,
bindingsToRemove: () => bindingsToRemove,
countCertificatesByStatus: () => countCertificatesByStatus,
createDomain: () => createDomain,
createDomainMonitor: () => createDomainMonitor,
createGroup: () => createGroup,
createService: () => createService,
createServiceGroup: () => createServiceGroup,
@@ -380,6 +429,7 @@ __export(repos_exports, {
deleteCertificatesNotIn: () => deleteCertificatesNotIn,
deleteDnsRecord: () => deleteDnsRecord,
deleteDomain: () => deleteDomain,
deleteDomainMonitor: () => deleteDomainMonitor,
deleteGroup: () => deleteGroup,
deleteIpHealthStatusForIp: () => deleteIpHealthStatusForIp,
deleteIpHealthStatusForRef: () => deleteIpHealthStatusForRef,
@@ -396,6 +446,7 @@ __export(repos_exports, {
getCertificate: () => getCertificate,
getDnsRecord: () => getDnsRecord,
getDomain: () => getDomain,
getDomainMonitor: () => getDomainMonitor,
getGroup: () => getGroup,
getGroupWithStats: () => getGroupWithStats,
getIpHealthStatusRow: () => getIpHealthStatusRow,
@@ -405,6 +456,7 @@ __export(repos_exports, {
getSyncJob: () => getSyncJob,
insertBinding: () => insertBinding,
insertDnsRecord: () => insertDnsRecord,
insertNotificationLog: () => insertNotificationLog,
linkBindingRecord: () => linkBindingRecord,
linkGroupDnsRecord: () => linkGroupDnsRecord,
listAllBindings: () => listAllBindings,
@@ -417,12 +469,18 @@ __export(repos_exports, {
listCertificates: () => listCertificates,
listDnsByDomain: () => listDnsByDomain,
listDnsRecords: () => listDnsRecords,
listDomainMonitorResults: () => listDomainMonitorResults,
listDomainMonitorResultsForDomain: () => listDomainMonitorResultsForDomain,
listDomainMonitors: () => listDomainMonitors,
listDomainTags: () => listDomainTags,
listDomains: () => listDomains,
listDomainsEnriched: () => listDomainsEnriched,
listEnabledDomainMonitors: () => listEnabledDomainMonitors,
listGroupDnsRecords: () => listGroupDnsRecords,
listGroups: () => listGroups,
listHealthCheckTargets: () => listHealthCheckTargets,
listIpHealthStatus: () => listIpHealthStatus,
listNotificationLog: () => listNotificationLog,
listRecordsForBinding: () => listRecordsForBinding,
listServiceGroups: () => listServiceGroups,
listServiceIps: () => listServiceIps,
@@ -439,6 +497,7 @@ __export(repos_exports, {
setBindingDnsRecordId: () => setBindingDnsRecordId,
setDnsSyncStatus: () => setDnsSyncStatus,
setDomainLastSynced: () => setDomainLastSynced,
setDomainTags: () => setDomainTags,
setServiceEnabled: () => setServiceEnabled,
setServiceGroup: () => setServiceGroup,
setServiceGroupEnabled: () => setServiceGroupEnabled,
@@ -449,6 +508,7 @@ __export(repos_exports, {
updateBindingLbConfig: () => updateBindingLbConfig,
updateDnsFields: () => updateDnsFields,
updateDomain: () => updateDomain,
updateDomainMonitorResult: () => updateDomainMonitorResult,
updateGroup: () => updateGroup,
updateService: () => updateService,
updateServiceGroup: () => updateServiceGroup,
@@ -497,14 +557,61 @@ function listDomains(db, groupId) {
}
function listDomainsEnriched(db, groupId) {
const base = groupId != null ? sql2`WHERE d.group_id = ${groupId}` : sql2``;
return db.all(sql2`
const rows = db.all(sql2`
SELECT d.*, g.name AS group_name,
(SELECT COUNT(*) FROM service_bindings sb WHERE sb.domain_id = d.id) AS service_count
(SELECT COUNT(*) FROM service_bindings sb WHERE sb.domain_id = d.id) AS service_count,
COALESCE((
SELECT CASE
WHEN MAX(CASE
WHEN ihs.status = 'down' THEN 3
WHEN ihs.status = 'degraded' THEN 2
WHEN ihs.status = 'up' THEN 1
ELSE 0
END) = 3 THEN 'down'
WHEN MAX(CASE
WHEN ihs.status = 'down' THEN 3
WHEN ihs.status = 'degraded' THEN 2
WHEN ihs.status = 'up' THEN 1
ELSE 0
END) = 2 THEN 'degraded'
WHEN MAX(CASE
WHEN ihs.status = 'down' THEN 3
WHEN ihs.status = 'degraded' THEN 2
WHEN ihs.status = 'up' THEN 1
ELSE 0
END) = 1 THEN 'up'
ELSE 'unknown'
END
FROM ip_health_status ihs
INNER JOIN service_bindings sb ON ihs.scope = 'binding' AND ihs.ref_id = sb.id
WHERE sb.domain_id = d.id
), 'unknown') AS health_status,
(
SELECT MAX(ihs.latency_ms)
FROM ip_health_status ihs
INNER JOIN service_bindings sb ON ihs.scope = 'binding' AND ihs.ref_id = sb.id
WHERE sb.domain_id = d.id
) AS health_latency_ms,
(
SELECT GROUP_CONCAT(dt.tag, ',')
FROM domain_tags dt
WHERE dt.domain_id = d.id
) AS tags_json
FROM domains d
LEFT JOIN groups g ON g.id = d.group_id
${base}
ORDER BY d.zone_name ASC
`);
return rows.map((row) => {
const { tags_json, ...rest } = row;
return {
...rest,
environment: rest.environment ?? null,
health_status: rest.health_status ?? "unknown",
health_latency_ms: rest.health_latency_ms ?? null,
tags: tags_json ? tags_json.split(",").map((t) => t.trim()).filter(Boolean) : []
};
});
}
function findDomainByZoneName(db, zoneName) {
const rows = db.all(sql2`
@@ -525,14 +632,18 @@ function createDomain(db, groupId, zoneName, cfZoneId) {
}).returning({ id: domains.id }).get().id;
return getDomain(db, id);
}
function updateDomain(db, id, groupId, status, certMonitoring) {
function updateDomain(db, id, patch) {
const existing = getDomain(db, id);
const updates = {
group_id: groupId,
status,
group_id: patch.group_id !== void 0 ? patch.group_id : existing.group_id,
status: patch.status !== void 0 ? patch.status : existing.status,
updated_at: sql2`datetime('now')`
};
if (certMonitoring !== void 0) {
updates.cert_monitoring = certMonitoring;
if (patch.cert_monitoring !== void 0) {
updates.cert_monitoring = patch.cert_monitoring;
}
if (patch.environment !== void 0) {
updates.environment = patch.environment;
}
const result = db.update(domains).set(updates).where(eq2(domains.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`domain ${id}`);
@@ -1320,6 +1431,115 @@ function listHealthCheckTargets(db) {
...groupInheritedCnameBindingTargets
];
}
function listDomainTags(db, domainId) {
return db.select({ tag: domainTags.tag }).from(domainTags).where(eq2(domainTags.domain_id, domainId)).all().map((r) => r.tag);
}
function setDomainTags(db, domainId, tags) {
db.delete(domainTags).where(eq2(domainTags.domain_id, domainId)).run();
const unique = [...new Set(tags.map((t) => t.trim()).filter(Boolean))];
for (const tag of unique) {
db.insert(domainTags).values({ domain_id: domainId, tag }).run();
}
}
function addDomainTags(db, domainId, tags) {
const unique = [...new Set(tags.map((t) => t.trim()).filter(Boolean))];
for (const tag of unique) {
db.run(sql2`
INSERT INTO domain_tags (domain_id, tag)
VALUES (${domainId}, ${tag})
ON CONFLICT(domain_id, tag) DO NOTHING
`);
}
}
function listDomainMonitors(db, domainId) {
return db.select().from(domainMonitors).where(eq2(domainMonitors.domain_id, domainId)).orderBy(asc(domainMonitors.id)).all();
}
function listEnabledDomainMonitors(db) {
return db.select().from(domainMonitors).where(eq2(domainMonitors.enabled, true)).all();
}
function getDomainMonitor(db, id) {
const row = db.select().from(domainMonitors).where(eq2(domainMonitors.id, id)).get();
if (!row) throw new NotFoundError(`domain_monitor ${id}`);
return row;
}
function createDomainMonitor(db, domainId, input) {
const id = db.insert(domainMonitors).values({
domain_id: domainId,
hostname: input.hostname.trim(),
type: input.type,
enabled: input.enabled ?? true,
interval_sec: input.interval_sec ?? 60,
timeout_ms: input.timeout_ms ?? 5e3,
path: input.path ?? null,
expected_status: input.expected_status ?? null
}).returning({ id: domainMonitors.id }).get().id;
return getDomainMonitor(db, id);
}
function deleteDomainMonitor(db, id) {
const result = db.delete(domainMonitors).where(eq2(domainMonitors.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`domain_monitor ${id}`);
}
function updateDomainMonitorResult(db, monitorId, status, latencyMs, error) {
db.update(domainMonitors).set({
last_status: status,
last_latency_ms: latencyMs,
last_checked_at: sql2`datetime('now')`,
last_error: error,
updated_at: sql2`datetime('now')`
}).where(eq2(domainMonitors.id, monitorId)).run();
db.insert(domainMonitorResults).values({
monitor_id: monitorId,
status,
latency_ms: latencyMs,
error
}).run();
db.run(sql2`
DELETE FROM domain_monitor_results
WHERE monitor_id = ${monitorId}
AND id NOT IN (
SELECT id FROM domain_monitor_results
WHERE monitor_id = ${monitorId}
ORDER BY checked_at DESC, id DESC
LIMIT 100
)
`);
}
function listDomainMonitorResults(db, monitorId, limit = 50) {
return db.all(sql2`
SELECT id, monitor_id, status, latency_ms, error, checked_at
FROM domain_monitor_results
WHERE monitor_id = ${monitorId}
ORDER BY checked_at DESC, id DESC
LIMIT ${limit}
`);
}
function listDomainMonitorResultsForDomain(db, domainId, limit = 50) {
return db.all(sql2`
SELECT r.id, r.monitor_id, m.hostname, m.type, r.status, r.latency_ms, r.error, r.checked_at
FROM domain_monitor_results r
JOIN domain_monitors m ON m.id = r.monitor_id
WHERE m.domain_id = ${domainId}
ORDER BY r.checked_at DESC, r.id DESC
LIMIT ${limit}
`);
}
function insertNotificationLog(db, kind, refType, refId, title, message) {
db.insert(notificationLog).values({
kind,
ref_type: refType,
ref_id: refId,
title,
message
}).run();
}
function listNotificationLog(db, limit = 50) {
return db.all(sql2`
SELECT id, kind, ref_type, ref_id, title, message, created_at
FROM notification_log
ORDER BY created_at DESC, id DESC
LIMIT ${limit}
`);
}
export {
ConflictError,
NotFoundError,
@@ -1328,6 +1548,9 @@ export {
createDb,
createMemoryDb,
dnsRecords,
domainMonitorResults,
domainMonitors,
domainTags,
domains,
getAppSettings,
getAppSettingsSecrets,
@@ -1335,6 +1558,7 @@ export {
groups,
healthCheck,
ipHealthStatus,
notificationLog,
repos_exports as repos,
resolveDatabasePath,
runMigrations,