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 <cursoragent@cursor.com>
This commit is contained in:
DenozordecandCursor committed 2026-07-17 02:25:12 +07:00
1 parent dd167a4fec
commit 64585ccd47
38 files changed
+4199 -677

No files matched your search

+315 -15
View File
@@ -24,9 +24,13 @@ import { NotFoundError } from "./errors.js";
import {
certificates,
dnsRecords,
domainMonitorResults,
domainMonitors,
domainTags,
domains,
groups,
ipHealthStatus,
notificationLog,
serviceBindingIps,
serviceBindingRecords,
serviceBindings,
@@ -119,14 +123,65 @@ export function listDomainsEnriched(db: Db, groupId?: number): DomainListItem[]
const base = groupId != null
? sql`WHERE d.group_id = ${groupId}`
: sql``;
return db.all<DomainListItem>(sql`
const rows = db.all<
Omit<DomainListItem, "tags"> & { tags_json: string | null }
>(sql`
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 as DomainListItem["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)
: [],
};
});
}
export function findDomainByZoneName(db: Db, zoneName: string): Domain | null {
@@ -163,22 +218,24 @@ export function createDomain(
export function updateDomain(
db: Db,
id: number,
groupId: number | null,
status: string,
certMonitoring?: string,
): Domain {
const updates: {
group_id: number | null;
status: string;
patch: {
group_id?: number | null;
status?: string;
cert_monitoring?: string;
updated_at: ReturnType<typeof sql>;
} = {
group_id: groupId,
status,
environment?: string | null;
},
): Domain {
const existing = getDomain(db, id);
const updates: Record<string, unknown> = {
group_id: patch.group_id !== undefined ? patch.group_id : existing.group_id,
status: patch.status !== undefined ? patch.status : existing.status,
updated_at: sql`datetime('now')`,
};
if (certMonitoring !== undefined) {
updates.cert_monitoring = certMonitoring;
if (patch.cert_monitoring !== undefined) {
updates.cert_monitoring = patch.cert_monitoring;
}
if (patch.environment !== undefined) {
updates.environment = patch.environment;
}
const result = db
.update(domains)
@@ -1595,3 +1652,246 @@ export function listHealthCheckTargets(db: Db): HealthCheckTarget[] {
...groupInheritedCnameBindingTargets,
];
}
// --- Domain tags ---
export function listDomainTags(db: Db, domainId: number): string[] {
return db
.select({ tag: domainTags.tag })
.from(domainTags)
.where(eq(domainTags.domain_id, domainId))
.all()
.map((r) => r.tag);
}
export function setDomainTags(db: Db, domainId: number, tags: string[]): void {
db.delete(domainTags).where(eq(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();
}
}
export function addDomainTags(db: Db, domainId: number, tags: string[]): void {
const unique = [...new Set(tags.map((t) => t.trim()).filter(Boolean))];
for (const tag of unique) {
db.run(sql`
INSERT INTO domain_tags (domain_id, tag)
VALUES (${domainId}, ${tag})
ON CONFLICT(domain_id, tag) DO NOTHING
`);
}
}
// --- Domain monitors ---
export interface DomainMonitorRow {
id: number;
domain_id: number;
hostname: string;
type: string;
enabled: boolean;
interval_sec: number;
timeout_ms: number;
path: string | null;
expected_status: number | null;
last_status: string;
last_latency_ms: number | null;
last_checked_at: string | null;
last_error: string | null;
created_at: string;
updated_at: string;
}
export function listDomainMonitors(
db: Db,
domainId: number,
): DomainMonitorRow[] {
return db
.select()
.from(domainMonitors)
.where(eq(domainMonitors.domain_id, domainId))
.orderBy(asc(domainMonitors.id))
.all() as DomainMonitorRow[];
}
export function listEnabledDomainMonitors(db: Db): DomainMonitorRow[] {
return db
.select()
.from(domainMonitors)
.where(eq(domainMonitors.enabled, true))
.all() as DomainMonitorRow[];
}
export function getDomainMonitor(db: Db, id: number): DomainMonitorRow {
const row = db
.select()
.from(domainMonitors)
.where(eq(domainMonitors.id, id))
.get();
if (!row) throw new NotFoundError(`domain_monitor ${id}`);
return row as DomainMonitorRow;
}
export function createDomainMonitor(
db: Db,
domainId: number,
input: {
hostname: string;
type: string;
enabled?: boolean;
interval_sec?: number;
timeout_ms?: number;
path?: string | null;
expected_status?: number | null;
},
): DomainMonitorRow {
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 ?? 5000,
path: input.path ?? null,
expected_status: input.expected_status ?? null,
})
.returning({ id: domainMonitors.id })
.get()!.id;
return getDomainMonitor(db, id);
}
export function deleteDomainMonitor(db: Db, id: number): void {
const result = db
.delete(domainMonitors)
.where(eq(domainMonitors.id, id))
.run();
if (result.changes === 0) throw new NotFoundError(`domain_monitor ${id}`);
}
export function updateDomainMonitorResult(
db: Db,
monitorId: number,
status: string,
latencyMs: number | null,
error: string | null,
): void {
db.update(domainMonitors)
.set({
last_status: status,
last_latency_ms: latencyMs,
last_checked_at: sql`datetime('now')`,
last_error: error,
updated_at: sql`datetime('now')`,
})
.where(eq(domainMonitors.id, monitorId))
.run();
db.insert(domainMonitorResults)
.values({
monitor_id: monitorId,
status,
latency_ms: latencyMs,
error,
})
.run();
// keep last 100 results per monitor
db.run(sql`
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
)
`);
}
export function listDomainMonitorResults(
db: Db,
monitorId: number,
limit = 50,
): {
id: number;
monitor_id: number;
status: string;
latency_ms: number | null;
error: string | null;
checked_at: string;
}[] {
return db.all(sql`
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}
`);
}
export function listDomainMonitorResultsForDomain(
db: Db,
domainId: number,
limit = 50,
): {
id: number;
monitor_id: number;
hostname: string;
type: string;
status: string;
latency_ms: number | null;
error: string | null;
checked_at: string;
}[] {
return db.all(sql`
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}
`);
}
// --- Notification log ---
export function insertNotificationLog(
db: Db,
kind: string,
refType: string,
refId: number | null,
title: string,
message: string,
): void {
db.insert(notificationLog)
.values({
kind,
ref_type: refType,
ref_id: refId,
title,
message,
})
.run();
}
export function listNotificationLog(
db: Db,
limit = 50,
): {
id: number;
kind: string;
ref_type: string;
ref_id: number | null;
title: string;
message: string;
created_at: string;
}[] {
return db.all(sql`
SELECT id, kind, ref_type, ref_id, title, message, created_at
FROM notification_log
ORDER BY created_at DESC, id DESC
LIMIT ${limit}
`);
}
+65
View File
@@ -77,6 +77,7 @@ export const 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()
@@ -287,6 +288,66 @@ export const appSettings = sqliteTable("app_settings", {
.default(sql`datetime('now')`),
});
export const 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] })],
);
export const 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(5000),
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')`),
});
export const 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')`),
});
export const 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')`),
});
export const schema = {
groups,
services,
@@ -303,4 +364,8 @@ export const schema = {
syncJobs,
ipHealthStatus,
appSettings,
domainTags,
domainMonitors,
domainMonitorResults,
notificationLog,
};