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 <[email protected]>
This commit is contained in:
Vendored
+1266
-3
File diff suppressed because it is too large
Load Diff
Vendored
+232
-8
@@ -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,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
-- Domain environment + tags + monitors + notification log
|
||||
ALTER TABLE domains ADD COLUMN environment TEXT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS domain_tags (
|
||||
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||
tag TEXT NOT NULL,
|
||||
PRIMARY KEY (domain_id, tag)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS domain_monitors (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||
hostname TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'http',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
interval_sec INTEGER NOT NULL DEFAULT 60,
|
||||
timeout_ms INTEGER NOT NULL DEFAULT 5000,
|
||||
path TEXT,
|
||||
expected_status INTEGER,
|
||||
last_status TEXT NOT NULL DEFAULT 'unknown',
|
||||
last_latency_ms INTEGER,
|
||||
last_checked_at TEXT,
|
||||
last_error TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_domain_monitors_domain
|
||||
ON domain_monitors(domain_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS domain_monitor_results (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_id INTEGER NOT NULL REFERENCES domain_monitors(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL,
|
||||
latency_ms INTEGER,
|
||||
error TEXT,
|
||||
checked_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_domain_monitor_results_monitor
|
||||
ON domain_monitor_results(monitor_id, checked_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notification_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
kind TEXT NOT NULL,
|
||||
ref_type TEXT NOT NULL,
|
||||
ref_id INTEGER,
|
||||
title TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_log_created
|
||||
ON notification_log(created_at DESC);
|
||||
+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