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
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:
1 parent
dd167a4fec
commit
64585ccd47
38 files changed
+4199
-677
No files matched your search
+315
-15
@@ -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}
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
Reference in new issue
Block a user