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
+44 -4
View File
@@ -43,11 +43,51 @@ export async function createDomain(
export function updateDomain(
db: Db,
id: number,
groupId: number | null,
status: string,
certMonitoring?: string,
patch: {
group_id?: number | null;
status?: string;
cert_monitoring?: string;
environment?: string | null;
tags?: string[];
},
): Domain {
return repos.updateDomain(db, id, groupId, status, certMonitoring);
const domain = repos.updateDomain(db, id, {
group_id: patch.group_id,
status: patch.status,
cert_monitoring: patch.cert_monitoring,
environment: patch.environment,
});
if (patch.tags !== undefined) {
repos.setDomainTags(db, id, patch.tags);
}
return domain;
}
export function bulkUpdateDomains(
db: Db,
ids: number[],
patch: {
group_id?: number | null;
environment?: string | null;
tags_add?: string[];
},
): number {
let updated = 0;
for (const id of ids) {
try {
repos.updateDomain(db, id, {
group_id: patch.group_id,
environment: patch.environment,
});
if (patch.tags_add?.length) {
repos.addDomainTags(db, id, patch.tags_add);
}
updated += 1;
} catch {
// skip missing
}
}
return updated;
}
export function deleteDomain(db: Db, id: number): void {
+121 -4
View File
@@ -1,4 +1,5 @@
import { connect } from "node:net";
import { resolve4, resolve6 } from "node:dns/promises";
import { Agent, fetch as undiciFetch } from "undici";
import type { Db } from "@cfdm/db";
import { repos } from "@cfdm/db";
@@ -68,10 +69,6 @@ async function httpProbe(
const pathWithSlash = path.startsWith("/") ? path : `/${path}`;
const port = target.port ?? 80;
const useTls = port === 443;
// For HTTPS on 443, probe via the hostname (URL host = hostname) so TLS SNI, Host header
// and any edge/vhost protection (e.g. Cloudflare origin 421 on direct-IP) all line up.
// For multi-A records this loses strict per-IP HTTPS granularity — use TCP probe for that.
// For plain HTTP we still hit the literal IP (per-record target).
const urlHost = useTls ? target.hostname || ip : ip;
const url = `${useTls ? "https" : "http"}://${urlHost}${pathWithSlash}`;
const dispatcher =
@@ -119,6 +116,47 @@ async function httpProbe(
}
}
/** Host reachability via TCP :443 then :80 (ICMP often unavailable in Node). */
async function pingProbe(hostname: string, timeoutMs: number): Promise<ProbeResult> {
const ports = [443, 80];
let last: ProbeResult = {
ok: false,
latencyMs: 0,
error: "unreachable",
};
for (const port of ports) {
last = await tcpProbe(hostname, port, timeoutMs);
if (last.ok) return last;
}
return last;
}
async function dnsProbe(hostname: string): Promise<ProbeResult> {
const started = Date.now();
try {
const [v4, v6] = await Promise.allSettled([
resolve4(hostname),
resolve6(hostname),
]);
const hasV4 = v4.status === "fulfilled" && v4.value.length > 0;
const hasV6 = v6.status === "fulfilled" && v6.value.length > 0;
if (!hasV4 && !hasV6) {
return {
ok: false,
latencyMs: Date.now() - started,
error: "no A/AAAA records",
};
}
return { ok: true, latencyMs: Date.now() - started, error: null };
} catch (err) {
return {
ok: false,
latencyMs: Date.now() - started,
error: err instanceof Error ? err.message : String(err),
};
}
}
export async function probeTarget(
target: HealthCheckTarget,
): Promise<ProbeResult> {
@@ -127,6 +165,12 @@ export async function probeTarget(
if (target.type === "http") {
return httpProbe(target.ip, target, timeoutMs);
}
if (target.type === "ping") {
return pingProbe(target.hostname || target.ip, timeoutMs);
}
if (target.type === "dns") {
return dnsProbe(target.hostname || target.ip);
}
return tcpProbe(target.ip, port, timeoutMs);
}
@@ -205,6 +249,79 @@ export async function runAllChecks(
return targets.length;
}
export async function runDomainMonitors(
db: Db,
thresholds: HealthCheckThresholds,
): Promise<number> {
const monitors = repos.listEnabledDomainMonitors(db);
let checked = 0;
for (const monitor of monitors) {
const target: HealthCheckTarget = {
scope: "binding",
ref_id: monitor.id,
ip: monitor.hostname,
hostname: monitor.hostname,
type: monitor.type as HealthCheckTarget["type"],
port: monitor.type === "http" ? (monitor.path?.includes("443") ? 443 : 80) : null,
path: monitor.path,
expected_status: monitor.expected_status,
timeout_ms: monitor.timeout_ms,
};
let result: ProbeResult;
if (monitor.type === "http") {
result = await httpProbe(monitor.hostname, {
...target,
port: 443,
path: monitor.path ?? "/",
}, monitor.timeout_ms);
if (!result.ok) {
result = await httpProbe(monitor.hostname, {
...target,
port: 80,
path: monitor.path ?? "/",
}, monitor.timeout_ms);
}
} else if (monitor.type === "ping") {
result = await pingProbe(monitor.hostname, monitor.timeout_ms);
} else {
result = await dnsProbe(monitor.hostname);
}
const prevStatus = monitor.last_status as IpHealthState;
const { state } = deriveState(
result.ok,
result.latencyMs,
{
consecutive_failures: result.ok ? 0 : 1,
status: prevStatus,
},
thresholds,
);
repos.updateDomainMonitorResult(
db,
monitor.id,
state,
result.latencyMs,
result.error,
);
if (prevStatus !== state && prevStatus !== "unknown") {
const label =
state === "up" ? "OK" : state === "degraded" ? "Slow" : state === "down" ? "Down" : "—";
repos.insertNotificationLog(
db,
"domain_monitor",
"domain_monitor",
monitor.id,
`${monitor.hostname}: ${label}`,
result.error
? `${monitor.type.toUpperCase()}${label}. ${result.error}`
: `${monitor.type.toUpperCase()}${label}${result.latencyMs != null ? ` (${result.latencyMs} мс)` : ""}`,
);
}
checked += 1;
}
return checked;
}
export function listStatus(
db: Db,
scope: "binding" | "group",