From 4224db8eb37fb46f4375b61faa125db43b8d7f36 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 19 Aug 2026 14:57:19 +0700 Subject: [PATCH] feat(services): enhance service health management with IP health tracking - Introduced IP health tracking in service views, allowing for detailed monitoring of individual IP statuses and latencies. - Updated the service configuration to include an `ip_health` array, providing structured health data for each IP. - Enhanced the `attachServiceHealth` function to aggregate IP health data alongside overall service health. - Modified relevant components to display IP health information, improving visibility and user experience in service management interfaces. This commit significantly improves the health monitoring capabilities of services, enabling better insights into the status of individual IPs associated with each service. --- .../src/services/service-config-service.ts | 23 ++- apps/api/test/service-groups-health.test.ts | 9 + .../components/kanban/service-kanban-card.tsx | 6 +- .../web/src/components/service-edit-sheet.tsx | 54 +++++- .../components/services/service-fqdn-list.tsx | 41 +++-- .../components/services/service-unit-card.tsx | 22 +-- .../services/services-grouped-catalog.tsx | 163 +++++------------- apps/web/src/lib/schemas.ts | 7 + packages/db/dist/index.d.ts | 11 +- packages/db/dist/index.js | 34 ++++ packages/db/src/repos.ts | 49 ++++++ packages/shared/dist/index.d.ts | 59 ++++++- packages/shared/dist/index.js | 9 +- packages/shared/src/schemas.ts | 9 + packages/shared/src/types.ts | 7 + 15 files changed, 346 insertions(+), 157 deletions(-) diff --git a/apps/api/src/services/service-config-service.ts b/apps/api/src/services/service-config-service.ts index e01f55b..f703059 100644 --- a/apps/api/src/services/service-config-service.ts +++ b/apps/api/src/services/service-config-service.ts @@ -307,6 +307,7 @@ async function buildView(db: Db, serviceId: number): Promise { domains: domainViews, health_status: "unknown", health_latency_ms: null, + ip_health: [], }; } @@ -314,16 +315,27 @@ function attachServiceHealth( db: Db, views: ServiceView[], ): ServiceView[] { - const healthByService = repos.aggregateIpHealthByServiceIds( - db, - views.map((v) => v.id), - ); + const ids = views.map((v) => v.id); + const healthByService = repos.aggregateIpHealthByServiceIds(db, ids); + const ipHealthByService = repos.listIpHealthByServiceIds(db, ids); return views.map((view) => { const health = healthByService.get(view.id); + const byIp = new Map( + (ipHealthByService.get(view.id) ?? []).map((row) => [row.ip, row]), + ); + const ip_health = (view.ips ?? []).map((ip) => { + const row = byIp.get(ip); + return { + ip, + status: row?.status ?? ("unknown" as const), + latency_ms: row?.latency_ms ?? null, + }; + }); return { ...view, health_status: health?.health_status ?? "unknown", health_latency_ms: health?.health_latency_ms ?? null, + ip_health, }; }); } @@ -372,7 +384,7 @@ export async function listGroupViews(db: Db): Promise { const groupViews = groupViewsRaw.map((group) => { const services = group.services.map( - (s) => healthById.get(s.id) ?? { ...s, health_status: "unknown" as const, health_latency_ms: null }, + (s) => healthById.get(s.id) ?? { ...s, health_status: "unknown" as const, health_latency_ms: null, ip_health: [] }, ); const groupScopeHealth = groupHealthById.get(group.id); // Only enabled services feed the group badge — a disabled service with a @@ -401,6 +413,7 @@ export async function listGroupViews(db: Db): Promise { ...s, health_status: "unknown" as const, health_latency_ms: null, + ip_health: [], }, ); diff --git a/apps/api/test/service-groups-health.test.ts b/apps/api/test/service-groups-health.test.ts index 1c3dbbc..fa44ac7 100644 --- a/apps/api/test/service-groups-health.test.ts +++ b/apps/api/test/service-groups-health.test.ts @@ -39,6 +39,7 @@ describe("service groups health enrichment", () => { const service = repos.createService(app.db, "Panel", "panel"); repos.setServiceGroup(app.db, service.id, group.id); repos.setServiceEnabled(app.db, service.id, true); + repos.replaceServiceIps(app.db, service.id, ["1.2.3.4"]); const binding = repos.insertBinding( app.db, domain.id, @@ -85,6 +86,11 @@ describe("service groups health enrichment", () => { id: number; health_status: string; health_latency_ms: number | null; + ip_health: Array<{ + ip: string; + status: string; + latency_ms: number | null; + }>; }>; }>; }; @@ -92,6 +98,9 @@ describe("service groups health enrichment", () => { expect(groupView).toBeDefined(); expect(groupView!.services[0]?.health_status).toBe("degraded"); expect(groupView!.services[0]?.health_latency_ms).toBe(120); + expect(groupView!.services[0]?.ip_health).toEqual([ + { ip: "1.2.3.4", status: "degraded", latency_ms: 120 }, + ]); // group worst = degraded (from service) over up (group scope) expect(groupView!.health_status).toBe("degraded"); diff --git a/apps/web/src/components/kanban/service-kanban-card.tsx b/apps/web/src/components/kanban/service-kanban-card.tsx index 3282f05..bfb4468 100644 --- a/apps/web/src/components/kanban/service-kanban-card.tsx +++ b/apps/web/src/components/kanban/service-kanban-card.tsx @@ -88,7 +88,11 @@ export function ServiceKanbanCard({
IP - +
diff --git a/apps/web/src/components/service-edit-sheet.tsx b/apps/web/src/components/service-edit-sheet.tsx index 311455a..99c5a07 100644 --- a/apps/web/src/components/service-edit-sheet.tsx +++ b/apps/web/src/components/service-edit-sheet.tsx @@ -3,9 +3,11 @@ import { PlusIcon, Trash2Icon } from 'lucide-react' import { ConfirmDialog } from '@/components/confirm-dialog' import { TaggedInput, isValidIpv4 } from '@/components/tagged-input' import { ServiceBindingIpInput } from '@/components/service-binding-ip-input' -import type { - LbMode, - HealthCheckType, +import { + HealthCheckConfigFields, + type LbAndHealthConfig, + type LbMode, + type HealthCheckType, } from '@/components/health-check-config-fields' import type { CreateServiceWithConfigInput, @@ -319,6 +321,43 @@ export function ServiceEditSheet({ ) } + function healthFromConfig(next: LbAndHealthConfig): BindingHealthConfig { + return { + enabled: next.enabled, + type: next.type, + port: next.port, + path: next.path, + expected_status: next.expected_status, + interval_sec: next.interval_sec, + timeout_ms: next.timeout_ms, + verify_tls: next.verify_tls, + provider: next.provider, + } + } + + function handlePrimaryHealthChange(next: LbAndHealthConfig) { + const health = healthFromConfig(next) + setBindings((current) => { + if (current.length === 0) { + return [ + { + ...withPoolIps(emptyBindingDraft(commonFqdn), ips), + lb_mode: next.lb_mode, + health, + }, + ] + } + return current.map((item, index) => + index === 0 ? { ...item, lb_mode: next.lb_mode, health } : item, + ) + }) + } + + const primaryHealthValue: LbAndHealthConfig = { + lb_mode: bindings[0]?.lb_mode ?? 'round_robin', + ...(bindings[0]?.health ?? defaultHealth), + } + function resolveServiceGroupId(): number | null { return serviceGroupId === 'none' ? null : Number(serviceGroupId) } @@ -464,6 +503,15 @@ export function ServiceEditSheet({ +
+

Health check

+ +
+

Доп. FQDN

diff --git a/apps/web/src/components/services/service-fqdn-list.tsx b/apps/web/src/components/services/service-fqdn-list.tsx index f4ad8f7..b57500d 100644 --- a/apps/web/src/components/services/service-fqdn-list.tsx +++ b/apps/web/src/components/services/service-fqdn-list.tsx @@ -1,6 +1,7 @@ import { CheckIcon, CopyIcon } from 'lucide-react' import { toast } from 'sonner' +import { HealthCheckBadge } from '@/components/health-check-badge' import { Badge } from '@/components/reui/badge' import { TruncatedText } from '@/components/truncated-text' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' @@ -119,6 +120,7 @@ const VISIBLE_IP_LIMIT = 6 interface ServiceIpListProps { ips: string[] + ipHealth?: ServiceView['ip_health'] className?: string emptyLabel?: string copyable?: boolean @@ -127,6 +129,7 @@ interface ServiceIpListProps { export function ServiceIpList({ ips, + ipHealth = [], className, emptyLabel = 'Нет IP', copyable = false, @@ -140,20 +143,33 @@ export function ServiceIpList({ ) } + const healthByIp = new Map(ipHealth.map((row) => [row.ip, row])) const visible = ips.slice(0, VISIBLE_IP_LIMIT) const extraCount = ips.length - visible.length - const copyValue = ips.join('\n') return ( -
- - {visible.join(' · ')} - +
+ {visible.map((ip) => { + const health = healthByIp.get(ip) + return ( +
+ + + {ip} + + {copyable ? : null} +
+ ) + })} {extraCount > 0 ? ( @@ -162,7 +178,7 @@ export function ServiceIpList({ } > @@ -170,7 +186,7 @@ export function ServiceIpList({
    - {ips.map((ip) => ( + {ips.slice(VISIBLE_IP_LIMIT).map((ip) => (
  • {ip}
  • ))}
@@ -178,7 +194,6 @@ export function ServiceIpList({
) : null} - {copyable ? : null}
) } diff --git a/apps/web/src/components/services/service-unit-card.tsx b/apps/web/src/components/services/service-unit-card.tsx index b046838..fb3c111 100644 --- a/apps/web/src/components/services/service-unit-card.tsx +++ b/apps/web/src/components/services/service-unit-card.tsx @@ -1,7 +1,6 @@ import { Link } from '@tanstack/react-router' import { MoreHorizontalIcon, ServerIcon } from 'lucide-react' -import { HealthCheckBadge } from '@/components/health-check-badge' import { Frame, FrameDescription, @@ -40,9 +39,9 @@ export function ServiceUnitCard({ onToggleService, }: ServiceUnitCardProps) { return ( - - -
+ + +
- - - + + - ) diff --git a/apps/web/src/components/services/services-grouped-catalog.tsx b/apps/web/src/components/services/services-grouped-catalog.tsx index 220d83e..649949c 100644 --- a/apps/web/src/components/services/services-grouped-catalog.tsx +++ b/apps/web/src/components/services/services-grouped-catalog.tsx @@ -1,14 +1,12 @@ import { useMemo, useState, type ReactNode } from 'react' import { ChevronDownIcon, - FilterIcon, FolderPlusIcon, - FunnelXIcon, PlusIcon, + SearchIcon, ServerIcon, } from 'lucide-react' -import { Filters, type Filter } from '@/components/reui/filters' import { Frame, FrameDescription, @@ -16,21 +14,13 @@ import { FramePanel, FrameTitle, } from '@/components/reui/frame' -import { CountedLineTabs } from '@/components/counted-line-tabs' import { EmptyState } from '@/components/empty-state' -import { - applyFiltersToData, - getActiveFilters, -} from '@/components/reui-kit/filter-utils' import { ServiceCatalogSection } from '@/components/services/service-catalog-section' import { - SERVICE_TABS, - createDefaultServiceFilters, - serviceFilterFieldValue, serviceTabFilter, - useServiceFilterFields, type ServiceCatalogRow, } from '@/components/columns/services-columns' +import { serviceDisplayFqdns } from '@/lib/service-utils' import type { ServiceGroupView, ServiceGroupsResponse, @@ -43,23 +33,29 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@cfdm/ui/components/dropdown-menu' -import { Separator } from '@cfdm/ui/components/separator' +import { + InputGroup, + InputGroupAddon, + InputGroupInput, + InputGroupText, +} from '@cfdm/ui/components/input-group' import { Skeleton } from '@cfdm/ui/components/skeleton' -const HEALTH_TABS = [ - { id: 'health-ok', label: 'OK' }, - { id: 'health-slow', label: 'Slow' }, - { id: 'health-down', label: 'Down' }, - { id: 'health-unknown', label: '—' }, -] as const - -const ALL_TABS = [...SERVICE_TABS, ...HEALTH_TABS] as const - function serviceMatchesDomain(service: ServiceView, domainId?: number) { if (domainId == null) return true return service.domains.some((d) => d.domain_id === domainId) } +function serviceMatchesQuery(service: ServiceView, query: string) { + const needle = query.trim().toLowerCase() + if (!needle) return true + if (service.name.toLowerCase().includes(needle)) return true + if (service.slug.toLowerCase().includes(needle)) return true + return serviceDisplayFqdns(service).some((fqdn) => + fqdn.toLowerCase().includes(needle), + ) +} + function toCatalogRow( service: ServiceView, groupId: number | null, @@ -77,18 +73,6 @@ function toCatalogRow( } } -function catalogTabFilter(row: ServiceCatalogRow, tabId: string) { - if (tabId.startsWith('health-')) { - const status = row.service.health_status ?? 'unknown' - if (tabId === 'health-ok') return status === 'up' - if (tabId === 'health-slow') return status === 'degraded' - if (tabId === 'health-down') return status === 'down' - if (tabId === 'health-unknown') return status === 'unknown' - return true - } - return serviceTabFilter(row, tabId) -} - interface GroupUnitData { id: string group: ServiceGroupView | null @@ -195,8 +179,7 @@ export function ServicesGroupedCatalog({ primaryAction, hideHeader = false, togglingId, - activeTab: controlledTab, - onTabChange, + activeTab = 'all', onEditService, onDeleteService, onToggleService, @@ -205,13 +188,7 @@ export function ServicesGroupedCatalog({ onAddServiceToGroup, emptyAction, }: ServicesGroupedCatalogProps) { - const [internalTab, setInternalTab] = useState('all') - const tab = controlledTab ?? internalTab - const setTab = onTabChange ?? setInternalTab - const [filters, setFilters] = useState(() => - createDefaultServiceFilters(), - ) - const filterFields = useServiceFilterFields() + const [query, setQuery] = useState('') const flatRows = useMemo(() => { const rows: ServiceCatalogRow[] = [] @@ -228,24 +205,16 @@ export function ServicesGroupedCatalog({ return rows }, [data, domainId]) - const tabCounts = useMemo(() => { - const counts: Record = {} - for (const t of ALL_TABS) { - counts[t.id] = flatRows.filter((row) => catalogTabFilter(row, t.id)).length - } - return counts - }, [flatRows]) - const filteredIds = useMemo(() => { - const afterTab = flatRows.filter((row) => catalogTabFilter(row, tab)) - const afterFilters = applyFiltersToData(afterTab, filters, (item, field) => - serviceFilterFieldValue(item, field), + const afterTab = flatRows.filter((row) => serviceTabFilter(row, activeTab)) + const afterQuery = afterTab.filter((row) => + serviceMatchesQuery(row.service, query), ) - return new Set(afterFilters.map((r) => r.id)) - }, [flatRows, tab, filters]) + return new Set(afterQuery.map((r) => r.id)) + }, [flatRows, activeTab, query]) const showEmptyGroups = - tab === 'all' && domainId == null && getActiveFilters(filters).length === 0 + activeTab === 'all' && domainId == null && query.trim().length === 0 const units = useMemo( () => buildGroupUnits(data, filteredIds, domainId, showEmptyGroups), @@ -260,7 +229,7 @@ export function ServicesGroupedCatalog({ - + {Array.from({ length: 3 }).map((_, i) => ( ))} @@ -307,70 +276,28 @@ export function ServicesGroupedCatalog({ ) : null} - -
- ({ - id: t.id, - label: t.label, - count: tabCounts[t.id] ?? 0, - }))} - value={tab} - onValueChange={setTab} + + + + + + + + setQuery(event.target.value)} + placeholder="Поиск по названию" + aria-label="Поиск по названию" /> -
- - - -
- - - Фильтры - - } - /> - -
- - + {units.length === 0 ? ( -
- { - setTab('all') - setFilters(createDefaultServiceFilters()) - }} - > - Сбросить - - } - /> -
+ ) : ( -
+
{units.map((unit) => ( ; /** Worst binding-scope health rolled up per service_id. */ declare function aggregateIpHealthByServiceIds(db: Db, serviceIds: number[]): Map; +type ServiceIpHealthRow = { + ip: string; + status: IpHealthState; + latency_ms: number | null; +}; +/** Per-IP binding-scope health, worst status if the same IP is on several bindings. */ +declare function listIpHealthByServiceIds(db: Db, serviceIds: number[]): Map; declare function mergeHealthAggregates(parts: Array): HealthAggregate; declare function getIpHealthStatusRow(db: Db, scope: HealthCheckScope, refId: number, ip: string): IpHealthStatus | null; declare function upsertIpHealthStatus(db: Db, scope: HealthCheckScope, refId: number, ip: string, status: string, latencyMs: number | null, consecutiveFailures: number, lastError: string | null, consecutiveSuccesses?: number): void; @@ -8448,6 +8455,7 @@ type repos_DnsListFilter = DnsListFilter; type repos_DomainMonitorRow = DomainMonitorRow; type repos_HealthAggregate = HealthAggregate; type repos_ServiceGroupLbPatch = ServiceGroupLbPatch; +type repos_ServiceIpHealthRow = ServiceIpHealthRow; type repos_UpdateSubdomainPatch = UpdateSubdomainPatch; declare const repos_addDomainTags: typeof addDomainTags; declare const repos_aggregateGroupScopeHealthByIds: typeof aggregateGroupScopeHealthByIds; @@ -8531,6 +8539,7 @@ declare const repos_listGroupDnsRecords: typeof listGroupDnsRecords; declare const repos_listGroups: typeof listGroups; declare const repos_listHealthCheckTargets: typeof listHealthCheckTargets; declare const repos_listHealthChecks: typeof listHealthChecks; +declare const repos_listIpHealthByServiceIds: typeof listIpHealthByServiceIds; declare const repos_listIpHealthStatus: typeof listIpHealthStatus; declare const repos_listNodes: typeof listNodes; declare const repos_listNotificationLog: typeof listNotificationLog; @@ -8577,7 +8586,7 @@ declare const repos_upsertCertificateCheck: typeof upsertCertificateCheck; declare const repos_upsertIpHealthStatus: typeof upsertIpHealthStatus; declare const repos_upsertSubdomain: typeof upsertSubdomain; declare namespace repos { - export { type repos_BindingIpMeta as BindingIpMeta, type repos_BindingLbPatch as BindingLbPatch, type repos_DnsListFilter as DnsListFilter, type repos_DomainMonitorRow as DomainMonitorRow, type repos_HealthAggregate as HealthAggregate, type repos_ServiceGroupLbPatch as ServiceGroupLbPatch, type repos_UpdateSubdomainPatch as UpdateSubdomainPatch, repos_addDomainTags as addDomainTags, repos_aggregateGroupScopeHealthByIds as aggregateGroupScopeHealthByIds, repos_aggregateIpHealthByRefs as aggregateIpHealthByRefs, repos_aggregateIpHealthByServiceIds as aggregateIpHealthByServiceIds, repos_bindingsToRemove as bindingsToRemove, repos_bumpBindingVersion as bumpBindingVersion, repos_countCertificatesByStatus as countCertificatesByStatus, repos_createDomain as createDomain, repos_createDomainMonitor as createDomainMonitor, repos_createGroup as createGroup, repos_createHealthCheck as createHealthCheck, repos_createNode as createNode, repos_createService as createService, repos_createServiceGroup as createServiceGroup, repos_createSubdomain as createSubdomain, repos_createSyncJob as createSyncJob, repos_deleteBinding as deleteBinding, repos_deleteBindingsExcept as deleteBindingsExcept, repos_deleteCertificatesNotIn as deleteCertificatesNotIn, repos_deleteDnsRecord as deleteDnsRecord, repos_deleteDomain as deleteDomain, repos_deleteDomainMonitor as deleteDomainMonitor, repos_deleteGroup as deleteGroup, repos_deleteHealthCheck as deleteHealthCheck, repos_deleteIpHealthStatusForIp as deleteIpHealthStatusForIp, repos_deleteIpHealthStatusForRef as deleteIpHealthStatusForRef, repos_deleteNode as deleteNode, repos_deleteService as deleteService, repos_deleteServiceGroup as deleteServiceGroup, repos_deleteSubdomain as deleteSubdomain, repos_ensureNode as ensureNode, repos_findBinding as findBinding, repos_findDnsByCfId as findDnsByCfId, repos_findDomainByZoneName as findDomainByZoneName, repos_findHealthCheckByCfId as findHealthCheckByCfId, repos_findNodeByAddress as findNodeByAddress, repos_findNodeByIp as findNodeByIp, repos_findSubdomainByDomainAndName as findSubdomainByDomainAndName, repos_finishSyncJob as finishSyncJob, repos_getBinding as getBinding, repos_getBindingView as getBindingView, repos_getCertificate as getCertificate, repos_getDnsRecord as getDnsRecord, repos_getDomain as getDomain, repos_getDomainMonitor as getDomainMonitor, repos_getGroup as getGroup, repos_getGroupWithStats as getGroupWithStats, repos_getHealthCheck as getHealthCheck, repos_getIpHealthStatusRow as getIpHealthStatusRow, repos_getNode as getNode, repos_getService as getService, repos_getServiceGroup as getServiceGroup, repos_getSubdomain as getSubdomain, repos_getSyncJob as getSyncJob, repos_insertBinding as insertBinding, repos_insertDnsRecord as insertDnsRecord, repos_insertNotificationLog as insertNotificationLog, repos_linkBindingRecord as linkBindingRecord, repos_linkGroupDnsRecord as linkGroupDnsRecord, repos_listAllBindings as listAllBindings, repos_listAllDomains as listAllDomains, repos_listAllNodes as listAllNodes, repos_listAllSubdomains as listAllSubdomains, repos_listBindingIps as listBindingIps, repos_listBindingIpsWithMeta as listBindingIpsWithMeta, repos_listBindingNodes as listBindingNodes, repos_listBindingsByDomain as listBindingsByDomain, repos_listBindingsByService as listBindingsByService, repos_listCertificates as listCertificates, repos_listDnsByDomain as listDnsByDomain, repos_listDnsRecords as listDnsRecords, repos_listDomainMonitorResults as listDomainMonitorResults, repos_listDomainMonitorResultsForDomain as listDomainMonitorResultsForDomain, repos_listDomainMonitors as listDomainMonitors, repos_listDomainTags as listDomainTags, repos_listDomains as listDomains, repos_listDomainsEnriched as listDomainsEnriched, repos_listEnabledDomainMonitors as listEnabledDomainMonitors, repos_listGroupDnsRecords as listGroupDnsRecords, repos_listGroups as listGroups, repos_listHealthCheckTargets as listHealthCheckTargets, repos_listHealthChecks as listHealthChecks, repos_listIpHealthStatus as listIpHealthStatus, repos_listNodes as listNodes, repos_listNotificationLog as listNotificationLog, repos_listOriginIpsForFqdn as listOriginIpsForFqdn, repos_listRecordsForBinding as listRecordsForBinding, repos_listServiceGroups as listServiceGroups, repos_listServiceIps as listServiceIps, repos_listServices as listServices, repos_listServicesByGroup as listServicesByGroup, repos_listSubdomainsByDomain as listSubdomainsByDomain, repos_listUngroupedServices as listUngroupedServices, repos_markDnsPendingDelete as markDnsPendingDelete, repos_mergeHealthAggregates as mergeHealthAggregates, repos_pruneStaleIpHealthStatus as pruneStaleIpHealthStatus, repos_reorderServices as reorderServices, repos_replaceBindingIps as replaceBindingIps, repos_replaceBindingIpsWithMeta as replaceBindingIpsWithMeta, repos_replaceServiceIps as replaceServiceIps, repos_setBindingCnameTarget as setBindingCnameTarget, repos_setBindingDnsRecordId as setBindingDnsRecordId, repos_setBindingRoutingStrategy as setBindingRoutingStrategy, repos_setDnsSyncStatus as setDnsSyncStatus, repos_setDomainLastSynced as setDomainLastSynced, repos_setDomainTags as setDomainTags, repos_setServiceEnabled as setServiceEnabled, repos_setServiceGroup as setServiceGroup, repos_setServiceGroupEnabled as setServiceGroupEnabled, repos_setServiceLb as setServiceLb, repos_unlinkBindingRecord as unlinkBindingRecord, repos_unlinkGroupDnsRecord as unlinkGroupDnsRecord, repos_updateBindingDomain as updateBindingDomain, repos_updateBindingFields as updateBindingFields, repos_updateBindingLbConfig as updateBindingLbConfig, repos_updateDnsFields as updateDnsFields, repos_updateDomain as updateDomain, repos_updateDomainMonitorResult as updateDomainMonitorResult, repos_updateGroup as updateGroup, repos_updateHealthCheck as updateHealthCheck, repos_updateNode as updateNode, repos_updateService as updateService, repos_updateServiceGroup as updateServiceGroup, repos_updateSubdomain as updateSubdomain, repos_upsertCertificateCheck as upsertCertificateCheck, repos_upsertIpHealthStatus as upsertIpHealthStatus, repos_upsertSubdomain as upsertSubdomain }; + export { type repos_BindingIpMeta as BindingIpMeta, type repos_BindingLbPatch as BindingLbPatch, type repos_DnsListFilter as DnsListFilter, type repos_DomainMonitorRow as DomainMonitorRow, type repos_HealthAggregate as HealthAggregate, type repos_ServiceGroupLbPatch as ServiceGroupLbPatch, type repos_ServiceIpHealthRow as ServiceIpHealthRow, type repos_UpdateSubdomainPatch as UpdateSubdomainPatch, repos_addDomainTags as addDomainTags, repos_aggregateGroupScopeHealthByIds as aggregateGroupScopeHealthByIds, repos_aggregateIpHealthByRefs as aggregateIpHealthByRefs, repos_aggregateIpHealthByServiceIds as aggregateIpHealthByServiceIds, repos_bindingsToRemove as bindingsToRemove, repos_bumpBindingVersion as bumpBindingVersion, repos_countCertificatesByStatus as countCertificatesByStatus, repos_createDomain as createDomain, repos_createDomainMonitor as createDomainMonitor, repos_createGroup as createGroup, repos_createHealthCheck as createHealthCheck, repos_createNode as createNode, repos_createService as createService, repos_createServiceGroup as createServiceGroup, repos_createSubdomain as createSubdomain, repos_createSyncJob as createSyncJob, repos_deleteBinding as deleteBinding, repos_deleteBindingsExcept as deleteBindingsExcept, repos_deleteCertificatesNotIn as deleteCertificatesNotIn, repos_deleteDnsRecord as deleteDnsRecord, repos_deleteDomain as deleteDomain, repos_deleteDomainMonitor as deleteDomainMonitor, repos_deleteGroup as deleteGroup, repos_deleteHealthCheck as deleteHealthCheck, repos_deleteIpHealthStatusForIp as deleteIpHealthStatusForIp, repos_deleteIpHealthStatusForRef as deleteIpHealthStatusForRef, repos_deleteNode as deleteNode, repos_deleteService as deleteService, repos_deleteServiceGroup as deleteServiceGroup, repos_deleteSubdomain as deleteSubdomain, repos_ensureNode as ensureNode, repos_findBinding as findBinding, repos_findDnsByCfId as findDnsByCfId, repos_findDomainByZoneName as findDomainByZoneName, repos_findHealthCheckByCfId as findHealthCheckByCfId, repos_findNodeByAddress as findNodeByAddress, repos_findNodeByIp as findNodeByIp, repos_findSubdomainByDomainAndName as findSubdomainByDomainAndName, repos_finishSyncJob as finishSyncJob, repos_getBinding as getBinding, repos_getBindingView as getBindingView, repos_getCertificate as getCertificate, repos_getDnsRecord as getDnsRecord, repos_getDomain as getDomain, repos_getDomainMonitor as getDomainMonitor, repos_getGroup as getGroup, repos_getGroupWithStats as getGroupWithStats, repos_getHealthCheck as getHealthCheck, repos_getIpHealthStatusRow as getIpHealthStatusRow, repos_getNode as getNode, repos_getService as getService, repos_getServiceGroup as getServiceGroup, repos_getSubdomain as getSubdomain, repos_getSyncJob as getSyncJob, repos_insertBinding as insertBinding, repos_insertDnsRecord as insertDnsRecord, repos_insertNotificationLog as insertNotificationLog, repos_linkBindingRecord as linkBindingRecord, repos_linkGroupDnsRecord as linkGroupDnsRecord, repos_listAllBindings as listAllBindings, repos_listAllDomains as listAllDomains, repos_listAllNodes as listAllNodes, repos_listAllSubdomains as listAllSubdomains, repos_listBindingIps as listBindingIps, repos_listBindingIpsWithMeta as listBindingIpsWithMeta, repos_listBindingNodes as listBindingNodes, repos_listBindingsByDomain as listBindingsByDomain, repos_listBindingsByService as listBindingsByService, repos_listCertificates as listCertificates, repos_listDnsByDomain as listDnsByDomain, repos_listDnsRecords as listDnsRecords, repos_listDomainMonitorResults as listDomainMonitorResults, repos_listDomainMonitorResultsForDomain as listDomainMonitorResultsForDomain, repos_listDomainMonitors as listDomainMonitors, repos_listDomainTags as listDomainTags, repos_listDomains as listDomains, repos_listDomainsEnriched as listDomainsEnriched, repos_listEnabledDomainMonitors as listEnabledDomainMonitors, repos_listGroupDnsRecords as listGroupDnsRecords, repos_listGroups as listGroups, repos_listHealthCheckTargets as listHealthCheckTargets, repos_listHealthChecks as listHealthChecks, repos_listIpHealthByServiceIds as listIpHealthByServiceIds, repos_listIpHealthStatus as listIpHealthStatus, repos_listNodes as listNodes, repos_listNotificationLog as listNotificationLog, repos_listOriginIpsForFqdn as listOriginIpsForFqdn, repos_listRecordsForBinding as listRecordsForBinding, repos_listServiceGroups as listServiceGroups, repos_listServiceIps as listServiceIps, repos_listServices as listServices, repos_listServicesByGroup as listServicesByGroup, repos_listSubdomainsByDomain as listSubdomainsByDomain, repos_listUngroupedServices as listUngroupedServices, repos_markDnsPendingDelete as markDnsPendingDelete, repos_mergeHealthAggregates as mergeHealthAggregates, repos_pruneStaleIpHealthStatus as pruneStaleIpHealthStatus, repos_reorderServices as reorderServices, repos_replaceBindingIps as replaceBindingIps, repos_replaceBindingIpsWithMeta as replaceBindingIpsWithMeta, repos_replaceServiceIps as replaceServiceIps, repos_setBindingCnameTarget as setBindingCnameTarget, repos_setBindingDnsRecordId as setBindingDnsRecordId, repos_setBindingRoutingStrategy as setBindingRoutingStrategy, repos_setDnsSyncStatus as setDnsSyncStatus, repos_setDomainLastSynced as setDomainLastSynced, repos_setDomainTags as setDomainTags, repos_setServiceEnabled as setServiceEnabled, repos_setServiceGroup as setServiceGroup, repos_setServiceGroupEnabled as setServiceGroupEnabled, repos_setServiceLb as setServiceLb, repos_unlinkBindingRecord as unlinkBindingRecord, repos_unlinkGroupDnsRecord as unlinkGroupDnsRecord, repos_updateBindingDomain as updateBindingDomain, repos_updateBindingFields as updateBindingFields, repos_updateBindingLbConfig as updateBindingLbConfig, repos_updateDnsFields as updateDnsFields, repos_updateDomain as updateDomain, repos_updateDomainMonitorResult as updateDomainMonitorResult, repos_updateGroup as updateGroup, repos_updateHealthCheck as updateHealthCheck, repos_updateNode as updateNode, repos_updateService as updateService, repos_updateServiceGroup as updateServiceGroup, repos_updateSubdomain as updateSubdomain, repos_upsertCertificateCheck as upsertCertificateCheck, repos_upsertIpHealthStatus as upsertIpHealthStatus, repos_upsertSubdomain as upsertSubdomain }; } export { type AppSettingsDto, type AppSettingsPatch, type AppendAuditInput, ConflictError, type Db, type DnsListFilter, NotFoundError, type Sqlite, type UpdateSubdomainPatch, appSettings, appendAudit, auditLog, bindingNodes, certificates, createDb, createMemoryDb, dnsRecords, domainMonitorResults, domainMonitors, domainTags, domains, getAppSettings, getAppSettingsSecrets, groups, healthCheck, healthChecks, ipHealthStatus, listAudit, nodes, notificationLog, repos, resolveDatabasePath, runMigrations, schema, serviceBindingIps, serviceBindingRecords, serviceBindings, serviceGroupDnsRecords, serviceGroups, serviceIps, services, subdomains, syncJobs, touchVpsTrackerSync, updateAppSettings }; diff --git a/packages/db/dist/index.js b/packages/db/dist/index.js index 51b44c8..f03b1a3 100644 --- a/packages/db/dist/index.js +++ b/packages/db/dist/index.js @@ -631,6 +631,7 @@ __export(repos_exports, { listGroups: () => listGroups, listHealthCheckTargets: () => listHealthCheckTargets, listHealthChecks: () => listHealthChecks, + listIpHealthByServiceIds: () => listIpHealthByServiceIds, listIpHealthStatus: () => listIpHealthStatus, listNodes: () => listNodes, listNotificationLog: () => listNotificationLog, @@ -1822,6 +1823,39 @@ function aggregateIpHealthByServiceIds(db, serviceIds) { } return result; } +function listIpHealthByServiceIds(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, + ihs.ip AS ip, + ${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, ihs.ip + `); + for (const row of rows) { + const parsed = parseHealthAggregateRow({ + health_status: row.health_status, + health_latency_ms: row.health_latency_ms + }); + const list = result.get(row.service_id) ?? []; + list.push({ + ip: row.ip, + status: parsed.health_status, + latency_ms: parsed.health_latency_ms + }); + result.set(row.service_id, list); + } + return result; +} function mergeHealthAggregates(parts) { const rank = { unknown: 0, diff --git a/packages/db/src/repos.ts b/packages/db/src/repos.ts index 882b84b..6aed9e6 100644 --- a/packages/db/src/repos.ts +++ b/packages/db/src/repos.ts @@ -2070,6 +2070,55 @@ export function aggregateIpHealthByServiceIds( return result; } +export type ServiceIpHealthRow = { + ip: string; + status: IpHealthState; + latency_ms: number | null; +}; + +/** Per-IP binding-scope health, worst status if the same IP is on several bindings. */ +export function listIpHealthByServiceIds( + db: Db, + serviceIds: number[], +): Map { + const result = new Map(); + if (serviceIds.length === 0) return result; + const idList = sql.join( + serviceIds.map((id) => sql`${id}`), + sql`, `, + ); + const rows = db.all<{ + service_id: number; + ip: string; + health_status: string | null; + health_latency_ms: number | null; + }>(sql` + SELECT sb.service_id AS service_id, + ihs.ip AS ip, + ${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, ihs.ip + `); + for (const row of rows) { + const parsed = parseHealthAggregateRow({ + health_status: row.health_status, + health_latency_ms: row.health_latency_ms, + }); + const list = result.get(row.service_id) ?? []; + list.push({ + ip: row.ip, + status: parsed.health_status, + latency_ms: parsed.health_latency_ms, + }); + result.set(row.service_id, list); + } + return result; +} + export function mergeHealthAggregates( parts: Array, ): HealthAggregate { diff --git a/packages/shared/dist/index.d.ts b/packages/shared/dist/index.d.ts index 2608810..87d46d3 100644 --- a/packages/shared/dist/index.d.ts +++ b/packages/shared/dist/index.d.ts @@ -132,6 +132,7 @@ interface ServiceView$1 { domains: ServiceDomainBindingView[]; health_status: IpHealthState; health_latency_ms: number | null; + ip_health: ServiceIpHealth$1[]; } interface SyncJob { id: string; @@ -192,6 +193,11 @@ interface IpHealthStatus { last_checked_at: string | null; last_error: string | null; } +interface ServiceIpHealth$1 { + ip: string; + status: IpHealthState; + latency_ms: number | null; +} interface ServiceNode { id: number; service_id: number; @@ -372,6 +378,17 @@ declare const ipHealthStatusSchema: z.ZodObject<{ last_checked_at: z.ZodNullable; last_error: z.ZodNullable; }, z.core.$strip>; +declare const serviceIpHealthSchema: z.ZodObject<{ + ip: z.ZodString; + status: z.ZodEnum<{ + unknown: "unknown"; + up: "up"; + down: "down"; + degraded: "degraded"; + }>; + latency_ms: z.ZodNullable; +}, z.core.$strip>; +type ServiceIpHealth = z.infer; declare const groupSchema: z.ZodObject<{ id: z.ZodNumber; name: z.ZodString; @@ -619,6 +636,16 @@ declare const serviceViewSchema: z.ZodObject<{ degraded: "degraded"; }>>; health_latency_ms: z.ZodDefault>; + ip_health: z.ZodDefault; + latency_ms: z.ZodNullable; + }, z.core.$strip>>>; }, z.core.$strip>; declare const serviceGroupViewSchema: z.ZodObject<{ id: z.ZodNumber; @@ -752,6 +779,16 @@ declare const serviceGroupViewSchema: z.ZodObject<{ degraded: "degraded"; }>>; health_latency_ms: z.ZodDefault>; + ip_health: z.ZodDefault; + latency_ms: z.ZodNullable; + }, z.core.$strip>>>; }, z.core.$strip>>>; health_status: z.ZodDefault>; health_latency_ms: z.ZodDefault>; + ip_health: z.ZodDefault; + latency_ms: z.ZodNullable; + }, z.core.$strip>>>; }, z.core.$strip>>>; health_status: z.ZodDefault>; health_latency_ms: z.ZodDefault>; + ip_health: z.ZodDefault; + latency_ms: z.ZodNullable; + }, z.core.$strip>>>; }, z.core.$strip>>>; }, z.core.$strip>; declare const domainSchema: z.ZodObject<{ @@ -1835,4 +1892,4 @@ declare const ingestAuditEventSchema: z.ZodObject<{ }, z.core.$strip>; type IngestAuditEvent = z.infer; -export { AUDIT_SEVERITIES, AUDIT_SOURCE_APPS, AUDIT_TARGET_TYPES, type AppSettingsPatch, type AppSwitcherConfig, type AppSwitcherEntry, type AuditListQuery, type AuditLogEntry, type AuditSeverity, type AuditSourceApp, type AuditTargetType, type BulkUpdateDomainsInput, CERT_ERROR, CERT_EXPIRED, CERT_MONITORING_VALUES, CERT_MONITOR_AUTO, CERT_MONITOR_REQUIRED, CERT_MONITOR_SKIPPED, CERT_OK, CERT_UNKNOWN, CERT_WARNING, type CertMonitoring, type Certificate, type CfDnsRecord, type CfHealthCheck, type CfZone, type CfdmBindingSyncItem, type ChangeDomainInput, type ChangeIpInput, type CreateDnsRecordInput, type CreateDnsRecordPayload, type CreateDomainInput, type CreateDomainMonitorInput, type CreateGroupInput, type CreateOriginHealthCheckInput, type CreateServiceBindingInput, type CreateServiceGroupInput, type CreateServiceInput, type CreateServiceNodeInput, type CreateServiceWithConfigInput, type CreateSubdomainInput, type DnsRecord, type Domain, type DomainEnvironment, type DomainListItem, type DomainMonitor, type DomainMonitorResult, type DomainMonitorType, type Group, type GroupWithStats, type HealthCheckConfig, type HealthCheckProvider, type HealthCheckScope, type HealthCheckTarget, type HealthCheckType, type HealthStatusQuery, type IngestAuditEvent, type IpHealthState, type IpHealthStatus, type JwtClaims, type LbMode, type LoginInput, type LoginRequest, type LoginResponse, type NodeHealthState, type NotificationLog, type OriginHealthCheck, type OriginHealthCheckRecord, type ParsedFqdn, type PatchDnsRecordPayload, type ReorderServicesInput, SYNC_CONFLICT, SYNC_ERROR, SYNC_PENDING_DELETE, SYNC_PENDING_PUSH, SYNC_SYNCED, type Service, type ServiceBinding, type ServiceBindingView, type ServiceDomainBinding, type ServiceDomainBindingView, type ServiceGroup, type ServiceGroupView, type ServiceGroupsResponse, type ServiceNode, type ServiceNodeRecord, type ServiceOverview, type ServiceView, type Subdomain, type SubdomainRecord, type SyncJob, type ToggleEnabledInput, type UpdateDomainInput, type UpdateServiceConfigInput, type UpdateServiceGroupInput, type UpdateServiceNodeInput, type UpdateSubdomainInput, ValidationError, type VpsTrackerEvent, appSettingsPatchSchema, appSwitcherConfigSchema, appSwitcherEntrySchema, appSwitcherIconSchema, auditListQuerySchema, auditLogEntrySchema, auditSeveritySchema, auditSourceAppSchema, auditTargetTypeSchema, bindingToFqdn, bulkUpdateDomainsSchema, certMonitoringSchema, certStatusFromExpiry, certificateSchema, cfdmBindingSyncItemSchema, cfdmSyncBindingsBodySchema, changeDomainSchema, changeIpSchema, createDnsRecordSchema, createDomainMonitorSchema, createDomainSchema, createGroupSchema, createOriginHealthCheckSchema, createServiceBindingSchema, createServiceGroupSchema, createServiceNodeSchema, createServiceSchema, createServiceWithConfigSchema, createSubdomainSchema, dnsNameToSubdomainLabel, dnsRecordNamesMatch, dnsRecordSchema, domainEnvironmentSchema, domainListItemSchema, domainMonitorResultSchema, domainMonitorSchema, domainMonitorTypeSchema, domainSchema, fqdnToDisplay, groupSchema, groupWithStatsSchema, healthCheckConfigSchema, healthCheckProviderSchema, healthCheckScopeSchema, healthCheckTypeSchema, healthStatusQuerySchema, ingestAuditEventSchema, ipHealthStateSchema, ipHealthStatusSchema, isIpLiteral, isValidIpv4, lbModeSchema, loginSchema, nodeHealthStateSchema, normalizeDnsRecordName, notificationLogSchema, originHealthCheckSchema, parseFqdn, reorderServicesSchema, serviceBindingSchema, serviceDomainBindingSchema, serviceGroupSchema, serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, serviceNodeSchema, serviceSchema, serviceViewSchema, shouldMonitorService, subdomainLabelToFqdn, subdomainSchema, toggleEnabledSchema, updateDomainGroupSchema, updateDomainSchema, updateServiceConfigSchema, updateServiceGroupSchema, updateServiceNodeSchema, updateSubdomainSchema, validateDnsRecord, vpsTrackerEventSchema }; +export { AUDIT_SEVERITIES, AUDIT_SOURCE_APPS, AUDIT_TARGET_TYPES, type AppSettingsPatch, type AppSwitcherConfig, type AppSwitcherEntry, type AuditListQuery, type AuditLogEntry, type AuditSeverity, type AuditSourceApp, type AuditTargetType, type BulkUpdateDomainsInput, CERT_ERROR, CERT_EXPIRED, CERT_MONITORING_VALUES, CERT_MONITOR_AUTO, CERT_MONITOR_REQUIRED, CERT_MONITOR_SKIPPED, CERT_OK, CERT_UNKNOWN, CERT_WARNING, type CertMonitoring, type Certificate, type CfDnsRecord, type CfHealthCheck, type CfZone, type CfdmBindingSyncItem, type ChangeDomainInput, type ChangeIpInput, type CreateDnsRecordInput, type CreateDnsRecordPayload, type CreateDomainInput, type CreateDomainMonitorInput, type CreateGroupInput, type CreateOriginHealthCheckInput, type CreateServiceBindingInput, type CreateServiceGroupInput, type CreateServiceInput, type CreateServiceNodeInput, type CreateServiceWithConfigInput, type CreateSubdomainInput, type DnsRecord, type Domain, type DomainEnvironment, type DomainListItem, type DomainMonitor, type DomainMonitorResult, type DomainMonitorType, type Group, type GroupWithStats, type HealthCheckConfig, type HealthCheckProvider, type HealthCheckScope, type HealthCheckTarget, type HealthCheckType, type HealthStatusQuery, type IngestAuditEvent, type IpHealthState, type IpHealthStatus, type JwtClaims, type LbMode, type LoginInput, type LoginRequest, type LoginResponse, type NodeHealthState, type NotificationLog, type OriginHealthCheck, type OriginHealthCheckRecord, type ParsedFqdn, type PatchDnsRecordPayload, type ReorderServicesInput, SYNC_CONFLICT, SYNC_ERROR, SYNC_PENDING_DELETE, SYNC_PENDING_PUSH, SYNC_SYNCED, type Service, type ServiceBinding, type ServiceBindingView, type ServiceDomainBinding, type ServiceDomainBindingView, type ServiceGroup, type ServiceGroupView, type ServiceGroupsResponse, type ServiceIpHealth, type ServiceNode, type ServiceNodeRecord, type ServiceOverview, type ServiceView, type Subdomain, type SubdomainRecord, type SyncJob, type ToggleEnabledInput, type UpdateDomainInput, type UpdateServiceConfigInput, type UpdateServiceGroupInput, type UpdateServiceNodeInput, type UpdateSubdomainInput, ValidationError, type VpsTrackerEvent, appSettingsPatchSchema, appSwitcherConfigSchema, appSwitcherEntrySchema, appSwitcherIconSchema, auditListQuerySchema, auditLogEntrySchema, auditSeveritySchema, auditSourceAppSchema, auditTargetTypeSchema, bindingToFqdn, bulkUpdateDomainsSchema, certMonitoringSchema, certStatusFromExpiry, certificateSchema, cfdmBindingSyncItemSchema, cfdmSyncBindingsBodySchema, changeDomainSchema, changeIpSchema, createDnsRecordSchema, createDomainMonitorSchema, createDomainSchema, createGroupSchema, createOriginHealthCheckSchema, createServiceBindingSchema, createServiceGroupSchema, createServiceNodeSchema, createServiceSchema, createServiceWithConfigSchema, createSubdomainSchema, dnsNameToSubdomainLabel, dnsRecordNamesMatch, dnsRecordSchema, domainEnvironmentSchema, domainListItemSchema, domainMonitorResultSchema, domainMonitorSchema, domainMonitorTypeSchema, domainSchema, fqdnToDisplay, groupSchema, groupWithStatsSchema, healthCheckConfigSchema, healthCheckProviderSchema, healthCheckScopeSchema, healthCheckTypeSchema, healthStatusQuerySchema, ingestAuditEventSchema, ipHealthStateSchema, ipHealthStatusSchema, isIpLiteral, isValidIpv4, lbModeSchema, loginSchema, nodeHealthStateSchema, normalizeDnsRecordName, notificationLogSchema, originHealthCheckSchema, parseFqdn, reorderServicesSchema, serviceBindingSchema, serviceDomainBindingSchema, serviceGroupSchema, serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, serviceIpHealthSchema, serviceNodeSchema, serviceSchema, serviceViewSchema, shouldMonitorService, subdomainLabelToFqdn, subdomainSchema, toggleEnabledSchema, updateDomainGroupSchema, updateDomainSchema, updateServiceConfigSchema, updateServiceGroupSchema, updateServiceNodeSchema, updateSubdomainSchema, validateDnsRecord, vpsTrackerEventSchema }; diff --git a/packages/shared/dist/index.js b/packages/shared/dist/index.js index 5263f91..2b71bac 100644 --- a/packages/shared/dist/index.js +++ b/packages/shared/dist/index.js @@ -192,6 +192,11 @@ var ipHealthStatusSchema = z.object({ last_checked_at: z.string().nullable(), last_error: z.string().nullable() }); +var serviceIpHealthSchema = z.object({ + ip: z.string(), + status: ipHealthStateSchema, + latency_ms: z.number().nullable() +}); var groupSchema = z.object({ id: z.number(), name: z.string(), @@ -277,7 +282,8 @@ var serviceViewSchema = serviceSchema.extend({ ips: z.array(z.string()).default([]), domains: z.array(serviceDomainBindingSchema).default([]), health_status: ipHealthStateSchema.default("unknown"), - health_latency_ms: z.number().nullable().default(null) + health_latency_ms: z.number().nullable().default(null), + ip_health: z.array(serviceIpHealthSchema).default([]) }); var serviceGroupViewSchema = serviceGroupSchema.extend({ services: z.array(serviceViewSchema).default([]), @@ -843,6 +849,7 @@ export { serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, + serviceIpHealthSchema, serviceNodeSchema, serviceSchema, serviceViewSchema, diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts index ea2595f..ab35750 100644 --- a/packages/shared/src/schemas.ts +++ b/packages/shared/src/schemas.ts @@ -49,6 +49,14 @@ export const ipHealthStatusSchema = z.object({ export type IpHealthStatus = z.infer +export const serviceIpHealthSchema = z.object({ + ip: z.string(), + status: ipHealthStateSchema, + latency_ms: z.number().nullable(), +}) + +export type ServiceIpHealth = z.infer + export const groupSchema = z.object({ id: z.number(), name: z.string(), @@ -150,6 +158,7 @@ export const serviceViewSchema = serviceSchema.extend({ domains: z.array(serviceDomainBindingSchema).default([]), health_status: ipHealthStateSchema.default('unknown'), health_latency_ms: z.number().nullable().default(null), + ip_health: z.array(serviceIpHealthSchema).default([]), }) export const serviceGroupViewSchema = serviceGroupSchema.extend({ diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 635f10f..f723145 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -196,6 +196,7 @@ export interface ServiceView { domains: ServiceDomainBindingView[]; health_status: IpHealthState; health_latency_ms: number | null; + ip_health: ServiceIpHealth[]; } export interface GroupWithStats extends Group { @@ -293,6 +294,12 @@ export interface IpHealthStatus { last_error: string | null; } +export interface ServiceIpHealth { + ip: string; + status: IpHealthState; + latency_ms: number | null; +} + export interface ServiceNode { id: number; service_id: number;