Compare commits

...
4 Commits
Author SHA1 Message Date
DenozordecandCursor 3da6de9311 fix(health): сузить тип переключателя источников для tsc -b
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / changes (push) Successful in 8s
quality / api (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Successful in 52s
CD / quality (push) Successful in 1m3s
CD / publish (push) Successful in 1m53s
ALL_TAB как литерал 'all', чтобы SourceTab не попадал в HealthCheckProvider[].

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 01:49:42 +07:00
DenozordecandCursor 7a8bacade9 feat(health): объединить источники, график и таймлайн в один блок мониторинга
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 7s
quality / changes (push) Successful in 9s
quality / api (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Failing after 49s
CD / quality (push) Failing after 1m1s
CD / publish (push) Skipped
Переключатель серий в шапке Frame меняет график uptime и смены статуса без отдельного списка плиток.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 01:44:16 +07:00
DenozordecandCursor 5d84c7bf6c fix(services): сохранять источники health-check и агрегацию в форме сервиса
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 9s
quality / changes (push) Successful in 12s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m14s
quality / api (push) Successful in 53s
CD / quality (push) Successful in 2m23s
CD / publish (push) Successful in 2m5s
GET отдавал sqlite-типы (0/1 и JSON-строка), Zod отклонял PATCH; форма сбрасывалась на refetch.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 01:03:00 +07:00
Denozordec 78811bc9b1 feat(uptime-chart): enhance tooltip functionality and refactor delta display
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / changes (push) Successful in 8s
quality / api (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Successful in 54s
CD / quality (push) Successful in 1m6s
CD / publish (push) Successful in 1m53s
- Added useEffect to set tooltip portal for improved rendering.
- Introduced UptimeDelta component to streamline delta display logic.
- Updated ChartTooltip to utilize ChartTooltipContent for better formatting of tooltip data.
- Refactored chart margin and overflow properties for improved layout consistency.
2026-08-20 00:49:27 +07:00
15 changed files with 692 additions and 194 deletions

No files matched your search

+84 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { serviceGroupsResponseSchema } from "@cfdm/shared"; import { serviceGroupsResponseSchema, updateServiceConfigSchema } from "@cfdm/shared";
import { repos } from "@cfdm/db"; import { repos } from "@cfdm/db";
import type { CloudflareClient } from "../src/lib/cf-client.js"; import type { CloudflareClient } from "../src/lib/cf-client.js";
import { buildApp } from "../src/app.js"; import { buildApp } from "../src/app.js";
@@ -51,6 +51,26 @@ async function authHeaders(app: Awaited<ReturnType<typeof buildApp>>) {
} }
describe("create service then list groups", () => { describe("create service then list groups", () => {
it("accepts sqlite-shaped health fields on service config PATCH", () => {
const parsed = updateServiceConfigSchema.parse({
domains: [
{
fqdn: "gw.example.com",
target_ips: ["1.2.3.4"],
health_check_enabled: 1,
health_check_verify_tls: 0,
health_check_providers: '["local","cloudflare"]',
health_check_aggregate: "majority",
},
],
});
expect(parsed.domains?.[0]?.health_check_enabled).toBe(true);
expect(parsed.domains?.[0]?.health_check_verify_tls).toBe(false);
expect(parsed.domains?.[0]?.health_check_providers).toEqual([
"local",
"cloudflare",
]);
});
it("create + updateConfig then listGroupViews parses with shared Zod schema", async () => { it("create + updateConfig then listGroupViews parses with shared Zod schema", async () => {
const app = await buildApp({ const app = await buildApp({
config: { ...loadConfig(), staticDir: null }, config: { ...loadConfig(), staticDir: null },
@@ -133,6 +153,69 @@ describe("create service then list groups", () => {
await app.close(); await app.close();
}); });
it("PATCH /services/:id persists health providers and aggregate", async () => {
const app = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(app);
const cf = mockCf();
repos.createDomain(app.db, null, "example.com", "zone-1");
const createRes = await app.inject({
method: "POST",
url: "/api/v1/services",
headers,
payload: { name: "GW", slug: "gw" },
});
expect(createRes.statusCode).toBe(200);
const created = createRes.json() as { id: number };
await updateConfig(app.db, cf, created.id, {
ips: ["1.2.3.4"],
domains: [
{
fqdn: "gw.example.com",
target_ips: ["1.2.3.4"],
health_check_enabled: true,
health_check_type: "tcp",
health_check_interval_sec: 30,
health_check_timeout_ms: 3000,
health_check_providers: ["local", "cloudflare"],
health_check_aggregate: "majority",
},
],
});
const stored = repos.listBindingsByService(app.db, created.id)[0]!;
expect(stored.health_check_enabled).toBe(true);
expect(Array.isArray(stored.health_check_providers)).toBe(true);
expect(stored.health_check_providers).toEqual(["local", "cloudflare"]);
expect(stored.health_check_aggregate).toBe("majority");
const getRes = await app.inject({
method: "GET",
url: `/api/v1/services/${created.id}`,
headers,
});
expect(getRes.statusCode).toBe(200);
const view = getRes.json() as {
domains: Array<{
health_check_enabled: boolean;
health_check_providers: string[];
health_check_aggregate: string;
}>;
};
expect(view.domains[0]?.health_check_enabled).toBe(true);
expect(view.domains[0]?.health_check_providers).toEqual([
"local",
"cloudflare",
]);
expect(view.domains[0]?.health_check_aggregate).toBe("majority");
await app.close();
});
it("POST /services returns resolved ServiceView with numeric id", async () => { it("POST /services returns resolved ServiceView with numeric id", async () => {
const app = await buildApp({ const app = await buildApp({
config: { ...loadConfig(), staticDir: null }, config: { ...loadConfig(), staticDir: null },
@@ -28,7 +28,7 @@ import {
type HealthAggregate, type HealthAggregate,
type HealthProvider, type HealthProvider,
} from '@/components/reui-kit/health-source-tiles' } from '@/components/reui-kit/health-source-tiles'
import { uniqueHealthProviders } from '@cfdm/shared' import { parseHealthProviders } from '@cfdm/shared'
export type LbMode = 'round_robin' | 'failover' | 'weighted' export type LbMode = 'round_robin' | 'failover' | 'weighted'
export type HealthCheckType = 'tcp' | 'http' export type HealthCheckType = 'tcp' | 'http'
@@ -116,10 +116,10 @@ export function HealthCheckConfigFields({
onChange({ ...value, ...next }) onChange({ ...value, ...next })
} }
const providers = const providers = parseHealthProviders(
value.providers?.length > 0 value.providers,
? uniqueHealthProviders(value.providers) value.provider ?? 'local',
: uniqueHealthProviders([value.provider ?? 'local']) )
const aggregate = value.aggregate ?? 'majority' const aggregate = value.aggregate ?? 'majority'
const isHttp = value.type === 'http' const isHttp = value.type === 'http'
const rowClass = 'gap-3 px-0 py-3' const rowClass = 'gap-3 px-0 py-3'
@@ -14,7 +14,7 @@ import {
} from '@cfdm/ui/components/item' } from '@cfdm/ui/components/item'
import { HealthCheckBadge } from '@/components/health-check-badge' import { HealthCheckBadge } from '@/components/health-check-badge'
import { cn } from '@cfdm/ui/lib/utils' import { cn } from '@cfdm/ui/lib/utils'
import type { HealthCheckAggregate, HealthCheckProvider } from '@cfdm/shared' import { parseHealthProviders, type HealthCheckAggregate, type HealthCheckProvider } from '@cfdm/shared'
import type { HealthLogStatus } from '@/lib/health-log' import type { HealthLogStatus } from '@/lib/health-log'
export type HealthProvider = HealthCheckProvider export type HealthProvider = HealthCheckProvider
@@ -189,7 +189,7 @@ export function HealthSourceTiles({
value: HealthProvider[] value: HealthProvider[]
onChange: (next: HealthProvider[]) => void onChange: (next: HealthProvider[]) => void
}) { }) {
const selected = value.length > 0 ? value : (['local'] as HealthProvider[]) const selected = parseHealthProviders(value)
function toggle(id: HealthProvider) { function toggle(id: HealthProvider) {
if (selected.includes(id)) { if (selected.includes(id)) {
+1 -1
View File
@@ -1,4 +1,4 @@
export { UptimeChart, type UptimeProbe, type UptimePeriodKey } from './uptime-chart' export { UptimeChart, type UptimeProbe, type UptimePeriodKey, probeUptimePercent, lastProbeLatency } from './uptime-chart'
export { ServiceHealthMonitor } from './service-health-monitor' export { ServiceHealthMonitor } from './service-health-monitor'
export { applyFiltersToData, getActiveFilters, renderSingleSelectedLabel } from './filter-utils' export { applyFiltersToData, getActiveFilters, renderSingleSelectedLabel } from './filter-utils'
export { CertStatusChart, GroupDomainsChart } from './dashboard-analytics' export { CertStatusChart, GroupDomainsChart } from './dashboard-analytics'
@@ -1,6 +1,8 @@
import { useMemo, useState } from 'react' import { useMemo, useState, type ReactNode } from 'react'
import { LayersIcon } from 'lucide-react'
import { HealthTimeline } from '@/components/health/health-timeline' import { HealthTimeline } from '@/components/health/health-timeline'
import { HealthCheckBadge } from '@/components/health-check-badge'
import { import {
Frame, Frame,
FrameDescription, FrameDescription,
@@ -8,41 +10,144 @@ import {
FramePanel, FramePanel,
FrameTitle, FrameTitle,
} from '@/components/reui/frame' } from '@/components/reui/frame'
import { UptimeChart, UPTIME_PERIODS, type UptimePeriodKey } from '@/components/reui-kit/uptime-chart' import { IconTile } from '@/components/reui/icon-tile'
import { Badge } from '@/components/reui/badge'
import {
lastProbeLatency,
probeUptimePercent,
UptimeChart,
UPTIME_PERIODS,
type UptimePeriodKey,
} from '@/components/reui-kit/uptime-chart'
import { HEALTH_PROVIDER_ITEMS } from '@/components/reui-kit/health-source-tiles'
import { import {
collapseStatusChanges, collapseStatusChanges,
filterByPeriod, filterByPeriod,
filterByProviders, filterByProviders,
type HealthLogProbe, type HealthLogProbe,
type HealthLogStatus,
} from '@/lib/health-log' } from '@/lib/health-log'
import { cn } from '@cfdm/ui/lib/utils'
import type { HealthCheckProvider } from '@cfdm/shared' import type { HealthCheckProvider } from '@cfdm/shared'
const ALL_TAB = 'all' as const
type SourceTab = typeof ALL_TAB | HealthCheckProvider
function formatUptime(value: number | null): string {
if (value == null) return '—'
return `${value.toFixed(value >= 99.95 ? 2 : 1)}%`
}
function formatLatency(ms: number | null): string {
if (ms == null) return 'нет проб'
return `${ms} мс`
}
function statusTileClass(status: HealthLogStatus | undefined): string {
if (status === 'down') return 'text-destructive'
if (status === 'degraded') return 'text-warning'
if (status === 'up') return 'text-success'
return 'text-muted-foreground'
}
/** /**
* Combined uptime chart + status-change timeline (solution-analytics-8 DNA). * Единый блок мониторинга: переключатель источников (dashboard-4) + график
* Preview: https://reui.io/preview/base/solution-analytics-8 · https://reui.io/preview/base/chart-17 * (chart-17) + таймлайн смен статуса (solution-ai-ops-1 / timeline).
* Docs: https://reui.io/docs/components/base/frame · https://reui.io/docs/components/base/timeline *
* Preview: https://reui.io/preview/base/dashboard-4
* Preview: https://reui.io/preview/base/chart-17
* Preview: https://reui.io/preview/base/solution-ai-ops-1
* Docs: https://reui.io/docs/components/base/frame
* Docs: https://reui.io/docs/components/base/icon-tile
* Docs: https://reui.io/docs/components/base/timeline
* Docs: https://reui.io/docs/components/base/badge
*/ */
export function ServiceHealthMonitor({ export function ServiceHealthMonitor({
items, items,
selectedProviders, enabledProviders,
statuses,
isLoading = false, isLoading = false,
}: { }: {
items: HealthLogProbe[] items: HealthLogProbe[]
selectedProviders: readonly HealthCheckProvider[] enabledProviders: readonly HealthCheckProvider[]
statuses: Partial<Record<HealthCheckProvider, HealthLogStatus>>
isLoading?: boolean isLoading?: boolean
}) { }) {
const [period, setPeriod] = useState<UptimePeriodKey>('5D') const [period, setPeriod] = useState<UptimePeriodKey>('5D')
const [source, setSource] = useState<SourceTab>(ALL_TAB)
const days = UPTIME_PERIODS.find((entry) => entry.key === period)?.days ?? 5 const days = UPTIME_PERIODS.find((entry) => entry.key === period)?.days ?? 5
const enabledItems = useMemo(
() => HEALTH_PROVIDER_ITEMS.filter((item) => enabledProviders.includes(item.id)),
[enabledProviders],
)
const selectedProviders = useMemo((): HealthCheckProvider[] => {
if (source === ALL_TAB) return [...enabledProviders]
if (enabledProviders.includes(source)) return [source]
return [...enabledProviders]
}, [enabledProviders, source])
const periodItems = useMemo(
() => filterByPeriod(items, days),
[items, days],
)
const filtered = useMemo( const filtered = useMemo(
() => filterByProviders(filterByPeriod(items, days), selectedProviders), () => filterByProviders(periodItems, selectedProviders),
[items, days, selectedProviders], [periodItems, selectedProviders],
) )
const changes = useMemo(() => collapseStatusChanges(filtered), [filtered]) const changes = useMemo(() => collapseStatusChanges(filtered), [filtered])
const showAllTab = enabledItems.length > 1
const tabCount = enabledItems.length + (showAllTab ? 1 : 0)
const activeSource: SourceTab =
source === ALL_TAB || enabledProviders.includes(source)
? source
: ALL_TAB
return ( return (
<Frame stacked spacing="sm" className="min-w-0 w-full"> <Frame stacked spacing="sm" className="min-w-0 w-full">
<FrameHeader className="p-0!">
<div
className={cn(
'grid',
tabCount <= 2 && 'grid-cols-2',
tabCount === 3 && 'grid-cols-1 sm:grid-cols-3',
tabCount >= 4 && 'grid-cols-2',
)}
>
{showAllTab ? (
<SourceMetricButton
selected={activeSource === ALL_TAB}
icon={<LayersIcon />}
iconClassName="text-muted-foreground"
label="Все источники"
value={formatUptime(probeUptimePercent(periodItems))}
hint={`${periodItems.length} проб`}
onSelect={() => setSource(ALL_TAB)}
/>
) : null}
{enabledItems.map((item) => {
const series = filterByProviders(periodItems, [item.id])
return (
<SourceMetricButton
key={item.id}
selected={activeSource === item.id}
icon={item.icon}
iconClassName={item.iconClassName}
label={item.title}
value={formatUptime(probeUptimePercent(series))}
hint={formatLatency(lastProbeLatency(series))}
status={statuses[item.id] ?? 'unknown'}
onSelect={() => setSource(item.id)}
/>
)
})}
</div>
</FrameHeader>
<UptimeChart <UptimeChart
items={filtered} items={filtered}
isLoading={isLoading} isLoading={isLoading}
@@ -50,12 +155,15 @@ export function ServiceHealthMonitor({
onPeriodChange={setPeriod} onPeriodChange={setPeriod}
skipPeriodFilter skipPeriodFilter
embedded embedded
hideHeader
/> />
<FramePanel className="flex flex-col gap-3"> <FramePanel className="flex flex-col gap-3">
<FrameHeader className="px-0 py-0"> <FrameHeader className="px-0 py-0">
<FrameTitle>Смены статуса</FrameTitle> <FrameTitle>Смены статуса</FrameTitle>
<FrameDescription> <FrameDescription>
Только переходы up / degraded / down · Cloudflare = Worker, не Health Checks API Только переходы up / degraded / down · Cloudflare = Worker, не Health
Checks API
</FrameDescription> </FrameDescription>
</FrameHeader> </FrameHeader>
<HealthTimeline <HealthTimeline
@@ -77,3 +185,65 @@ export function ServiceHealthMonitor({
</Frame> </Frame>
) )
} }
function SourceMetricButton({
selected,
icon,
iconClassName,
label,
value,
hint,
status,
onSelect,
}: {
selected: boolean
icon: ReactNode
iconClassName: string
label: string
value: string
hint: string
status?: HealthLogStatus
onSelect: () => void
}) {
return (
<button
type="button"
aria-pressed={selected}
aria-label={`${label}: ${value}`}
onClick={onSelect}
className={cn(
'focus-visible:ring-ring/50 hover:bg-muted/40 relative flex min-w-0 items-start gap-3 border-e border-b p-4 text-start transition-colors last:border-e-0 focus-visible:ring-2 focus-visible:outline-none sm:border-b-0',
selected && 'bg-muted/40',
)}
>
<IconTile
variant="elevated"
className={cn(
'size-10.5',
status ? statusTileClass(status) : iconClassName,
)}
aria-hidden="true"
>
{icon}
</IconTile>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="flex items-center justify-between gap-2">
<span className="text-muted-foreground text-sm font-medium">{label}</span>
{status ? (
<HealthCheckBadge status={status} size="xs" />
) : (
<Badge variant="outline" size="sm">
{hint}
</Badge>
)}
</span>
<span className="text-foreground text-2xl leading-none font-bold tabular-nums">
{value}
</span>
{status ? (
<span className="text-muted-foreground text-xs">{hint}</span>
) : null}
</span>
</button>
)
}
+124 -81
View File
@@ -1,4 +1,4 @@
import { useId, useMemo, useState } from 'react' import { useEffect, useId, useMemo, useState } from 'react'
import { ActivityIcon, InfoIcon, TrendingDownIcon, TrendingUpIcon } from 'lucide-react' import { ActivityIcon, InfoIcon, TrendingDownIcon, TrendingUpIcon } from 'lucide-react'
import { Area, AreaChart, XAxis } from 'recharts' import { Area, AreaChart, XAxis } from 'recharts'
@@ -12,6 +12,7 @@ import { Button } from '@cfdm/ui/components/button'
import { import {
ChartContainer, ChartContainer,
ChartTooltip, ChartTooltip,
ChartTooltipContent,
type ChartConfig, type ChartConfig,
} from '@cfdm/ui/components/chart' } from '@cfdm/ui/components/chart'
import { Tabs, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs' import { Tabs, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs'
@@ -87,22 +88,45 @@ function deltaPercent(points: ChartPoint[]): number | null {
return next - prev return next - prev
} }
interface UptimeTooltipProps { function UptimeDelta({ delta }: { delta: number }) {
active?: boolean if (Math.abs(delta) < 0.05) {
payload?: Array<{ payload: ChartPoint }> return <span className="text-muted-foreground">без изменений за период</span>
}
if (delta > 0) {
return (
<>
<TrendingUpIcon className="text-success size-4" aria-hidden="true" />
<span className="text-success font-medium">+{delta.toFixed(1)} п.п.</span>
<span className="text-muted-foreground">с начала периода</span>
</>
)
}
return (
<>
<TrendingDownIcon className="text-destructive size-4" aria-hidden="true" />
<span className="text-destructive font-medium">{delta.toFixed(1)} п.п.</span>
<span className="text-muted-foreground">с начала периода</span>
</>
)
} }
function UptimeTooltip({ active, payload }: UptimeTooltipProps) { export function probeUptimePercent(items: UptimeProbe[]): number | null {
if (!active || !payload?.[0]) return null return uptimePercent(toSeries(items))
const point = payload[0].payload }
return (
<div className="bg-popover text-popover-foreground rounded-md px-3 py-2 text-sm shadow-md"> export function lastProbeLatency(items: UptimeProbe[]): number | null {
<p className="font-medium tabular-nums"> if (items.length === 0) return null
{point.latency} мс · {point.ok ? 'OK' : 'Down'} const latest = [...items].sort(
</p> (a, b) => probeTime(b.checked_at) - probeTime(a.checked_at),
<p className="text-muted-foreground text-xs">{point.period}</p> )[0]
</div> return latest?.latency_ms ?? null
) }
function formatUptime(value: number | null): string {
if (value == null) return '—'
return `${value.toFixed(value >= 99.95 ? 2 : 1)}%`
} }
interface UptimeChartProps { interface UptimeChartProps {
@@ -112,6 +136,8 @@ interface UptimeChartProps {
onPeriodChange?: (period: UptimePeriodKey) => void onPeriodChange?: (period: UptimePeriodKey) => void
skipPeriodFilter?: boolean skipPeriodFilter?: boolean
embedded?: boolean embedded?: boolean
/** dashboard-4: chrome живёт в родительском FrameHeader (переключатель серий). */
hideHeader?: boolean
} }
export function UptimeChart({ export function UptimeChart({
@@ -121,12 +147,18 @@ export function UptimeChart({
onPeriodChange, onPeriodChange,
skipPeriodFilter = false, skipPeriodFilter = false,
embedded = false, embedded = false,
hideHeader = false,
}: UptimeChartProps) { }: UptimeChartProps) {
const gradientId = useId().replace(/:/g, '') const gradientId = useId().replace(/:/g, '')
const [internalPeriod, setInternalPeriod] = useState<UptimePeriodKey>('5D') const [internalPeriod, setInternalPeriod] = useState<UptimePeriodKey>('5D')
const [tooltipPortal, setTooltipPortal] = useState<HTMLElement | null>(null)
const period = periodProp ?? internalPeriod const period = periodProp ?? internalPeriod
const days = UPTIME_PERIODS.find((entry) => entry.key === period)?.days ?? 5 const days = UPTIME_PERIODS.find((entry) => entry.key === period)?.days ?? 5
useEffect(() => {
setTooltipPortal(document.body)
}, [])
function handlePeriodChange(next: UptimePeriodKey) { function handlePeriodChange(next: UptimePeriodKey) {
onPeriodChange?.(next) onPeriodChange?.(next)
if (periodProp == null) setInternalPeriod(next) if (periodProp == null) setInternalPeriod(next)
@@ -143,43 +175,45 @@ export function UptimeChart({
const panel = ( const panel = (
<FramePanel className="flex flex-col gap-6"> <FramePanel className="flex flex-col gap-6">
<div className="border-border flex items-center justify-between gap-2 border-b border-dashed pb-4"> {hideHeader ? null : (
<div className="flex items-center gap-2.5"> <div className="border-border flex items-center justify-between gap-2 border-b border-dashed pb-4">
<IconTile <div className="flex items-center gap-2.5">
variant="elevated" <IconTile
className={`size-10.5 ${tileClass}`} variant="elevated"
aria-hidden="true" className={`size-10.5 ${tileClass}`}
> aria-hidden="true"
<ActivityIcon />
</IconTile>
<div className="flex flex-col justify-center">
<h3 className="text-base font-semibold">Uptime</h3>
<p className="text-muted-foreground text-sm">
Пробы health-check за период
</p>
</div>
</div>
<TooltipProvider delay={150}>
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="О графике uptime"
className="text-muted-foreground/70 -mr-1"
size="icon-sm"
type="button"
variant="ghost"
/>
}
> >
<InfoIcon data-icon="inline-start" aria-hidden="true" /> <ActivityIcon />
</TooltipTrigger> </IconTile>
<TooltipContent side="top" sideOffset={8}> <div className="flex flex-col justify-center">
<p>Доля успешных проб и задержка (мс) по журналу health-log.</p> <h3 className="text-base font-semibold">Uptime</h3>
</TooltipContent> <p className="text-muted-foreground text-sm">
</Tooltip> Пробы health-check за период
</TooltipProvider> </p>
</div> </div>
</div>
<TooltipProvider delay={150}>
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="О графике uptime"
className="text-muted-foreground/70 -mr-1"
size="icon-sm"
type="button"
variant="ghost"
/>
}
>
<InfoIcon data-icon="inline-start" aria-hidden="true" />
</TooltipTrigger>
<TooltipContent side="top" sideOffset={8}>
<p>Доля успешных проб и задержка (мс) по журналу health-log.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
{isLoading ? ( {isLoading ? (
<div className="bg-muted h-40 w-full animate-pulse rounded-xl" /> <div className="bg-muted h-40 w-full animate-pulse rounded-xl" />
@@ -195,42 +229,28 @@ export function UptimeChart({
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="text-foreground text-3xl font-semibold tabular-nums"> <div className="text-foreground text-3xl font-semibold tabular-nums">
{uptime == null ? '—' : `${uptime.toFixed(uptime >= 99.95 ? 2 : 1)}%`} {formatUptime(uptime)}
</div> </div>
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
{delta == null ? ( {delta == null ? (
<Badge variant="outline" size="sm"> <Badge variant="outline" size="sm">
{points.length} проб {points.length} проб
</Badge> </Badge>
) : delta >= 0 ? (
<>
<TrendingUpIcon className="text-success size-4" aria-hidden="true" />
<span className="text-success font-medium">
+{delta.toFixed(1)} п.п.
</span>
<span className="text-muted-foreground">к первой половине окна</span>
</>
) : ( ) : (
<> <UptimeDelta delta={delta} />
<TrendingDownIcon className="text-destructive size-4" aria-hidden="true" />
<span className="text-destructive font-medium">
{delta.toFixed(1)} п.п.
</span>
<span className="text-muted-foreground">к первой половине окна</span>
</>
)} )}
</div> </div>
</div> </div>
<div className="h-40 w-full"> <div className="h-40 w-full overflow-visible">
<ChartContainer <ChartContainer
config={chartConfig} config={chartConfig}
className="h-full w-full overflow-hidden rounded-b-xl" className="h-full w-full overflow-visible rounded-b-xl"
initialDimension={{ width: 320, height: 160 }} initialDimension={{ width: 320, height: 160 }}
> >
<AreaChart <AreaChart
data={points} data={points}
margin={{ top: 10, left: 0, right: 0, bottom: 0 }} margin={{ top: 16, left: 8, right: 8, bottom: 4 }}
> >
<defs> <defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1"> <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
@@ -247,33 +267,56 @@ export function UptimeChart({
</linearGradient> </linearGradient>
</defs> </defs>
<XAxis dataKey="period" hide /> <XAxis dataKey="period" hide />
<ChartTooltip content={<UptimeTooltip />} /> <ChartTooltip
cursor={{ stroke: 'var(--border)', strokeDasharray: '4 4' }}
allowEscapeViewBox={{ x: true, y: true }}
portal={tooltipPortal ?? undefined}
wrapperStyle={{ zIndex: 50, pointerEvents: 'none' }}
content={
<ChartTooltipContent
formatter={(value, _name, item) => {
const point = item.payload as ChartPoint | undefined
const ping = Number(value)
return (
<div className="flex flex-1 items-center justify-between gap-4">
<span className="text-muted-foreground">
{point?.ok === false ? 'Down' : 'Пинг'}
</span>
<span className="text-foreground font-mono font-medium tabular-nums">
{Number.isFinite(ping) ? `${ping} мс` : '—'}
</span>
</div>
)
}}
/>
}
/>
<Area <Area
dataKey="latency" dataKey="latency"
name="latency"
type="natural" type="natural"
fill={`url(#${gradientId})`} fill={`url(#${gradientId})`}
stroke="var(--color-latency)" stroke="var(--color-latency)"
strokeWidth={2} strokeWidth={2}
isAnimationActive={false}
dot={(dotProps) => { dot={(dotProps) => {
const { cx, cy, payload, index } = dotProps as { const { cx, cy, payload, index } = dotProps
cx?: number
cy?: number
index?: number
payload?: ChartPoint
}
if (cx == null || cy == null) return <g key={index} /> if (cx == null || cy == null) return <g key={index} />
const fill = payload?.ok const point = payload as ChartPoint | undefined
? 'var(--color-latency)'
: 'var(--destructive)'
return ( return (
<circle <circle
key={index} key={index}
cx={cx} cx={cx}
cy={cy} cy={cy}
r={4} r={4}
fill={fill} fill={
point?.ok
? 'var(--color-latency)'
: 'var(--destructive)'
}
stroke="var(--background)" stroke="var(--background)"
strokeWidth={2} strokeWidth={2}
pointerEvents="none"
/> />
) )
}} }}
+10 -7
View File
@@ -18,6 +18,7 @@ import type {
ServiceView, ServiceView,
UpdateServiceConfigInput, UpdateServiceConfigInput,
} from '@/lib/schemas' } from '@/lib/schemas'
import { parseHealthProviders } from '@cfdm/shared'
import { bindingToFqdn, parseFqdn } from '@/lib/parse-fqdn' import { bindingToFqdn, parseFqdn } from '@/lib/parse-fqdn'
import { Badge } from '@/components/reui/badge' import { Badge } from '@/components/reui/badge'
import { toast } from 'sonner' import { toast } from 'sonner'
@@ -108,19 +109,19 @@ function toBindingDrafts(service: ServiceView): ServiceBindingDraft[] {
target_cname: binding.target_cname ?? '', target_cname: binding.target_cname ?? '',
lb_mode: binding.lb_mode, lb_mode: binding.lb_mode,
health: { health: {
enabled: binding.health_check_enabled, enabled: Boolean(binding.health_check_enabled),
type: binding.health_check_type === 'http' ? 'http' : 'tcp', type: binding.health_check_type === 'http' ? 'http' : 'tcp',
port: binding.health_check_port, port: binding.health_check_port,
path: binding.health_check_path, path: binding.health_check_path,
expected_status: binding.health_check_expected_status, expected_status: binding.health_check_expected_status,
interval_sec: binding.health_check_interval_sec, interval_sec: binding.health_check_interval_sec,
timeout_ms: binding.health_check_timeout_ms, timeout_ms: binding.health_check_timeout_ms,
verify_tls: binding.health_check_verify_tls ?? false, verify_tls: Boolean(binding.health_check_verify_tls),
provider: binding.health_check_provider ?? 'local', provider: binding.health_check_provider ?? 'local',
providers: providers: parseHealthProviders(
binding.health_check_providers?.length > 0 binding.health_check_providers,
? binding.health_check_providers binding.health_check_provider ?? 'local',
: [binding.health_check_provider ?? 'local'], ),
aggregate: binding.health_check_aggregate ?? 'majority', aggregate: binding.health_check_aggregate ?? 'majority',
}, },
target_ip_weights: binding.target_ip_weights ?? {}, target_ip_weights: binding.target_ip_weights ?? {},
@@ -232,6 +233,8 @@ export function ServiceEditSheet({
[groups], [groups],
) )
// Reset only when the sheet opens or the service id changes.
// Health polling replaces `service` by identity and would wipe unsaved settings.
useEffect(() => { useEffect(() => {
if (!open) return if (!open) return
if (mode === 'edit' && service) { if (mode === 'edit' && service) {
@@ -260,7 +263,7 @@ export function ServiceEditSheet({
setLbWeight(1) setLbWeight(1)
setLbPriority(1) setLbPriority(1)
} }
}, [open, mode, service, defaultGroupId]) }, [open, mode, service?.id, defaultGroupId])
const zoneHints = useMemo( const zoneHints = useMemo(
() => knownDomains.map((domain) => domain.zone_name), () => knownDomains.map((domain) => domain.zone_name),
+2 -2
View File
@@ -291,14 +291,14 @@ const lbModeSchema = z.enum(['round_robin', 'failover', 'weighted'])
const healthCheckTypeSchema = z.enum(['tcp', 'http', 'ping', 'dns']) const healthCheckTypeSchema = z.enum(['tcp', 'http', 'ping', 'dns'])
const healthCheckConfigFields = { const healthCheckConfigFields = {
health_check_enabled: z.boolean().optional(), health_check_enabled: z.coerce.boolean().optional(),
health_check_type: healthCheckTypeSchema.optional(), health_check_type: healthCheckTypeSchema.optional(),
health_check_port: z.number().int().min(1).max(65535).nullable().optional(), health_check_port: z.number().int().min(1).max(65535).nullable().optional(),
health_check_path: z.string().nullable().optional(), health_check_path: z.string().nullable().optional(),
health_check_expected_status: z.number().int().min(100).max(599).nullable().optional(), health_check_expected_status: z.number().int().min(100).max(599).nullable().optional(),
health_check_interval_sec: z.number().int().min(5).max(3600).optional(), health_check_interval_sec: z.number().int().min(5).max(3600).optional(),
health_check_timeout_ms: z.number().int().min(100).max(30000).optional(), health_check_timeout_ms: z.number().int().min(100).max(30000).optional(),
health_check_verify_tls: z.boolean().optional(), health_check_verify_tls: z.coerce.boolean().optional(),
health_check_provider: z.enum(['local', 'cloudflare', 'globalping']).optional(), health_check_provider: z.enum(['local', 'cloudflare', 'globalping']).optional(),
health_check_providers: z.array(z.enum(['local', 'cloudflare', 'globalping'])).min(1).optional(), health_check_providers: z.array(z.enum(['local', 'cloudflare', 'globalping'])).min(1).optional(),
health_check_aggregate: z.enum(['any', 'all', 'majority']).optional(), health_check_aggregate: z.enum(['any', 'all', 'majority']).optional(),
@@ -28,7 +28,6 @@ import {
} from '@/components/services/service-detail-grid' } from '@/components/services/service-detail-grid'
import { LbModeTile } from '@/components/services/service-unit-card' import { LbModeTile } from '@/components/services/service-unit-card'
import { import {
HealthProviderStatusTiles,
KpiStatGrid, KpiStatGrid,
ServiceHealthMonitor, ServiceHealthMonitor,
} from '@/components/reui-kit' } from '@/components/reui-kit'
@@ -45,7 +44,6 @@ import {
providerHealthStatuses, providerHealthStatuses,
} from '@/lib/health-log' } from '@/lib/health-log'
import type { ServiceView, UpdateServiceConfigInput } from '@/lib/schemas' import type { ServiceView, UpdateServiceConfigInput } from '@/lib/schemas'
import type { HealthCheckProvider } from '@cfdm/shared'
import { import {
createServiceNode, createServiceNode,
deleteServiceNode, deleteServiceNode,
@@ -112,9 +110,6 @@ function ServiceDetailPage() {
const [editOpen, setEditOpen] = useState(false) const [editOpen, setEditOpen] = useState(false)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [selectedProviders, setSelectedProviders] = useState<
HealthCheckProvider[] | null
>(null)
const [togglingIp, setTogglingIp] = useState<string | null>(null) const [togglingIp, setTogglingIp] = useState<string | null>(null)
const [changeIp, setChangeIp] = useState<{ const [changeIp, setChangeIp] = useState<{
bindingId: number bindingId: number
@@ -234,11 +229,6 @@ function ServiceDetailPage() {
() => enabledHealthProviders(service?.domains ?? []), () => enabledHealthProviders(service?.domains ?? []),
[service], [service],
) )
const activeProviders =
selectedProviders?.filter((provider) => enabledProviders.includes(provider)) ??
enabledProviders
const effectiveProviders =
activeProviders.length > 0 ? activeProviders : enabledProviders
const providerStatuses = useMemo( const providerStatuses = useMemo(
() => providerHealthStatuses(logItems, enabledProviders), () => providerHealthStatuses(logItems, enabledProviders),
[logItems, enabledProviders], [logItems, enabledProviders],
@@ -333,20 +323,14 @@ function ServiceDetailPage() {
]} ]}
/> />
<HealthProviderStatusTiles
enabled={enabledProviders}
selected={effectiveProviders}
statuses={providerStatuses}
onChange={setSelectedProviders}
/>
<section <section
aria-label="Мониторинг" aria-label="Мониторинг"
className="grid min-w-0 items-start gap-2 @3xl:grid-cols-2" className="grid min-w-0 items-start gap-2 @3xl:grid-cols-2"
> >
<ServiceHealthMonitor <ServiceHealthMonitor
items={logItems} items={logItems}
selectedProviders={effectiveProviders} enabledProviders={enabledProviders}
statuses={providerStatuses}
isLoading={logQuery.isLoading} isLoading={logQuery.isLoading}
/> />
<Frame dense spacing="sm" className="min-w-0 w-full"> <Frame dense spacing="sm" className="min-w-0 w-full">
+74 -1
View File
@@ -1613,6 +1613,25 @@ declare const serviceBindings: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
}, {}, { }, {}, {
length: number | undefined; length: number | undefined;
}>; }>;
cert_monitoring: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "cert_monitoring";
tableName: "service_bindings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
routing_strategy: drizzle_orm_sqlite_core.SQLiteColumn<{ routing_strategy: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "routing_strategy"; name: "routing_strategy";
tableName: "service_bindings"; tableName: "service_bindings";
@@ -2682,6 +2701,23 @@ declare const certificates: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
identity: undefined; identity: undefined;
generated: undefined; generated: undefined;
}, {}, {}>; }, {}, {}>;
service_id: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "service_id";
tableName: "certificates";
dataType: "number";
columnType: "SQLiteInteger";
data: number;
driverParam: number;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
hostname: drizzle_orm_sqlite_core.SQLiteColumn<{ hostname: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "hostname"; name: "hostname";
tableName: "certificates"; tableName: "certificates";
@@ -6296,6 +6332,25 @@ declare const schema: {
}, {}, { }, {}, {
length: number | undefined; length: number | undefined;
}>; }>;
cert_monitoring: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "cert_monitoring";
tableName: "service_bindings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
routing_strategy: drizzle_orm_sqlite_core.SQLiteColumn<{ routing_strategy: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "routing_strategy"; name: "routing_strategy";
tableName: "service_bindings"; tableName: "service_bindings";
@@ -7365,6 +7420,23 @@ declare const schema: {
identity: undefined; identity: undefined;
generated: undefined; generated: undefined;
}, {}, {}>; }, {}, {}>;
service_id: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "service_id";
tableName: "certificates";
dataType: "number";
columnType: "SQLiteInteger";
data: number;
driverParam: number;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
hostname: drizzle_orm_sqlite_core.SQLiteColumn<{ hostname: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "hostname"; name: "hostname";
tableName: "certificates"; tableName: "certificates";
@@ -9677,6 +9749,7 @@ interface BindingLbPatch {
health_check_provider?: HealthCheckProvider; health_check_provider?: HealthCheckProvider;
health_check_providers?: HealthCheckProvider[]; health_check_providers?: HealthCheckProvider[];
health_check_aggregate?: HealthCheckAggregate; health_check_aggregate?: HealthCheckAggregate;
cert_monitoring?: string;
} }
declare function updateBindingLbConfig(db: Db, bindingId: number, patch: BindingLbPatch): void; declare function updateBindingLbConfig(db: Db, bindingId: number, patch: BindingLbPatch): void;
declare function setBindingCnameTarget(db: Db, bindingId: number, target: string | null): void; declare function setBindingCnameTarget(db: Db, bindingId: number, target: string | null): void;
@@ -9702,7 +9775,7 @@ declare function deleteBindingsExcept(db: Db, serviceId: number, keepIds: number
declare function deleteBinding(db: Db, id: number): void; declare function deleteBinding(db: Db, id: number): void;
declare function listCertificates(db: Db, status?: string): Certificate[]; declare function listCertificates(db: Db, status?: string): Certificate[];
declare function getCertificate(db: Db, id: number): Certificate; declare function getCertificate(db: Db, id: number): Certificate;
declare function upsertCertificateCheck(db: Db, domainId: number, subdomainId: number | null, hostname: string, expiresAt: string | null, status: string, lastError: string | null): Certificate; declare function upsertCertificateCheck(db: Db, domainId: number, subdomainId: number | null, hostname: string, expiresAt: string | null, status: string, lastError: string | null, serviceId?: number | null): Certificate;
declare function countCertificatesByStatus(db: Db): Array<[string, number]>; declare function countCertificatesByStatus(db: Db): Array<[string, number]>;
declare function deleteCertificatesNotIn(db: Db, hostnames: string[]): number; declare function deleteCertificatesNotIn(db: Db, hostnames: string[]): number;
declare function createSyncJob(db: Db, id: string, domainId: number | null): void; declare function createSyncJob(db: Db, id: string, domainId: number | null): void;
+58 -11
View File
@@ -123,6 +123,7 @@ var serviceBindings = sqliteTable(
health_check_provider: text("health_check_provider").notNull().default("local"), health_check_provider: text("health_check_provider").notNull().default("local"),
health_check_providers: text("health_check_providers").notNull().default('["local"]'), health_check_providers: text("health_check_providers").notNull().default('["local"]'),
health_check_aggregate: text("health_check_aggregate").notNull().default("majority"), health_check_aggregate: text("health_check_aggregate").notNull().default("majority"),
cert_monitoring: text("cert_monitoring").notNull().default("auto"),
routing_strategy: text("routing_strategy").notNull().default("round_robin"), routing_strategy: text("routing_strategy").notNull().default("round_robin"),
operation_version: integer("operation_version").notNull().default(0), operation_version: integer("operation_version").notNull().default(0),
created_at: text("created_at").notNull().default(sql`datetime('now')`), created_at: text("created_at").notNull().default(sql`datetime('now')`),
@@ -228,6 +229,9 @@ var certificates = sqliteTable("certificates", {
subdomain_id: integer("subdomain_id").references(() => subdomains.id, { subdomain_id: integer("subdomain_id").references(() => subdomains.id, {
onDelete: "set null" onDelete: "set null"
}), }),
service_id: integer("service_id").references(() => services.id, {
onDelete: "set null"
}),
hostname: text("hostname").notNull().unique(), hostname: text("hostname").notNull().unique(),
expires_at: text("expires_at"), expires_at: text("expires_at"),
last_checked_at: text("last_checked_at"), last_checked_at: text("last_checked_at"),
@@ -1221,15 +1225,16 @@ function mapServiceBinding(row) {
cname_target: row.cname_target, cname_target: row.cname_target,
dns_record_id: row.dns_record_id, dns_record_id: row.dns_record_id,
lb_mode: row.lb_mode, lb_mode: row.lb_mode,
health_check_enabled: row.health_check_enabled, health_check_enabled: Boolean(row.health_check_enabled),
health_check_type: row.health_check_type, health_check_type: row.health_check_type,
health_check_port: row.health_check_port, health_check_port: row.health_check_port,
health_check_path: row.health_check_path, health_check_path: row.health_check_path,
health_check_expected_status: row.health_check_expected_status, health_check_expected_status: row.health_check_expected_status,
health_check_interval_sec: row.health_check_interval_sec, health_check_interval_sec: row.health_check_interval_sec,
health_check_timeout_ms: row.health_check_timeout_ms, health_check_timeout_ms: row.health_check_timeout_ms,
health_check_verify_tls: row.health_check_verify_tls, health_check_verify_tls: Boolean(row.health_check_verify_tls),
...mapHealthFields(row), ...mapHealthFields(row),
cert_monitoring: row.cert_monitoring ?? "auto",
routing_strategy: row.routing_strategy, routing_strategy: row.routing_strategy,
operation_version: row.operation_version, operation_version: row.operation_version,
created_at: row.created_at, created_at: row.created_at,
@@ -1625,6 +1630,8 @@ function updateBindingLbConfig(db, bindingId, patch) {
update.health_check_timeout_ms = patch.health_check_timeout_ms; update.health_check_timeout_ms = patch.health_check_timeout_ms;
if (patch.health_check_verify_tls !== void 0) if (patch.health_check_verify_tls !== void 0)
update.health_check_verify_tls = patch.health_check_verify_tls; update.health_check_verify_tls = patch.health_check_verify_tls;
if (patch.cert_monitoring !== void 0)
update.cert_monitoring = patch.cert_monitoring;
Object.assign(update, healthProviderColumns(patch)); Object.assign(update, healthProviderColumns(patch));
db.update(serviceBindings).set(update).where(eq3(serviceBindings.id, bindingId)).run(); db.update(serviceBindings).set(update).where(eq3(serviceBindings.id, bindingId)).run();
} }
@@ -1685,7 +1692,7 @@ var SERVICE_BINDING_SELECT_COLUMNS = `sb.id, sb.domain_id, sb.service_id, sb.hos
sb.lb_mode, sb.health_check_enabled, sb.health_check_type, sb.health_check_port, sb.lb_mode, sb.health_check_enabled, sb.health_check_type, sb.health_check_port,
sb.health_check_path, sb.health_check_expected_status, sb.health_check_interval_sec, sb.health_check_path, sb.health_check_expected_status, sb.health_check_interval_sec,
sb.health_check_timeout_ms, sb.health_check_verify_tls, sb.health_check_provider, sb.health_check_timeout_ms, sb.health_check_verify_tls, sb.health_check_provider,
sb.health_check_providers, sb.health_check_aggregate, sb.cname_target, sb.health_check_providers, sb.health_check_aggregate, sb.cert_monitoring, sb.cname_target,
d.zone_name, d.group_id, g.name AS group_name, d.zone_name, d.group_id, g.name AS group_name,
s.name AS service_name, s.slug AS service_slug, s.name AS service_name, s.slug AS service_slug,
dr.content AS target_ip, dr.sync_status, dr.content AS target_ip, dr.sync_status,
@@ -1733,6 +1740,7 @@ function enrichServiceBindingView(db, row) {
return { return {
...row, ...row,
cname_target: row.cname_target ?? null, cname_target: row.cname_target ?? null,
cert_monitoring: row.cert_monitoring ?? "auto",
...mapHealthFields(row), ...mapHealthFields(row),
target_ips, target_ips,
target_ip: target_ips[0] ?? null, target_ip: target_ips[0] ?? null,
@@ -1765,11 +1773,15 @@ function listBindingsByDomain(db, domainId) {
`).map((row) => enrichServiceBindingView(db, row)); `).map((row) => enrichServiceBindingView(db, row));
} }
function listBindingsByService(db, serviceId) { function listBindingsByService(db, serviceId) {
return db.all(sql2` const rows = db.all(sql2`
SELECT sb.*, d.zone_name FROM service_bindings sb SELECT sb.*, d.zone_name FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id JOIN domains d ON d.id = sb.domain_id
WHERE sb.service_id = ${serviceId} WHERE sb.service_id = ${serviceId}
`); `);
return rows.map((row) => ({
...mapServiceBinding(row),
zone_name: row.zone_name
}));
} }
function getBinding(db, id) { function getBinding(db, id) {
const row = db.select().from(serviceBindings).where(eq3(serviceBindings.id, id)).get(); const row = db.select().from(serviceBindings).where(eq3(serviceBindings.id, id)).get();
@@ -1839,23 +1851,57 @@ function deleteBinding(db, id) {
const result = db.delete(serviceBindings).where(eq3(serviceBindings.id, id)).run(); const result = db.delete(serviceBindings).where(eq3(serviceBindings.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`service binding ${id}`); if (result.changes === 0) throw new NotFoundError(`service binding ${id}`);
} }
var CERTIFICATE_SELECT = `c.id, c.domain_id, c.subdomain_id, c.service_id, c.hostname,
c.expires_at, c.last_checked_at, c.last_error, c.status, c.created_at, c.updated_at,
s.name AS service_name`;
function mapCertificate(row) {
return {
id: row.id,
domain_id: row.domain_id,
subdomain_id: row.subdomain_id,
service_id: row.service_id ?? null,
service_name: row.service_name ?? null,
hostname: row.hostname,
expires_at: row.expires_at,
last_checked_at: row.last_checked_at,
last_error: row.last_error,
status: row.status,
created_at: row.created_at,
updated_at: row.updated_at
};
}
function listCertificates(db, status) { function listCertificates(db, status) {
if (status) { const rows = status ? db.all(sql2`
return db.select().from(certificates).where(eq3(certificates.status, status)).orderBy(asc(certificates.expires_at)).all(); SELECT ${sql2.raw(CERTIFICATE_SELECT)}
} FROM certificates c
return db.select().from(certificates).orderBy(asc(certificates.expires_at)).all(); LEFT JOIN services s ON s.id = c.service_id
WHERE c.status = ${status}
ORDER BY c.expires_at ASC
`) : db.all(sql2`
SELECT ${sql2.raw(CERTIFICATE_SELECT)}
FROM certificates c
LEFT JOIN services s ON s.id = c.service_id
ORDER BY c.expires_at ASC
`);
return rows.map(mapCertificate);
} }
function getCertificate(db, id) { function getCertificate(db, id) {
const row = db.select().from(certificates).where(eq3(certificates.id, id)).get(); const row = db.all(sql2`
SELECT ${sql2.raw(CERTIFICATE_SELECT)}
FROM certificates c
LEFT JOIN services s ON s.id = c.service_id
WHERE c.id = ${id}
`)[0];
if (!row) throw new NotFoundError(`certificate ${id}`); if (!row) throw new NotFoundError(`certificate ${id}`);
return row; return mapCertificate(row);
} }
function upsertCertificateCheck(db, domainId, subdomainId, hostname, expiresAt, status, lastError) { function upsertCertificateCheck(db, domainId, subdomainId, hostname, expiresAt, status, lastError, serviceId) {
const existing = db.select().from(certificates).where(eq3(certificates.hostname, hostname)).get(); const existing = db.select().from(certificates).where(eq3(certificates.hostname, hostname)).get();
if (existing) { if (existing) {
db.update(certificates).set({ db.update(certificates).set({
domain_id: domainId, domain_id: domainId,
subdomain_id: subdomainId, subdomain_id: subdomainId,
service_id: serviceId === void 0 ? existing.service_id : serviceId,
expires_at: expiresAt, expires_at: expiresAt,
last_checked_at: sql2`datetime('now')`, last_checked_at: sql2`datetime('now')`,
last_error: lastError, last_error: lastError,
@@ -1867,6 +1913,7 @@ function upsertCertificateCheck(db, domainId, subdomainId, hostname, expiresAt,
const id = db.insert(certificates).values({ const id = db.insert(certificates).values({
domain_id: domainId, domain_id: domainId,
subdomain_id: subdomainId, subdomain_id: subdomainId,
service_id: serviceId ?? null,
hostname, hostname,
expires_at: expiresAt, expires_at: expiresAt,
last_checked_at: sql2`datetime('now')`, last_checked_at: sql2`datetime('now')`,
+7 -3
View File
@@ -842,14 +842,14 @@ function mapServiceBinding(
cname_target: row.cname_target, cname_target: row.cname_target,
dns_record_id: row.dns_record_id, dns_record_id: row.dns_record_id,
lb_mode: row.lb_mode as LbMode, lb_mode: row.lb_mode as LbMode,
health_check_enabled: row.health_check_enabled, health_check_enabled: Boolean(row.health_check_enabled),
health_check_type: row.health_check_type as HealthCheckType, health_check_type: row.health_check_type as HealthCheckType,
health_check_port: row.health_check_port, health_check_port: row.health_check_port,
health_check_path: row.health_check_path, health_check_path: row.health_check_path,
health_check_expected_status: row.health_check_expected_status, health_check_expected_status: row.health_check_expected_status,
health_check_interval_sec: row.health_check_interval_sec, health_check_interval_sec: row.health_check_interval_sec,
health_check_timeout_ms: row.health_check_timeout_ms, health_check_timeout_ms: row.health_check_timeout_ms,
health_check_verify_tls: row.health_check_verify_tls, health_check_verify_tls: Boolean(row.health_check_verify_tls),
...mapHealthFields(row), ...mapHealthFields(row),
cert_monitoring: row.cert_monitoring ?? "auto", cert_monitoring: row.cert_monitoring ?? "auto",
routing_strategy: row.routing_strategy as LbMode, routing_strategy: row.routing_strategy as LbMode,
@@ -1777,11 +1777,15 @@ export function listBindingsByDomain(db: Db, domainId: number): ServiceBindingVi
} }
export function listBindingsByService(db: Db, serviceId: number): Array<ServiceBinding & { zone_name: string }> { export function listBindingsByService(db: Db, serviceId: number): Array<ServiceBinding & { zone_name: string }> {
return db.all(sql` const rows = db.all<typeof serviceBindings.$inferSelect & { zone_name: string }>(sql`
SELECT sb.*, d.zone_name FROM service_bindings sb SELECT sb.*, d.zone_name FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id JOIN domains d ON d.id = sb.domain_id
WHERE sb.service_id = ${serviceId} WHERE sb.service_id = ${serviceId}
`); `);
return rows.map((row) => ({
...mapServiceBinding(row),
zone_name: row.zone_name,
}));
} }
export function getBinding(db: Db, id: number): ServiceBinding { export function getBinding(db: Db, id: number): ServiceBinding {
+103 -39
View File
@@ -96,6 +96,7 @@ interface ServiceBinding {
health_check_provider: HealthCheckProvider; health_check_provider: HealthCheckProvider;
health_check_providers: HealthCheckProvider[]; health_check_providers: HealthCheckProvider[];
health_check_aggregate: HealthCheckAggregate; health_check_aggregate: HealthCheckAggregate;
cert_monitoring: string;
routing_strategy: LbMode; routing_strategy: LbMode;
operation_version: number; operation_version: number;
created_at: string; created_at: string;
@@ -129,6 +130,7 @@ interface ServiceBindingView {
health_check_provider: HealthCheckProvider; health_check_provider: HealthCheckProvider;
health_check_providers: HealthCheckProvider[]; health_check_providers: HealthCheckProvider[];
health_check_aggregate: HealthCheckAggregate; health_check_aggregate: HealthCheckAggregate;
cert_monitoring: string;
sync_status: string | null; sync_status: string | null;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
@@ -156,6 +158,7 @@ interface ServiceDomainBindingView {
health_check_provider: HealthCheckProvider; health_check_provider: HealthCheckProvider;
health_check_providers: HealthCheckProvider[]; health_check_providers: HealthCheckProvider[];
health_check_aggregate: HealthCheckAggregate; health_check_aggregate: HealthCheckAggregate;
cert_monitoring: string;
sync_status: string | null; sync_status: string | null;
} }
interface ServiceView$1 { interface ServiceView$1 {
@@ -424,11 +427,11 @@ declare const healthCheckAggregateSchema: z.ZodEnum<{
all: "all"; all: "all";
majority: "majority"; majority: "majority";
}>; }>;
declare const healthCheckProvidersSchema: z.ZodPipe<z.ZodArray<z.ZodEnum<{ declare const healthCheckProvidersSchema: z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>;
declare const healthCheckScopeSchema: z.ZodEnum<{ declare const healthCheckScopeSchema: z.ZodEnum<{
binding: "binding"; binding: "binding";
group: "group"; group: "group";
@@ -560,11 +563,11 @@ declare const serviceGroupSchema: z.ZodObject<{
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodCatch<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodCatch<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodCatch<z.ZodEnum<{ health_check_aggregate: z.ZodCatch<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
@@ -624,16 +627,21 @@ declare const serviceDomainBindingSchema: z.ZodPipe<z.ZodObject<{
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodCatch<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodCatch<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodCatch<z.ZodEnum<{ health_check_aggregate: z.ZodCatch<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
majority: "majority"; majority: "majority";
}>>; }>>;
cert_monitoring: z.ZodDefault<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
sync_status: z.ZodDefault<z.ZodNullable<z.ZodString>>; sync_status: z.ZodDefault<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>, z.ZodTransform<{ }, z.core.$strip>, z.ZodTransform<{
target_ips: string[]; target_ips: string[];
@@ -658,6 +666,7 @@ declare const serviceDomainBindingSchema: z.ZodPipe<z.ZodObject<{
health_check_provider: "local" | "cloudflare" | "globalping"; health_check_provider: "local" | "cloudflare" | "globalping";
health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"]; health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"];
health_check_aggregate: "any" | "all" | "majority"; health_check_aggregate: "any" | "all" | "majority";
cert_monitoring: "auto" | "required" | "skipped";
sync_status: string | null; sync_status: string | null;
target_ip?: string | null | undefined; target_ip?: string | null | undefined;
}, { }, {
@@ -679,6 +688,7 @@ declare const serviceDomainBindingSchema: z.ZodPipe<z.ZodObject<{
health_check_provider: "local" | "cloudflare" | "globalping"; health_check_provider: "local" | "cloudflare" | "globalping";
health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"]; health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"];
health_check_aggregate: "any" | "all" | "majority"; health_check_aggregate: "any" | "all" | "majority";
cert_monitoring: "auto" | "required" | "skipped";
sync_status: string | null; sync_status: string | null;
target_ips?: string[] | undefined; target_ips?: string[] | undefined;
target_ip?: string | null | undefined; target_ip?: string | null | undefined;
@@ -737,16 +747,21 @@ declare const serviceViewSchema: z.ZodObject<{
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodCatch<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodCatch<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodCatch<z.ZodEnum<{ health_check_aggregate: z.ZodCatch<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
majority: "majority"; majority: "majority";
}>>; }>>;
cert_monitoring: z.ZodDefault<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
sync_status: z.ZodDefault<z.ZodNullable<z.ZodString>>; sync_status: z.ZodDefault<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>, z.ZodTransform<{ }, z.core.$strip>, z.ZodTransform<{
target_ips: string[]; target_ips: string[];
@@ -771,6 +786,7 @@ declare const serviceViewSchema: z.ZodObject<{
health_check_provider: "local" | "cloudflare" | "globalping"; health_check_provider: "local" | "cloudflare" | "globalping";
health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"]; health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"];
health_check_aggregate: "any" | "all" | "majority"; health_check_aggregate: "any" | "all" | "majority";
cert_monitoring: "auto" | "required" | "skipped";
sync_status: string | null; sync_status: string | null;
target_ip?: string | null | undefined; target_ip?: string | null | undefined;
}, { }, {
@@ -792,6 +808,7 @@ declare const serviceViewSchema: z.ZodObject<{
health_check_provider: "local" | "cloudflare" | "globalping"; health_check_provider: "local" | "cloudflare" | "globalping";
health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"]; health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"];
health_check_aggregate: "any" | "all" | "majority"; health_check_aggregate: "any" | "all" | "majority";
cert_monitoring: "auto" | "required" | "skipped";
sync_status: string | null; sync_status: string | null;
target_ips?: string[] | undefined; target_ips?: string[] | undefined;
target_ip?: string | null | undefined; target_ip?: string | null | undefined;
@@ -869,11 +886,11 @@ declare const serviceGroupViewSchema: z.ZodObject<{
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodCatch<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodCatch<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodCatch<z.ZodEnum<{ health_check_aggregate: z.ZodCatch<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
@@ -932,16 +949,21 @@ declare const serviceGroupViewSchema: z.ZodObject<{
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodCatch<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodCatch<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodCatch<z.ZodEnum<{ health_check_aggregate: z.ZodCatch<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
majority: "majority"; majority: "majority";
}>>; }>>;
cert_monitoring: z.ZodDefault<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
sync_status: z.ZodDefault<z.ZodNullable<z.ZodString>>; sync_status: z.ZodDefault<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>, z.ZodTransform<{ }, z.core.$strip>, z.ZodTransform<{
target_ips: string[]; target_ips: string[];
@@ -966,6 +988,7 @@ declare const serviceGroupViewSchema: z.ZodObject<{
health_check_provider: "local" | "cloudflare" | "globalping"; health_check_provider: "local" | "cloudflare" | "globalping";
health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"]; health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"];
health_check_aggregate: "any" | "all" | "majority"; health_check_aggregate: "any" | "all" | "majority";
cert_monitoring: "auto" | "required" | "skipped";
sync_status: string | null; sync_status: string | null;
target_ip?: string | null | undefined; target_ip?: string | null | undefined;
}, { }, {
@@ -987,6 +1010,7 @@ declare const serviceGroupViewSchema: z.ZodObject<{
health_check_provider: "local" | "cloudflare" | "globalping"; health_check_provider: "local" | "cloudflare" | "globalping";
health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"]; health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"];
health_check_aggregate: "any" | "all" | "majority"; health_check_aggregate: "any" | "all" | "majority";
cert_monitoring: "auto" | "required" | "skipped";
sync_status: string | null; sync_status: string | null;
target_ips?: string[] | undefined; target_ips?: string[] | undefined;
target_ip?: string | null | undefined; target_ip?: string | null | undefined;
@@ -1073,11 +1097,11 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodCatch<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodCatch<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodCatch<z.ZodEnum<{ health_check_aggregate: z.ZodCatch<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
@@ -1136,16 +1160,21 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodCatch<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodCatch<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodCatch<z.ZodEnum<{ health_check_aggregate: z.ZodCatch<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
majority: "majority"; majority: "majority";
}>>; }>>;
cert_monitoring: z.ZodDefault<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
sync_status: z.ZodDefault<z.ZodNullable<z.ZodString>>; sync_status: z.ZodDefault<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>, z.ZodTransform<{ }, z.core.$strip>, z.ZodTransform<{
target_ips: string[]; target_ips: string[];
@@ -1170,6 +1199,7 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
health_check_provider: "local" | "cloudflare" | "globalping"; health_check_provider: "local" | "cloudflare" | "globalping";
health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"]; health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"];
health_check_aggregate: "any" | "all" | "majority"; health_check_aggregate: "any" | "all" | "majority";
cert_monitoring: "auto" | "required" | "skipped";
sync_status: string | null; sync_status: string | null;
target_ip?: string | null | undefined; target_ip?: string | null | undefined;
}, { }, {
@@ -1191,6 +1221,7 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
health_check_provider: "local" | "cloudflare" | "globalping"; health_check_provider: "local" | "cloudflare" | "globalping";
health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"]; health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"];
health_check_aggregate: "any" | "all" | "majority"; health_check_aggregate: "any" | "all" | "majority";
cert_monitoring: "auto" | "required" | "skipped";
sync_status: string | null; sync_status: string | null;
target_ips?: string[] | undefined; target_ips?: string[] | undefined;
target_ip?: string | null | undefined; target_ip?: string | null | undefined;
@@ -1291,16 +1322,21 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodCatch<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodCatch<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodCatch<z.ZodEnum<{ health_check_aggregate: z.ZodCatch<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
majority: "majority"; majority: "majority";
}>>; }>>;
cert_monitoring: z.ZodDefault<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
sync_status: z.ZodDefault<z.ZodNullable<z.ZodString>>; sync_status: z.ZodDefault<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>, z.ZodTransform<{ }, z.core.$strip>, z.ZodTransform<{
target_ips: string[]; target_ips: string[];
@@ -1325,6 +1361,7 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
health_check_provider: "local" | "cloudflare" | "globalping"; health_check_provider: "local" | "cloudflare" | "globalping";
health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"]; health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"];
health_check_aggregate: "any" | "all" | "majority"; health_check_aggregate: "any" | "all" | "majority";
cert_monitoring: "auto" | "required" | "skipped";
sync_status: string | null; sync_status: string | null;
target_ip?: string | null | undefined; target_ip?: string | null | undefined;
}, { }, {
@@ -1346,6 +1383,7 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
health_check_provider: "local" | "cloudflare" | "globalping"; health_check_provider: "local" | "cloudflare" | "globalping";
health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"]; health_check_providers: ("local" | "cloudflare" | "globalping")[] | readonly ["local"];
health_check_aggregate: "any" | "all" | "majority"; health_check_aggregate: "any" | "all" | "majority";
cert_monitoring: "auto" | "required" | "skipped";
sync_status: string | null; sync_status: string | null;
target_ips?: string[] | undefined; target_ips?: string[] | undefined;
target_ip?: string | null | undefined; target_ip?: string | null | undefined;
@@ -1471,6 +1509,11 @@ declare const serviceBindingSchema: z.ZodPipe<z.ZodObject<{
health_check_interval_sec: z.ZodDefault<z.ZodNumber>; health_check_interval_sec: z.ZodDefault<z.ZodNumber>;
health_check_timeout_ms: z.ZodDefault<z.ZodNumber>; health_check_timeout_ms: z.ZodDefault<z.ZodNumber>;
health_check_verify_tls: z.ZodDefault<z.ZodCoercedBoolean<unknown>>; health_check_verify_tls: z.ZodDefault<z.ZodCoercedBoolean<unknown>>;
cert_monitoring: z.ZodDefault<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
sync_status: z.ZodDefault<z.ZodNullable<z.ZodString>>; sync_status: z.ZodDefault<z.ZodNullable<z.ZodString>>;
created_at: z.ZodString; created_at: z.ZodString;
updated_at: z.ZodString; updated_at: z.ZodString;
@@ -1498,6 +1541,7 @@ declare const serviceBindingSchema: z.ZodPipe<z.ZodObject<{
health_check_interval_sec: number; health_check_interval_sec: number;
health_check_timeout_ms: number; health_check_timeout_ms: number;
health_check_verify_tls: boolean; health_check_verify_tls: boolean;
cert_monitoring: "auto" | "required" | "skipped";
sync_status: string | null; sync_status: string | null;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
@@ -1522,6 +1566,7 @@ declare const serviceBindingSchema: z.ZodPipe<z.ZodObject<{
health_check_interval_sec: number; health_check_interval_sec: number;
health_check_timeout_ms: number; health_check_timeout_ms: number;
health_check_verify_tls: boolean; health_check_verify_tls: boolean;
cert_monitoring: "auto" | "required" | "skipped";
sync_status: string | null; sync_status: string | null;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
@@ -1549,6 +1594,8 @@ declare const certificateSchema: z.ZodObject<{
id: z.ZodNumber; id: z.ZodNumber;
domain_id: z.ZodNumber; domain_id: z.ZodNumber;
subdomain_id: z.ZodNullable<z.ZodNumber>; subdomain_id: z.ZodNullable<z.ZodNumber>;
service_id: z.ZodDefault<z.ZodOptional<z.ZodNullable<z.ZodNumber>>>;
service_name: z.ZodDefault<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
hostname: z.ZodString; hostname: z.ZodString;
expires_at: z.ZodNullable<z.ZodString>; expires_at: z.ZodNullable<z.ZodString>;
last_checked_at: z.ZodNullable<z.ZodString>; last_checked_at: z.ZodNullable<z.ZodString>;
@@ -1557,6 +1604,22 @@ declare const certificateSchema: z.ZodObject<{
created_at: z.ZodString; created_at: z.ZodString;
updated_at: z.ZodString; updated_at: z.ZodString;
}, z.core.$strip>; }, z.core.$strip>;
declare const serviceCertificateRowSchema: z.ZodObject<{
binding_id: z.ZodNumber;
domain_id: z.ZodNumber;
service_id: z.ZodNumber;
hostname: z.ZodString;
cert_monitoring: z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>;
id: z.ZodNullable<z.ZodNumber>;
status: z.ZodString;
expires_at: z.ZodNullable<z.ZodString>;
last_checked_at: z.ZodNullable<z.ZodString>;
last_error: z.ZodNullable<z.ZodString>;
}, z.core.$strip>;
type Group = z.infer<typeof groupSchema>; type Group = z.infer<typeof groupSchema>;
type GroupWithStats = z.infer<typeof groupWithStatsSchema>; type GroupWithStats = z.infer<typeof groupWithStatsSchema>;
type Service = z.infer<typeof serviceSchema>; type Service = z.infer<typeof serviceSchema>;
@@ -1569,12 +1632,13 @@ type Domain = z.infer<typeof domainSchema>;
type DomainListItem = z.infer<typeof domainListItemSchema>; type DomainListItem = z.infer<typeof domainListItemSchema>;
type DnsRecord = z.infer<typeof dnsRecordSchema>; type DnsRecord = z.infer<typeof dnsRecordSchema>;
type Certificate = z.infer<typeof certificateSchema>; type Certificate = z.infer<typeof certificateSchema>;
type ServiceCertificateRow = z.infer<typeof serviceCertificateRowSchema>;
declare const createGroupSchema: z.ZodObject<{ declare const createGroupSchema: z.ZodObject<{
name: z.ZodString; name: z.ZodString;
slug: z.ZodString; slug: z.ZodString;
}, z.core.$strip>; }, z.core.$strip>;
declare const healthCheckConfigSchema: z.ZodObject<{ declare const healthCheckConfigSchema: z.ZodObject<{
health_check_enabled: z.ZodOptional<z.ZodBoolean>; health_check_enabled: z.ZodOptional<z.ZodCoercedBoolean<unknown>>;
health_check_type: z.ZodOptional<z.ZodEnum<{ health_check_type: z.ZodOptional<z.ZodEnum<{
tcp: "tcp"; tcp: "tcp";
http: "http"; http: "http";
@@ -1586,17 +1650,17 @@ declare const healthCheckConfigSchema: z.ZodObject<{
health_check_expected_status: z.ZodOptional<z.ZodNullable<z.ZodNumber>>; health_check_expected_status: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
health_check_interval_sec: z.ZodOptional<z.ZodNumber>; health_check_interval_sec: z.ZodOptional<z.ZodNumber>;
health_check_timeout_ms: z.ZodOptional<z.ZodNumber>; health_check_timeout_ms: z.ZodOptional<z.ZodNumber>;
health_check_verify_tls: z.ZodOptional<z.ZodBoolean>; health_check_verify_tls: z.ZodOptional<z.ZodCoercedBoolean<unknown>>;
health_check_provider: z.ZodOptional<z.ZodEnum<{ health_check_provider: z.ZodOptional<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodOptional<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodOptional<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodOptional<z.ZodEnum<{ health_check_aggregate: z.ZodOptional<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
@@ -1616,7 +1680,7 @@ declare const createServiceWithConfigSchema: z.ZodObject<{
lb_weight: z.ZodOptional<z.ZodNumber>; lb_weight: z.ZodOptional<z.ZodNumber>;
lb_priority: z.ZodOptional<z.ZodNumber>; lb_priority: z.ZodOptional<z.ZodNumber>;
domains: z.ZodDefault<z.ZodArray<z.ZodObject<{ domains: z.ZodDefault<z.ZodArray<z.ZodObject<{
health_check_enabled: z.ZodOptional<z.ZodBoolean>; health_check_enabled: z.ZodOptional<z.ZodCoercedBoolean<unknown>>;
health_check_type: z.ZodOptional<z.ZodEnum<{ health_check_type: z.ZodOptional<z.ZodEnum<{
tcp: "tcp"; tcp: "tcp";
http: "http"; http: "http";
@@ -1628,17 +1692,17 @@ declare const createServiceWithConfigSchema: z.ZodObject<{
health_check_expected_status: z.ZodOptional<z.ZodNullable<z.ZodNumber>>; health_check_expected_status: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
health_check_interval_sec: z.ZodOptional<z.ZodNumber>; health_check_interval_sec: z.ZodOptional<z.ZodNumber>;
health_check_timeout_ms: z.ZodOptional<z.ZodNumber>; health_check_timeout_ms: z.ZodOptional<z.ZodNumber>;
health_check_verify_tls: z.ZodOptional<z.ZodBoolean>; health_check_verify_tls: z.ZodOptional<z.ZodCoercedBoolean<unknown>>;
health_check_provider: z.ZodOptional<z.ZodEnum<{ health_check_provider: z.ZodOptional<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodOptional<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodOptional<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodOptional<z.ZodEnum<{ health_check_aggregate: z.ZodOptional<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
@@ -1819,7 +1883,7 @@ declare const updateServiceConfigSchema: z.ZodObject<{
lb_weight: z.ZodOptional<z.ZodNumber>; lb_weight: z.ZodOptional<z.ZodNumber>;
lb_priority: z.ZodOptional<z.ZodNumber>; lb_priority: z.ZodOptional<z.ZodNumber>;
domains: z.ZodOptional<z.ZodArray<z.ZodObject<{ domains: z.ZodOptional<z.ZodArray<z.ZodObject<{
health_check_enabled: z.ZodOptional<z.ZodBoolean>; health_check_enabled: z.ZodOptional<z.ZodCoercedBoolean<unknown>>;
health_check_type: z.ZodOptional<z.ZodEnum<{ health_check_type: z.ZodOptional<z.ZodEnum<{
tcp: "tcp"; tcp: "tcp";
http: "http"; http: "http";
@@ -1831,17 +1895,17 @@ declare const updateServiceConfigSchema: z.ZodObject<{
health_check_expected_status: z.ZodOptional<z.ZodNullable<z.ZodNumber>>; health_check_expected_status: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
health_check_interval_sec: z.ZodOptional<z.ZodNumber>; health_check_interval_sec: z.ZodOptional<z.ZodNumber>;
health_check_timeout_ms: z.ZodOptional<z.ZodNumber>; health_check_timeout_ms: z.ZodOptional<z.ZodNumber>;
health_check_verify_tls: z.ZodOptional<z.ZodBoolean>; health_check_verify_tls: z.ZodOptional<z.ZodCoercedBoolean<unknown>>;
health_check_provider: z.ZodOptional<z.ZodEnum<{ health_check_provider: z.ZodOptional<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodOptional<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodOptional<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodOptional<z.ZodEnum<{ health_check_aggregate: z.ZodOptional<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
@@ -1861,7 +1925,7 @@ declare const updateServiceConfigSchema: z.ZodObject<{
}, z.core.$strip>; }, z.core.$strip>;
type UpdateServiceConfigInput = z.infer<typeof updateServiceConfigSchema>; type UpdateServiceConfigInput = z.infer<typeof updateServiceConfigSchema>;
declare const createServiceGroupSchema: z.ZodObject<{ declare const createServiceGroupSchema: z.ZodObject<{
health_check_enabled: z.ZodOptional<z.ZodBoolean>; health_check_enabled: z.ZodOptional<z.ZodCoercedBoolean<unknown>>;
health_check_type: z.ZodOptional<z.ZodEnum<{ health_check_type: z.ZodOptional<z.ZodEnum<{
tcp: "tcp"; tcp: "tcp";
http: "http"; http: "http";
@@ -1873,17 +1937,17 @@ declare const createServiceGroupSchema: z.ZodObject<{
health_check_expected_status: z.ZodOptional<z.ZodNullable<z.ZodNumber>>; health_check_expected_status: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
health_check_interval_sec: z.ZodOptional<z.ZodNumber>; health_check_interval_sec: z.ZodOptional<z.ZodNumber>;
health_check_timeout_ms: z.ZodOptional<z.ZodNumber>; health_check_timeout_ms: z.ZodOptional<z.ZodNumber>;
health_check_verify_tls: z.ZodOptional<z.ZodBoolean>; health_check_verify_tls: z.ZodOptional<z.ZodCoercedBoolean<unknown>>;
health_check_provider: z.ZodOptional<z.ZodEnum<{ health_check_provider: z.ZodOptional<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodOptional<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodOptional<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodOptional<z.ZodEnum<{ health_check_aggregate: z.ZodOptional<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
@@ -1906,7 +1970,7 @@ declare const createServiceGroupSchema: z.ZodObject<{
}>>; }>>;
}, z.core.$strip>; }, z.core.$strip>;
declare const updateServiceGroupSchema: z.ZodObject<{ declare const updateServiceGroupSchema: z.ZodObject<{
health_check_enabled: z.ZodOptional<z.ZodBoolean>; health_check_enabled: z.ZodOptional<z.ZodCoercedBoolean<unknown>>;
health_check_type: z.ZodOptional<z.ZodEnum<{ health_check_type: z.ZodOptional<z.ZodEnum<{
tcp: "tcp"; tcp: "tcp";
http: "http"; http: "http";
@@ -1918,17 +1982,17 @@ declare const updateServiceGroupSchema: z.ZodObject<{
health_check_expected_status: z.ZodOptional<z.ZodNullable<z.ZodNumber>>; health_check_expected_status: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
health_check_interval_sec: z.ZodOptional<z.ZodNumber>; health_check_interval_sec: z.ZodOptional<z.ZodNumber>;
health_check_timeout_ms: z.ZodOptional<z.ZodNumber>; health_check_timeout_ms: z.ZodOptional<z.ZodNumber>;
health_check_verify_tls: z.ZodOptional<z.ZodBoolean>; health_check_verify_tls: z.ZodOptional<z.ZodCoercedBoolean<unknown>>;
health_check_provider: z.ZodOptional<z.ZodEnum<{ health_check_provider: z.ZodOptional<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>; }>>;
health_check_providers: z.ZodOptional<z.ZodPipe<z.ZodArray<z.ZodEnum<{ health_check_providers: z.ZodOptional<z.ZodPreprocess<z.ZodPipe<z.ZodArray<z.ZodEnum<{
local: "local"; local: "local";
cloudflare: "cloudflare"; cloudflare: "cloudflare";
globalping: "globalping"; globalping: "globalping";
}>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>; }>>, z.ZodTransform<("local" | "cloudflare" | "globalping")[] | readonly ["local"], ("local" | "cloudflare" | "globalping")[]>>>>;
health_check_aggregate: z.ZodOptional<z.ZodEnum<{ health_check_aggregate: z.ZodOptional<z.ZodEnum<{
any: "any"; any: "any";
all: "all"; all: "all";
@@ -2348,4 +2412,4 @@ declare const ingestAuditEventSchema: z.ZodObject<{
}, z.core.$strip>; }, z.core.$strip>;
type IngestAuditEvent = z.infer<typeof ingestAuditEventSchema>; 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, HEALTH_CHECK_AGGREGATES, HEALTH_CHECK_PROVIDERS, HEALTH_KV_CURSOR_KEY, HEALTH_KV_RESULTS_KEY, HEALTH_KV_TARGETS_KEY, HEALTH_PROBE_BATCH, HEALTH_PROBE_CONCURRENCY, HEALTH_PROBE_KV_TITLE, HEALTH_PROBE_SCRIPT_NAME, HEALTH_STATUS_PROVIDERS, type HealthCheckAggregate, type HealthCheckConfig, type HealthCheckProvider, type HealthCheckScope, type HealthCheckTarget, type HealthCheckType, type HealthProbeLog, type HealthProbeResultItem, type HealthProbeResultsDoc, type HealthProbeTargetItem, type HealthProbeTargetsDoc, type HealthStatusProvider, type HealthStatusQuery, type HealthWorkerStatus, 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 ToggleServiceIpInput, type UpdateDomainInput, type UpdateServiceConfigInput, type UpdateServiceGroupInput, type UpdateServiceNodeInput, type UpdateSubdomainInput, ValidationError, type VpsTrackerEvent, aggregateHealthOk, appSettingsPatchSchema, appSwitcherConfigSchema, appSwitcherEntrySchema, appSwitcherIconSchema, auditListQuerySchema, auditLogEntrySchema, auditSeveritySchema, auditSourceAppSchema, auditTargetTypeSchema, bindingToFqdn, bulkUpdateDomainsSchema, certMonitoringSchema, certStatusFromExpiry, certificateSchema, cfdmBindingSyncItemSchema, cfdmSyncBindingsBodySchema, changeDomainSchema, changeIpSchema, clampGlobalpingLimit, createDnsRecordSchema, createDomainMonitorSchema, createDomainSchema, createGroupSchema, createOriginHealthCheckSchema, createServiceBindingSchema, createServiceGroupSchema, createServiceNodeSchema, createServiceSchema, createServiceWithConfigSchema, createSubdomainSchema, derivePrimaryProvider, dnsNameToSubdomainLabel, dnsRecordNamesMatch, dnsRecordSchema, domainEnvironmentSchema, domainListItemSchema, domainMonitorResultSchema, domainMonitorSchema, domainMonitorTypeSchema, domainSchema, fqdnToDisplay, groupSchema, groupWithStatsSchema, healthCheckAggregateSchema, healthCheckConfigSchema, healthCheckProviderSchema, healthCheckProvidersSchema, healthCheckScopeSchema, healthCheckTypeSchema, healthProbeLogSchema, healthStatusProviderSchema, healthStatusQuerySchema, ingestAuditEventSchema, ipHealthStateSchema, ipHealthStatusSchema, isIpLiteral, isValidIpv4, lbModeSchema, loginSchema, nodeHealthStateSchema, normalizeDnsRecordName, normalizeProbeProvider, normalizeStatusProvider, notificationLogSchema, originHealthCheckSchema, parseFqdn, parseGlobalpingLocations, parseHealthAggregate, parseHealthProviders, reorderServicesSchema, serializeHealthProviders, serviceBindingSchema, serviceDomainBindingSchema, serviceGroupSchema, serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, serviceIpHealthSchema, serviceNodeSchema, serviceSchema, serviceViewSchema, shouldMonitorService, subdomainLabelToFqdn, subdomainSchema, targetHasProvider, targetProviders, toggleEnabledSchema, toggleServiceIpSchema, uniqueHealthProviders, 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, HEALTH_CHECK_AGGREGATES, HEALTH_CHECK_PROVIDERS, HEALTH_KV_CURSOR_KEY, HEALTH_KV_RESULTS_KEY, HEALTH_KV_TARGETS_KEY, HEALTH_PROBE_BATCH, HEALTH_PROBE_CONCURRENCY, HEALTH_PROBE_KV_TITLE, HEALTH_PROBE_SCRIPT_NAME, HEALTH_STATUS_PROVIDERS, type HealthCheckAggregate, type HealthCheckConfig, type HealthCheckProvider, type HealthCheckScope, type HealthCheckTarget, type HealthCheckType, type HealthProbeLog, type HealthProbeResultItem, type HealthProbeResultsDoc, type HealthProbeTargetItem, type HealthProbeTargetsDoc, type HealthStatusProvider, type HealthStatusQuery, type HealthWorkerStatus, 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 ServiceCertificateRow, 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 ToggleServiceIpInput, type UpdateDomainInput, type UpdateServiceConfigInput, type UpdateServiceGroupInput, type UpdateServiceNodeInput, type UpdateSubdomainInput, ValidationError, type VpsTrackerEvent, aggregateHealthOk, appSettingsPatchSchema, appSwitcherConfigSchema, appSwitcherEntrySchema, appSwitcherIconSchema, auditListQuerySchema, auditLogEntrySchema, auditSeveritySchema, auditSourceAppSchema, auditTargetTypeSchema, bindingToFqdn, bulkUpdateDomainsSchema, certMonitoringSchema, certStatusFromExpiry, certificateSchema, cfdmBindingSyncItemSchema, cfdmSyncBindingsBodySchema, changeDomainSchema, changeIpSchema, clampGlobalpingLimit, createDnsRecordSchema, createDomainMonitorSchema, createDomainSchema, createGroupSchema, createOriginHealthCheckSchema, createServiceBindingSchema, createServiceGroupSchema, createServiceNodeSchema, createServiceSchema, createServiceWithConfigSchema, createSubdomainSchema, derivePrimaryProvider, dnsNameToSubdomainLabel, dnsRecordNamesMatch, dnsRecordSchema, domainEnvironmentSchema, domainListItemSchema, domainMonitorResultSchema, domainMonitorSchema, domainMonitorTypeSchema, domainSchema, fqdnToDisplay, groupSchema, groupWithStatsSchema, healthCheckAggregateSchema, healthCheckConfigSchema, healthCheckProviderSchema, healthCheckProvidersSchema, healthCheckScopeSchema, healthCheckTypeSchema, healthProbeLogSchema, healthStatusProviderSchema, healthStatusQuerySchema, ingestAuditEventSchema, ipHealthStateSchema, ipHealthStatusSchema, isIpLiteral, isValidIpv4, lbModeSchema, loginSchema, nodeHealthStateSchema, normalizeDnsRecordName, normalizeProbeProvider, normalizeStatusProvider, notificationLogSchema, originHealthCheckSchema, parseFqdn, parseGlobalpingLocations, parseHealthAggregate, parseHealthProviders, reorderServicesSchema, serializeHealthProviders, serviceBindingSchema, serviceCertificateRowSchema, serviceDomainBindingSchema, serviceGroupSchema, serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, serviceIpHealthSchema, serviceNodeSchema, serviceSchema, serviceViewSchema, shouldMonitorService, subdomainLabelToFqdn, subdomainSchema, targetHasProvider, targetProviders, toggleEnabledSchema, toggleServiceIpSchema, uniqueHealthProviders, updateDomainGroupSchema, updateDomainSchema, updateServiceConfigSchema, updateServiceGroupSchema, updateServiceNodeSchema, updateSubdomainSchema, validateDnsRecord, vpsTrackerEventSchema };
+29 -6
View File
@@ -276,10 +276,16 @@ var nodeHealthStateSchema = z.enum([
var healthCheckProviderSchema = z.enum(HEALTH_CHECK_PROVIDERS); var healthCheckProviderSchema = z.enum(HEALTH_CHECK_PROVIDERS);
var healthStatusProviderSchema = z.enum(HEALTH_STATUS_PROVIDERS); var healthStatusProviderSchema = z.enum(HEALTH_STATUS_PROVIDERS);
var healthCheckAggregateSchema = z.enum(HEALTH_CHECK_AGGREGATES); var healthCheckAggregateSchema = z.enum(HEALTH_CHECK_AGGREGATES);
var healthCheckProvidersSchema = z.array(healthCheckProviderSchema).min(1).transform((arr) => { var healthCheckProvidersSchema = z.preprocess(
const unique = uniqueHealthProviders(arr); (value) => {
return unique.length > 0 ? unique : ["local"]; if (value === void 0) return void 0;
}); return Array.isArray(value) ? value : parseHealthProviders(value);
},
z.array(healthCheckProviderSchema).min(1).transform((arr) => {
const unique = uniqueHealthProviders(arr);
return unique.length > 0 ? unique : ["local"];
})
);
var healthCheckScopeSchema = z.enum(["binding", "group"]); var healthCheckScopeSchema = z.enum(["binding", "group"]);
var ipHealthStatusSchema = z.object({ var ipHealthStatusSchema = z.object({
scope: healthCheckScopeSchema, scope: healthCheckScopeSchema,
@@ -392,6 +398,7 @@ var serviceDomainBindingSchema = z.object({
health_check_provider: healthCheckProviderSchema.catch("local"), health_check_provider: healthCheckProviderSchema.catch("local"),
health_check_providers: healthCheckProvidersSchema.catch(["local"]), health_check_providers: healthCheckProvidersSchema.catch(["local"]),
health_check_aggregate: healthCheckAggregateSchema.catch("majority"), health_check_aggregate: healthCheckAggregateSchema.catch("majority"),
cert_monitoring: certMonitoringSchema.default("auto"),
sync_status: z.string().nullable().default(null) sync_status: z.string().nullable().default(null)
}).transform((binding) => ({ }).transform((binding) => ({
...binding, ...binding,
@@ -465,6 +472,7 @@ var serviceBindingSchema = z.object({
health_check_interval_sec: z.number().default(30), health_check_interval_sec: z.number().default(30),
health_check_timeout_ms: z.number().default(3e3), health_check_timeout_ms: z.number().default(3e3),
health_check_verify_tls: z.coerce.boolean().default(false), health_check_verify_tls: z.coerce.boolean().default(false),
cert_monitoring: certMonitoringSchema.default("auto"),
sync_status: z.string().nullable().default(null), sync_status: z.string().nullable().default(null),
created_at: z.string(), created_at: z.string(),
updated_at: z.string() updated_at: z.string()
@@ -494,6 +502,8 @@ var certificateSchema = z.object({
id: z.number(), id: z.number(),
domain_id: z.number(), domain_id: z.number(),
subdomain_id: z.number().nullable(), subdomain_id: z.number().nullable(),
service_id: z.number().nullable().optional().default(null),
service_name: z.string().nullable().optional().default(null),
hostname: z.string(), hostname: z.string(),
expires_at: z.string().nullable(), expires_at: z.string().nullable(),
last_checked_at: z.string().nullable(), last_checked_at: z.string().nullable(),
@@ -502,6 +512,18 @@ var certificateSchema = z.object({
created_at: z.string(), created_at: z.string(),
updated_at: z.string() updated_at: z.string()
}); });
var serviceCertificateRowSchema = z.object({
binding_id: z.number(),
domain_id: z.number(),
service_id: z.number(),
hostname: z.string(),
cert_monitoring: certMonitoringSchema,
id: z.number().nullable(),
status: z.string(),
expires_at: z.string().nullable(),
last_checked_at: z.string().nullable(),
last_error: z.string().nullable()
});
var createGroupSchema = z.object({ var createGroupSchema = z.object({
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435"), name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435"),
slug: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 slug") slug: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 slug")
@@ -512,14 +534,14 @@ var ipv4Schema = z.string().regex(
); );
var nodeAddressSchema = z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 IP \u0438\u043B\u0438 hostname").max(255); var nodeAddressSchema = z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 IP \u0438\u043B\u0438 hostname").max(255);
var healthCheckConfigFields = { var healthCheckConfigFields = {
health_check_enabled: z.boolean().optional(), health_check_enabled: z.coerce.boolean().optional(),
health_check_type: healthCheckTypeSchema.optional(), health_check_type: healthCheckTypeSchema.optional(),
health_check_port: z.number().int().min(1).max(65535).nullable().optional(), health_check_port: z.number().int().min(1).max(65535).nullable().optional(),
health_check_path: z.string().nullable().optional(), health_check_path: z.string().nullable().optional(),
health_check_expected_status: z.number().int().min(100).max(599).nullable().optional(), health_check_expected_status: z.number().int().min(100).max(599).nullable().optional(),
health_check_interval_sec: z.number().int().min(5).max(3600).optional(), health_check_interval_sec: z.number().int().min(5).max(3600).optional(),
health_check_timeout_ms: z.number().int().min(100).max(3e4).optional(), health_check_timeout_ms: z.number().int().min(100).max(3e4).optional(),
health_check_verify_tls: z.boolean().optional(), health_check_verify_tls: z.coerce.boolean().optional(),
health_check_provider: healthCheckProviderSchema.optional(), health_check_provider: healthCheckProviderSchema.optional(),
health_check_providers: healthCheckProvidersSchema.optional(), health_check_providers: healthCheckProvidersSchema.optional(),
health_check_aggregate: healthCheckAggregateSchema.optional() health_check_aggregate: healthCheckAggregateSchema.optional()
@@ -1029,6 +1051,7 @@ export {
reorderServicesSchema, reorderServicesSchema,
serializeHealthProviders, serializeHealthProviders,
serviceBindingSchema, serviceBindingSchema,
serviceCertificateRowSchema,
serviceDomainBindingSchema, serviceDomainBindingSchema,
serviceGroupSchema, serviceGroupSchema,
serviceGroupTypeSchema, serviceGroupTypeSchema,
+11 -7
View File
@@ -3,6 +3,7 @@ import {
HEALTH_CHECK_AGGREGATES, HEALTH_CHECK_AGGREGATES,
HEALTH_CHECK_PROVIDERS, HEALTH_CHECK_PROVIDERS,
HEALTH_STATUS_PROVIDERS, HEALTH_STATUS_PROVIDERS,
parseHealthProviders,
uniqueHealthProviders, uniqueHealthProviders,
} from './health-providers.js' } from './health-providers.js'
@@ -41,13 +42,16 @@ export const healthStatusProviderSchema = z.enum(HEALTH_STATUS_PROVIDERS)
export const healthCheckAggregateSchema = z.enum(HEALTH_CHECK_AGGREGATES) export const healthCheckAggregateSchema = z.enum(HEALTH_CHECK_AGGREGATES)
export const healthCheckProvidersSchema = z export const healthCheckProvidersSchema = z.preprocess(
.array(healthCheckProviderSchema) (value) => {
.min(1) if (value === undefined) return undefined
.transform((arr) => { return Array.isArray(value) ? value : parseHealthProviders(value)
},
z.array(healthCheckProviderSchema).min(1).transform((arr) => {
const unique = uniqueHealthProviders(arr) const unique = uniqueHealthProviders(arr)
return unique.length > 0 ? unique : (['local'] as const) return unique.length > 0 ? unique : (['local'] as const)
}) }),
)
export const healthCheckScopeSchema = z.enum(['binding', 'group']) export const healthCheckScopeSchema = z.enum(['binding', 'group'])
export type HealthCheckScope = z.infer<typeof healthCheckScopeSchema> export type HealthCheckScope = z.infer<typeof healthCheckScopeSchema>
@@ -361,14 +365,14 @@ const nodeAddressSchema = z
.max(255) .max(255)
const healthCheckConfigFields = { const healthCheckConfigFields = {
health_check_enabled: z.boolean().optional(), health_check_enabled: z.coerce.boolean().optional(),
health_check_type: healthCheckTypeSchema.optional(), health_check_type: healthCheckTypeSchema.optional(),
health_check_port: z.number().int().min(1).max(65535).nullable().optional(), health_check_port: z.number().int().min(1).max(65535).nullable().optional(),
health_check_path: z.string().nullable().optional(), health_check_path: z.string().nullable().optional(),
health_check_expected_status: z.number().int().min(100).max(599).nullable().optional(), health_check_expected_status: z.number().int().min(100).max(599).nullable().optional(),
health_check_interval_sec: z.number().int().min(5).max(3600).optional(), health_check_interval_sec: z.number().int().min(5).max(3600).optional(),
health_check_timeout_ms: z.number().int().min(100).max(30000).optional(), health_check_timeout_ms: z.number().int().min(100).max(30000).optional(),
health_check_verify_tls: z.boolean().optional(), health_check_verify_tls: z.coerce.boolean().optional(),
health_check_provider: healthCheckProviderSchema.optional(), health_check_provider: healthCheckProviderSchema.optional(),
health_check_providers: healthCheckProvidersSchema.optional(), health_check_providers: healthCheckProvidersSchema.optional(),
health_check_aggregate: healthCheckAggregateSchema.optional(), health_check_aggregate: healthCheckAggregateSchema.optional(),