Compare commits

...
1 Commits
Author SHA1 Message Date
Denozordec 4224db8eb3 feat(services): enhance service health management with IP health tracking
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 6s
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 55s
quality / api (push) Successful in 41s
CD / quality (push) Successful in 1m45s
CD / publish (push) Successful in 1m33s
- 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.
2026-08-19 14:57:19 +07:00
15 changed files with 346 additions and 157 deletions
@@ -307,6 +307,7 @@ async function buildView(db: Db, serviceId: number): Promise<ServiceView> {
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<ServiceGroupsResponse> {
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<ServiceGroupsResponse> {
...s,
health_status: "unknown" as const,
health_latency_ms: null,
ip_health: [],
},
);
@@ -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");
@@ -88,7 +88,11 @@ export function ServiceKanbanCard({
</div>
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-muted-foreground text-xs">IP</span>
<ServiceIpList copyable ips={service.ips ?? []} />
<ServiceIpList
copyable
ips={service.ips ?? []}
ipHealth={service.ip_health ?? []}
/>
</div>
</ItemContent>
+51 -3
View File
@@ -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({
</FieldGroup>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Health check</h3>
<HealthCheckConfigFields
idPrefix="service-health"
value={primaryHealthValue}
onChange={handlePrimaryHealthChange}
/>
</section>
<section className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-medium">Доп. FQDN</h3>
@@ -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 (
<div className={cn('flex min-w-0 items-center gap-1.5', className)}>
<TruncatedText
className={cn(
'text-muted-foreground min-w-0 font-mono text-xs',
textClassName,
)}
>
{visible.join(' · ')}
</TruncatedText>
<div className={cn('flex min-w-0 flex-col gap-1', className)}>
{visible.map((ip) => {
const health = healthByIp.get(ip)
return (
<div key={ip} className="flex min-w-0 items-center gap-1.5">
<HealthCheckBadge
status={health?.status ?? 'unknown'}
latencyMs={health?.latency_ms}
size="xs"
/>
<TruncatedText
className={cn(
'text-muted-foreground min-w-0 font-mono text-xs',
textClassName,
)}
>
{ip}
</TruncatedText>
{copyable ? <CopyFqdnButton value={ip} /> : null}
</div>
)
})}
{extraCount > 0 ? (
<TooltipProvider>
<Tooltip>
@@ -162,7 +178,7 @@ export function ServiceIpList({
<Badge
variant="outline"
size="xs"
className="shrink-0 tabular-nums"
className="w-fit shrink-0 tabular-nums"
/>
}
>
@@ -170,7 +186,7 @@ export function ServiceIpList({
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<ul className="flex flex-col gap-0.5 font-mono text-xs">
{ips.map((ip) => (
{ips.slice(VISIBLE_IP_LIMIT).map((ip) => (
<li key={ip}>{ip}</li>
))}
</ul>
@@ -178,7 +194,6 @@ export function ServiceIpList({
</Tooltip>
</TooltipProvider>
) : null}
{copyable ? <CopyFqdnButton value={copyValue} /> : null}
</div>
)
}
@@ -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 (
<Frame dense spacing="sm" className="h-full min-w-0">
<FrameHeader className="flex-row items-start justify-between gap-2">
<div className="flex min-w-0 items-start gap-2">
<Frame dense spacing="sm" className="h-full min-w-0 overflow-hidden">
<FrameHeader className="flex-row items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<IconTile
variant="elevated"
size="sm"
@@ -67,11 +66,6 @@ export function ServiceUnitCard({
</div>
</div>
<div className="flex shrink-0 items-center gap-1">
<HealthCheckBadge
status={service.health_status ?? 'unknown'}
latencyMs={service.health_latency_ms}
size="xs"
/>
<Switch
size="sm"
checked={service.enabled}
@@ -121,13 +115,13 @@ export function ServiceUnitCard({
</div>
</FrameHeader>
<FramePanel className="flex flex-col gap-1 pt-0 shadow-none!">
<ServiceFqdnList
<FramePanel className="flex min-w-0 flex-col gap-1.5 pt-0 shadow-none!">
<ServiceFqdnList copyable service={service} emptyLabel="Не задан" />
<ServiceIpList
copyable
service={service}
emptyLabel="Не задан"
ips={service.ips ?? []}
ipHealth={service.ip_health ?? []}
/>
<ServiceIpList copyable ips={service.ips ?? []} />
</FramePanel>
</Frame>
)
@@ -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<Filter[]>(() =>
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<string, number> = {}
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({
<Skeleton className="h-4 w-72" />
</FrameHeader>
<FramePanel className="flex flex-col gap-3 p-4">
<Skeleton className="h-9 w-full max-w-md" />
<Skeleton className="h-8 w-full max-w-md" />
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-36 w-full rounded-xl" />
))}
@@ -307,70 +276,28 @@ export function ServicesGroupedCatalog({
</FrameHeader>
) : null}
<FramePanel className="p-0 shadow-none!">
<div className="px-(--frame-panel-header-px) pt-(--frame-panel-header-py)">
<CountedLineTabs
tabs={ALL_TABS.map((t) => ({
id: t.id,
label: t.label,
count: tabCounts[t.id] ?? 0,
}))}
value={tab}
onValueChange={setTab}
<FramePanel className="flex flex-col gap-4">
<InputGroup className="max-w-md">
<InputGroupAddon>
<InputGroupText>
<SearchIcon aria-hidden />
</InputGroupText>
</InputGroupAddon>
<InputGroupInput
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Поиск по названию"
aria-label="Поиск по названию"
/>
</div>
<Separator />
<div className="flex flex-wrap items-center justify-between gap-3 px-(--frame-panel-header-px) py-(--frame-panel-header-py)">
<Filters
filters={filters}
fields={filterFields}
onChange={setFilters}
size="default"
trigger={
<Button type="button" variant="outline" aria-label="Фильтры">
<FilterIcon className="size-4" aria-hidden />
Фильтры
</Button>
}
/>
<Button
type="button"
variant="outline"
onClick={() => {
setTab('all')
setFilters(createDefaultServiceFilters())
}}
>
<FunnelXIcon className="size-4" aria-hidden />
Сбросить
</Button>
</div>
<Separator />
</InputGroup>
{units.length === 0 ? (
<div className="p-6">
<EmptyState
title="Нет совпадений"
description="Измените фильтры или вкладку."
action={
<Button
type="button"
variant="outline"
onClick={() => {
setTab('all')
setFilters(createDefaultServiceFilters())
}}
>
Сбросить
</Button>
}
/>
</div>
<EmptyState
title="Нет совпадений"
description="Измените запрос поиска."
/>
) : (
<div className="flex flex-col gap-6 px-(--frame-panel-header-px) py-(--frame-panel-header-py)">
<div className="flex flex-col gap-6">
{units.map((unit) => (
<ServiceCatalogSection
key={unit.id}
+7
View File
@@ -94,6 +94,12 @@ export const serviceDomainBindingSchema = z
: (binding.record_type ?? 'A'),
}))
export const serviceIpHealthSchema = z.object({
ip: z.string(),
status: z.enum(['up', 'down', 'degraded', 'unknown']),
latency_ms: z.number().nullable(),
})
export const serviceViewSchema = serviceSchema.extend({
subdomain: z.string().default(''),
enabled: z.coerce.boolean().default(false),
@@ -101,6 +107,7 @@ export const serviceViewSchema = serviceSchema.extend({
domains: z.array(serviceDomainBindingSchema).default([]),
health_status: z.enum(['up', 'down', 'degraded', 'unknown']).default('unknown'),
health_latency_ms: z.number().nullable().default(null),
ip_health: z.array(serviceIpHealthSchema).default([]),
})
export const serviceGroupViewSchema = serviceGroupSchema.extend({
+10 -1
View File
File diff suppressed because one or more lines are too long
+34
View File
@@ -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,
+49
View File
@@ -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<number, ServiceIpHealthRow[]> {
const result = new Map<number, ServiceIpHealthRow[]>();
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 | undefined | null>,
): HealthAggregate {
+58 -1
View File
@@ -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<z.ZodString>;
last_error: z.ZodNullable<z.ZodString>;
}, 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.ZodNumber>;
}, z.core.$strip>;
type ServiceIpHealth = z.infer<typeof serviceIpHealthSchema>;
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<z.ZodNullable<z.ZodNumber>>;
ip_health: z.ZodDefault<z.ZodArray<z.ZodObject<{
ip: z.ZodString;
status: z.ZodEnum<{
unknown: "unknown";
up: "up";
down: "down";
degraded: "degraded";
}>;
latency_ms: z.ZodNullable<z.ZodNumber>;
}, 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<z.ZodNullable<z.ZodNumber>>;
ip_health: z.ZodDefault<z.ZodArray<z.ZodObject<{
ip: z.ZodString;
status: z.ZodEnum<{
unknown: "unknown";
up: "up";
down: "down";
degraded: "degraded";
}>;
latency_ms: z.ZodNullable<z.ZodNumber>;
}, z.core.$strip>>>;
}, z.core.$strip>>>;
health_status: z.ZodDefault<z.ZodEnum<{
unknown: "unknown";
@@ -894,6 +931,16 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
degraded: "degraded";
}>>;
health_latency_ms: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
ip_health: z.ZodDefault<z.ZodArray<z.ZodObject<{
ip: z.ZodString;
status: z.ZodEnum<{
unknown: "unknown";
up: "up";
down: "down";
degraded: "degraded";
}>;
latency_ms: z.ZodNullable<z.ZodNumber>;
}, z.core.$strip>>>;
}, z.core.$strip>>>;
health_status: z.ZodDefault<z.ZodEnum<{
unknown: "unknown";
@@ -1002,6 +1049,16 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
degraded: "degraded";
}>>;
health_latency_ms: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
ip_health: z.ZodDefault<z.ZodArray<z.ZodObject<{
ip: z.ZodString;
status: z.ZodEnum<{
unknown: "unknown";
up: "up";
down: "down";
degraded: "degraded";
}>;
latency_ms: z.ZodNullable<z.ZodNumber>;
}, 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<typeof ingestAuditEventSchema>;
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 };
+8 -1
View File
@@ -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,
+9
View File
@@ -49,6 +49,14 @@ export const ipHealthStatusSchema = z.object({
export type IpHealthStatus = z.infer<typeof ipHealthStatusSchema>
export const serviceIpHealthSchema = z.object({
ip: z.string(),
status: ipHealthStateSchema,
latency_ms: z.number().nullable(),
})
export type ServiceIpHealth = z.infer<typeof serviceIpHealthSchema>
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({
+7
View File
@@ -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;