feat(api, web): integrate health status management for services and domains
Build, Test, and Push CFDM Docker Image / test (push) Successful in 10m17s
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

- Added health status and latency fields to service and domain schemas, enhancing monitoring capabilities.
- Implemented health status aggregation for services in the API, allowing for improved health checks and reporting.
- Updated web components to display health status using HealthCheckBadge, improving user visibility of service health.
- Refactored service and domain management components to incorporate health status in various views, enhancing overall functionality.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-17 02:55:49 +07:00
co-authored by Cursor
parent 64585ccd47
commit 1f710bbe8b
48 changed files with 4258 additions and 246 deletions
+15 -2
View File
File diff suppressed because one or more lines are too long
+106
View File
@@ -415,6 +415,8 @@ function getAppSwitcher(db) {
var repos_exports = {};
__export(repos_exports, {
addDomainTags: () => addDomainTags,
aggregateIpHealthByRefs: () => aggregateIpHealthByRefs,
aggregateIpHealthByServiceIds: () => aggregateIpHealthByServiceIds,
bindingsToRemove: () => bindingsToRemove,
countCertificatesByStatus: () => countCertificatesByStatus,
createDomain: () => createDomain,
@@ -489,6 +491,7 @@ __export(repos_exports, {
listSubdomainsByDomain: () => listSubdomainsByDomain,
listUngroupedServices: () => listUngroupedServices,
markDnsPendingDelete: () => markDnsPendingDelete,
mergeHealthAggregates: () => mergeHealthAggregates,
reorderServices: () => reorderServices,
replaceBindingIps: () => replaceBindingIps,
replaceBindingIpsWithMeta: () => replaceBindingIpsWithMeta,
@@ -1294,6 +1297,109 @@ function listIpHealthStatus(db, scope, refId) {
WHERE scope = ${scope} AND ref_id = ${refId}
`);
}
var UNKNOWN_HEALTH = {
health_status: "unknown",
health_latency_ms: null
};
function parseHealthAggregateRow(row) {
const status = row.health_status;
if (status === "up" || status === "down" || status === "degraded" || status === "unknown") {
return {
health_status: status,
health_latency_ms: row.health_latency_ms ?? null
};
}
return UNKNOWN_HEALTH;
}
var WORST_HEALTH_SQL = sql2.raw(`CASE
WHEN MAX(CASE
WHEN status = 'down' THEN 3
WHEN status = 'degraded' THEN 2
WHEN status = 'up' THEN 1
ELSE 0
END) = 3 THEN 'down'
WHEN MAX(CASE
WHEN status = 'down' THEN 3
WHEN status = 'degraded' THEN 2
WHEN status = 'up' THEN 1
ELSE 0
END) = 2 THEN 'degraded'
WHEN MAX(CASE
WHEN status = 'down' THEN 3
WHEN status = 'degraded' THEN 2
WHEN status = 'up' THEN 1
ELSE 0
END) = 1 THEN 'up'
ELSE 'unknown'
END`);
function aggregateIpHealthByRefs(db, scope, refIds) {
const result = /* @__PURE__ */ new Map();
if (refIds.length === 0) return result;
const idList = sql2.join(
refIds.map((id) => sql2`${id}`),
sql2`, `
);
const rows = db.all(sql2`
SELECT ref_id,
${WORST_HEALTH_SQL} AS health_status,
MAX(latency_ms) AS health_latency_ms
FROM ip_health_status
WHERE scope = ${scope} AND ref_id IN (${idList})
GROUP BY ref_id
`);
for (const row of rows) {
result.set(row.ref_id, parseHealthAggregateRow(row));
}
return result;
}
function aggregateIpHealthByServiceIds(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,
${WORST_HEALTH_SQL} AS health_status,
MAX(ihs.latency_ms) AS health_latency_ms
FROM ip_health_status ihs
INNER JOIN service_bindings sb
ON ihs.scope = 'binding' AND ihs.ref_id = sb.id
WHERE sb.service_id IN (${idList})
GROUP BY sb.service_id
`);
for (const row of rows) {
result.set(row.service_id, parseHealthAggregateRow(row));
}
return result;
}
function mergeHealthAggregates(parts) {
const rank = {
unknown: 0,
up: 1,
degraded: 2,
down: 3
};
let worst = UNKNOWN_HEALTH;
let hasAny = false;
for (const part of parts) {
if (!part) continue;
hasAny = true;
if (rank[part.health_status] > rank[worst.health_status]) {
worst = {
health_status: part.health_status,
health_latency_ms: part.health_latency_ms
};
} else if (part.health_status === worst.health_status && part.health_latency_ms != null && (worst.health_latency_ms == null || part.health_latency_ms > worst.health_latency_ms)) {
worst = {
health_status: worst.health_status,
health_latency_ms: part.health_latency_ms
};
}
}
return hasAny ? worst : UNKNOWN_HEALTH;
}
function getIpHealthStatusRow(db, scope, refId, ip) {
const rows = db.all(sql2`
SELECT scope, ref_id, ip, status, latency_ms, consecutive_failures,
+147
View File
@@ -8,6 +8,7 @@ import type {
HealthCheckScope,
HealthCheckTarget,
HealthCheckType,
IpHealthState,
IpHealthStatus,
LbMode,
Service,
@@ -1466,6 +1467,152 @@ export function listIpHealthStatus(
`);
}
export type HealthAggregate = {
health_status: IpHealthState;
health_latency_ms: number | null;
};
const UNKNOWN_HEALTH: HealthAggregate = {
health_status: "unknown",
health_latency_ms: null,
};
function parseHealthAggregateRow(row: {
health_status: string | null;
health_latency_ms: number | null;
}): HealthAggregate {
const status = row.health_status;
if (
status === "up" ||
status === "down" ||
status === "degraded" ||
status === "unknown"
) {
return {
health_status: status,
health_latency_ms: row.health_latency_ms ?? null,
};
}
return UNKNOWN_HEALTH;
}
const WORST_HEALTH_SQL = sql.raw(`CASE
WHEN MAX(CASE
WHEN status = 'down' THEN 3
WHEN status = 'degraded' THEN 2
WHEN status = 'up' THEN 1
ELSE 0
END) = 3 THEN 'down'
WHEN MAX(CASE
WHEN status = 'down' THEN 3
WHEN status = 'degraded' THEN 2
WHEN status = 'up' THEN 1
ELSE 0
END) = 2 THEN 'degraded'
WHEN MAX(CASE
WHEN status = 'down' THEN 3
WHEN status = 'degraded' THEN 2
WHEN status = 'up' THEN 1
ELSE 0
END) = 1 THEN 'up'
ELSE 'unknown'
END`);
/** Worst status across ip_health_status rows for each ref_id in a scope. */
export function aggregateIpHealthByRefs(
db: Db,
scope: HealthCheckScope,
refIds: number[],
): Map<number, HealthAggregate> {
const result = new Map<number, HealthAggregate>();
if (refIds.length === 0) return result;
const idList = sql.join(
refIds.map((id) => sql`${id}`),
sql`, `,
);
const rows = db.all<{
ref_id: number;
health_status: string | null;
health_latency_ms: number | null;
}>(sql`
SELECT ref_id,
${WORST_HEALTH_SQL} AS health_status,
MAX(latency_ms) AS health_latency_ms
FROM ip_health_status
WHERE scope = ${scope} AND ref_id IN (${idList})
GROUP BY ref_id
`);
for (const row of rows) {
result.set(row.ref_id, parseHealthAggregateRow(row));
}
return result;
}
/** Worst binding-scope health rolled up per service_id. */
export function aggregateIpHealthByServiceIds(
db: Db,
serviceIds: number[],
): Map<number, HealthAggregate> {
const result = new Map<number, HealthAggregate>();
if (serviceIds.length === 0) return result;
const idList = sql.join(
serviceIds.map((id) => sql`${id}`),
sql`, `,
);
const rows = db.all<{
service_id: number;
health_status: string | null;
health_latency_ms: number | null;
}>(sql`
SELECT sb.service_id AS service_id,
${WORST_HEALTH_SQL} AS health_status,
MAX(ihs.latency_ms) AS health_latency_ms
FROM ip_health_status ihs
INNER JOIN service_bindings sb
ON ihs.scope = 'binding' AND ihs.ref_id = sb.id
WHERE sb.service_id IN (${idList})
GROUP BY sb.service_id
`);
for (const row of rows) {
result.set(row.service_id, parseHealthAggregateRow(row));
}
return result;
}
export function mergeHealthAggregates(
parts: Array<HealthAggregate | undefined | null>,
): HealthAggregate {
const rank: Record<IpHealthState, number> = {
unknown: 0,
up: 1,
degraded: 2,
down: 3,
};
let worst: HealthAggregate = UNKNOWN_HEALTH;
let hasAny = false;
for (const part of parts) {
if (!part) continue;
hasAny = true;
if (rank[part.health_status] > rank[worst.health_status]) {
worst = {
health_status: part.health_status,
health_latency_ms: part.health_latency_ms,
};
} else if (
part.health_status === worst.health_status &&
part.health_latency_ms != null &&
(worst.health_latency_ms == null ||
part.health_latency_ms > worst.health_latency_ms)
) {
worst = {
health_status: worst.health_status,
health_latency_ms: part.health_latency_ms,
};
}
}
return hasAny ? worst : UNKNOWN_HEALTH;
}
export function getIpHealthStatusRow(
db: Db,
scope: HealthCheckScope,
+42
View File
@@ -497,6 +497,13 @@ declare const serviceViewSchema: z.ZodObject<{
target_ip_priorities?: Record<string, number> | undefined;
target_cname?: string | null | undefined;
}>>>>;
health_status: z.ZodDefault<z.ZodEnum<{
unknown: "unknown";
up: "up";
down: "down";
degraded: "degraded";
}>>;
health_latency_ms: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>;
declare const serviceGroupViewSchema: z.ZodObject<{
id: z.ZodNumber;
@@ -619,7 +626,21 @@ declare const serviceGroupViewSchema: z.ZodObject<{
target_ip_priorities?: Record<string, number> | undefined;
target_cname?: string | null | undefined;
}>>>>;
health_status: z.ZodDefault<z.ZodEnum<{
unknown: "unknown";
up: "up";
down: "down";
degraded: "degraded";
}>>;
health_latency_ms: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>>>;
health_status: z.ZodDefault<z.ZodEnum<{
unknown: "unknown";
up: "up";
down: "down";
degraded: "degraded";
}>>;
health_latency_ms: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>;
declare const serviceGroupsResponseSchema: z.ZodObject<{
groups: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -743,7 +764,21 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
target_ip_priorities?: Record<string, number> | undefined;
target_cname?: string | null | undefined;
}>>>>;
health_status: z.ZodDefault<z.ZodEnum<{
unknown: "unknown";
up: "up";
down: "down";
degraded: "degraded";
}>>;
health_latency_ms: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>>>;
health_status: z.ZodDefault<z.ZodEnum<{
unknown: "unknown";
up: "up";
down: "down";
degraded: "degraded";
}>>;
health_latency_ms: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>>>;
ungrouped: z.ZodDefault<z.ZodArray<z.ZodObject<{
id: z.ZodNumber;
@@ -834,6 +869,13 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
target_ip_priorities?: Record<string, number> | undefined;
target_cname?: string | null | undefined;
}>>>>;
health_status: z.ZodDefault<z.ZodEnum<{
unknown: "unknown";
up: "up";
down: "down";
degraded: "degraded";
}>>;
health_latency_ms: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
}, z.core.$strip>>>;
}, z.core.$strip>;
declare const domainSchema: z.ZodObject<{
+6 -2
View File
@@ -257,10 +257,14 @@ var serviceViewSchema = serviceSchema.extend({
subdomain: z.string().default(""),
enabled: z.coerce.boolean().default(false),
ips: z.array(z.string()).default([]),
domains: z.array(serviceDomainBindingSchema).default([])
domains: z.array(serviceDomainBindingSchema).default([]),
health_status: ipHealthStateSchema.default("unknown"),
health_latency_ms: z.number().nullable().default(null)
});
var serviceGroupViewSchema = serviceGroupSchema.extend({
services: z.array(serviceViewSchema).default([])
services: z.array(serviceViewSchema).default([]),
health_status: ipHealthStateSchema.default("unknown"),
health_latency_ms: z.number().nullable().default(null)
});
var serviceGroupsResponseSchema = z.object({
groups: z.array(serviceGroupViewSchema).default([]),
+4
View File
@@ -132,10 +132,14 @@ export const serviceViewSchema = serviceSchema.extend({
enabled: z.coerce.boolean().default(false),
ips: z.array(z.string()).default([]),
domains: z.array(serviceDomainBindingSchema).default([]),
health_status: ipHealthStateSchema.default('unknown'),
health_latency_ms: z.number().nullable().default(null),
})
export const serviceGroupViewSchema = serviceGroupSchema.extend({
services: z.array(serviceViewSchema).default([]),
health_status: ipHealthStateSchema.default('unknown'),
health_latency_ms: z.number().nullable().default(null),
})
export const serviceGroupsResponseSchema = z.object({
+4
View File
@@ -27,6 +27,8 @@ export interface ServiceGroup {
export interface ServiceGroupView extends ServiceGroup {
services: ServiceView[];
health_status: IpHealthState;
health_latency_ms: number | null;
}
export interface ServiceGroupsResponse {
@@ -185,6 +187,8 @@ export interface ServiceView {
updated_at: string;
ips: string[];
domains: ServiceDomainBindingView[];
health_status: IpHealthState;
health_latency_ms: number | null;
}
export interface GroupWithStats extends Group {
@@ -1,5 +1,3 @@
"use client"
import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"