Refactor MePoolPanel component to integrate DC and writer metrics, enhancing data presentation with new RTT and coverage indicators. Update TypeScript interfaces for improved data handling and parsing of DC and writer statuses. Adjust runtime page to fetch and display new metrics, ensuring comprehensive monitoring of ME pool performance.
This commit is contained in:
@@ -3,67 +3,124 @@
|
|||||||
import { Badge } from '@/components/reui/badge'
|
import { Badge } from '@/components/reui/badge'
|
||||||
import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle } from '@/components/reui/frame'
|
import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle } from '@/components/reui/frame'
|
||||||
import { MetricListFrame, MetricRow, StatusBadge } from '@/components/metric-list'
|
import { MetricListFrame, MetricRow, StatusBadge } from '@/components/metric-list'
|
||||||
|
import { cn } from '@telemt/ui/lib/utils'
|
||||||
|
import { Progress } from '@telemt/ui/components/progress'
|
||||||
import {
|
import {
|
||||||
formatEpoch,
|
formatEpoch,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
|
formatRttMs,
|
||||||
|
type DcStatusData,
|
||||||
|
type MeWritersData,
|
||||||
type RuntimeMePoolStateData,
|
type RuntimeMePoolStateData,
|
||||||
type RuntimeMeQualityData,
|
type RuntimeMeQualityData,
|
||||||
} from '@/lib/telemt'
|
} from '@/lib/telemt'
|
||||||
|
|
||||||
|
function rttTone(ms: number | null | undefined): 'success-light' | 'warning-light' | 'destructive-light' | 'secondary' {
|
||||||
|
if (ms == null || !Number.isFinite(ms)) return 'secondary'
|
||||||
|
if (ms < 80) return 'success-light'
|
||||||
|
if (ms < 180) return 'warning-light'
|
||||||
|
return 'destructive-light'
|
||||||
|
}
|
||||||
|
|
||||||
|
function coverageTone(pct: number | null | undefined): string {
|
||||||
|
const n = Number(pct)
|
||||||
|
if (!Number.isFinite(n)) return '**:data-[slot=progress-indicator]:bg-muted-foreground'
|
||||||
|
if (n >= 90) return '**:data-[slot=progress-indicator]:bg-emerald-500'
|
||||||
|
if (n >= 60) return '**:data-[slot=progress-indicator]:bg-amber-500'
|
||||||
|
return '**:data-[slot=progress-indicator]:bg-red-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
function WriterStateBadge({
|
||||||
|
state,
|
||||||
|
degraded,
|
||||||
|
draining,
|
||||||
|
}: {
|
||||||
|
state?: string
|
||||||
|
degraded?: boolean
|
||||||
|
draining?: boolean
|
||||||
|
}) {
|
||||||
|
if (draining) return <Badge variant="warning-light" size="sm">draining</Badge>
|
||||||
|
if (degraded) return <Badge variant="destructive-light" size="sm">degraded</Badge>
|
||||||
|
if (state === 'active') return <Badge variant="success-light" size="sm">active</Badge>
|
||||||
|
if (state === 'warm') return <Badge variant="outline" size="sm">warm</Badge>
|
||||||
|
return <Badge variant="secondary" size="sm">{state ?? '—'}</Badge>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ME Telemt status — stats-7 writers strip + Frame detail panels.
|
* ME Telemt status — stats-7 + DC/endpoint RTT tables (telemt_panel SoT).
|
||||||
* Preview: https://reui.io/preview/base/stats-7
|
* Preview: https://reui.io/preview/base/stats-7 · https://reui.io/preview/base/list-11
|
||||||
* Docs: https://reui.io/blocks · https://reui.io/docs/components/base/frame
|
* Docs: https://reui.io/blocks · https://reui.io/docs/components/base/frame
|
||||||
*/
|
*/
|
||||||
export function MePoolPanel({
|
export function MePoolPanel({
|
||||||
pool,
|
pool,
|
||||||
quality,
|
quality,
|
||||||
|
dcs,
|
||||||
|
writers,
|
||||||
isLoading,
|
isLoading,
|
||||||
}: {
|
}: {
|
||||||
pool: RuntimeMePoolStateData
|
pool: RuntimeMePoolStateData
|
||||||
quality: RuntimeMeQualityData
|
quality: RuntimeMeQualityData
|
||||||
|
dcs: DcStatusData
|
||||||
|
writers: MeWritersData
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
}) {
|
}) {
|
||||||
const enabled = pool.enabled !== false
|
const enabled = pool.enabled !== false
|
||||||
const writers = pool.data?.writers
|
const poolWriters = pool.data?.writers
|
||||||
const generations = pool.data?.generations
|
const generations = pool.data?.generations
|
||||||
const hardswap = pool.data?.hardswap
|
const hardswap = pool.data?.hardswap
|
||||||
const refill = pool.data?.refill
|
const refill = pool.data?.refill
|
||||||
const q = quality.data
|
const q = quality.data
|
||||||
|
|
||||||
|
const dcRows =
|
||||||
|
(dcs.dcs?.length ? dcs.dcs : null) ??
|
||||||
|
(q?.dc_rtt ?? []).map((row) => ({
|
||||||
|
dc: row.dc,
|
||||||
|
rtt_ms: row.rtt_ema_ms,
|
||||||
|
alive_writers: row.alive_writers,
|
||||||
|
required_writers: row.required_writers,
|
||||||
|
coverage_pct: row.coverage_pct,
|
||||||
|
endpoints: [] as string[],
|
||||||
|
endpoint_writers: [] as Array<{ endpoint?: string; active_writers?: number }>,
|
||||||
|
available_endpoints: undefined as number | undefined,
|
||||||
|
available_pct: undefined as number | undefined,
|
||||||
|
load: undefined as number | undefined,
|
||||||
|
fresh_alive_writers: undefined as number | undefined,
|
||||||
|
fresh_coverage_pct: undefined as number | undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const writerRows = [...(writers.writers ?? [])].sort((a, b) => {
|
||||||
|
const ra = a.rtt_ema_ms ?? Number.POSITIVE_INFINITY
|
||||||
|
const rb = b.rtt_ema_ms ?? Number.POSITIVE_INFINITY
|
||||||
|
return ra - rb
|
||||||
|
})
|
||||||
|
|
||||||
const writerCards = [
|
const writerCards = [
|
||||||
{
|
{
|
||||||
title: 'Живые writers',
|
title: 'Живые writers',
|
||||||
subtitle: 'alive_non_draining',
|
subtitle: 'alive_non_draining',
|
||||||
value: formatNumber(writers?.alive_non_draining),
|
value: formatNumber(poolWriters?.alive_non_draining ?? writers.summary?.alive_writers),
|
||||||
badge: {
|
badge: { text: 'healthy', variant: 'success-light' as const },
|
||||||
text: 'healthy',
|
subtext: `всего ${formatNumber(poolWriters?.total ?? writers.summary?.configured_endpoints)} · coverage ${formatNumber(writers.summary?.coverage_pct)}%`,
|
||||||
variant: 'success-light' as const,
|
|
||||||
},
|
|
||||||
subtext: `всего ${formatNumber(writers?.total)} · contour active ${formatNumber(writers?.contour?.active)}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Draining',
|
title: 'Draining',
|
||||||
subtitle: 'draining',
|
subtitle: 'draining',
|
||||||
value: formatNumber(writers?.draining),
|
value: formatNumber(poolWriters?.draining),
|
||||||
badge: {
|
badge: { text: 'drain', variant: 'warning-light' as const },
|
||||||
text: 'drain',
|
subtext: `contour draining ${formatNumber(poolWriters?.contour?.draining)}`,
|
||||||
variant: 'warning-light' as const,
|
|
||||||
},
|
|
||||||
subtext: `contour draining ${formatNumber(writers?.contour?.draining)}`,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Degraded',
|
title: 'Degraded',
|
||||||
subtitle: 'degraded',
|
subtitle: 'degraded',
|
||||||
value: formatNumber(writers?.degraded),
|
value: formatNumber(poolWriters?.degraded),
|
||||||
badge: {
|
badge: {
|
||||||
text: writers && (writers.degraded ?? 0) > 0 ? 'attention' : 'ok',
|
text: poolWriters && (poolWriters.degraded ?? 0) > 0 ? 'attention' : 'ok',
|
||||||
variant:
|
variant:
|
||||||
writers && (writers.degraded ?? 0) > 0
|
poolWriters && (poolWriters.degraded ?? 0) > 0
|
||||||
? ('destructive-light' as const)
|
? ('destructive-light' as const)
|
||||||
: ('secondary' as const),
|
: ('secondary' as const),
|
||||||
},
|
},
|
||||||
subtext: `health healthy ${formatNumber(writers?.health?.healthy)}`,
|
subtext: `endpoints available ${formatNumber(writers.summary?.available_endpoints)} / ${formatNumber(writers.summary?.configured_endpoints)}`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -74,7 +131,7 @@ export function MePoolPanel({
|
|||||||
<div className="flex min-w-0 flex-col gap-1">
|
<div className="flex min-w-0 flex-col gap-1">
|
||||||
<FrameTitle>ME Pool</FrameTitle>
|
<FrameTitle>ME Pool</FrameTitle>
|
||||||
<FrameDescription>
|
<FrameDescription>
|
||||||
Middle-End writers, generations и refill ·{' '}
|
Middle-End writers, DC latency и endpoints ·{' '}
|
||||||
{pool.generated_at_epoch_secs
|
{pool.generated_at_epoch_secs
|
||||||
? formatEpoch(pool.generated_at_epoch_secs)
|
? formatEpoch(pool.generated_at_epoch_secs)
|
||||||
: isLoading
|
: isLoading
|
||||||
@@ -97,9 +154,9 @@ export function MePoolPanel({
|
|||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
Writers health: healthy {formatNumber(writers?.health?.healthy)} ·
|
Writers health: healthy {formatNumber(poolWriters?.health?.healthy)} ·
|
||||||
degraded {formatNumber(writers?.health?.degraded)} · draining{' '}
|
degraded {formatNumber(poolWriters?.health?.degraded)} · draining{' '}
|
||||||
{formatNumber(writers?.health?.draining)}
|
{formatNumber(poolWriters?.health?.draining)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</FramePanel>
|
</FramePanel>
|
||||||
@@ -133,6 +190,220 @@ export function MePoolPanel({
|
|||||||
</FramePanel>
|
</FramePanel>
|
||||||
</Frame>
|
</Frame>
|
||||||
|
|
||||||
|
{/* Per-DC servers + RTT — list-11 DNA. Preview: https://reui.io/preview/base/list-11 */}
|
||||||
|
<MetricListFrame
|
||||||
|
title="DC в пуле"
|
||||||
|
description="Серверы (endpoints) Telegram DC, RTT и покрытие writers"
|
||||||
|
className="w-full"
|
||||||
|
trailing={
|
||||||
|
<StatusBadge
|
||||||
|
ok={dcs.middle_proxy_enabled !== false || dcRows.length > 0}
|
||||||
|
okLabel={`${dcRows.length} DC`}
|
||||||
|
failLabel={dcs.reason ? String(dcs.reason) : 'Нет DC'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{dcRows.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
{dcs.reason ??
|
||||||
|
'Нет данных /v1/stats/dcs (нужен minimal_runtime_enabled) и нет dc_rtt из me_quality.'}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b text-left">
|
||||||
|
<th className="py-2 pr-3 font-medium">DC</th>
|
||||||
|
<th className="py-2 pr-3 font-medium">RTT</th>
|
||||||
|
<th className="py-2 pr-3 font-medium">Writers</th>
|
||||||
|
<th className="py-2 pr-3 font-medium">Coverage</th>
|
||||||
|
<th className="py-2 pr-3 font-medium">Load</th>
|
||||||
|
<th className="py-2 font-medium">Endpoints</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{dcRows.map((row) => {
|
||||||
|
const coverage = Number(row.coverage_pct ?? 0)
|
||||||
|
const endpoints =
|
||||||
|
row.endpoints?.length
|
||||||
|
? row.endpoints
|
||||||
|
: (row.endpoint_writers ?? []).map((e) => e.endpoint).filter(Boolean)
|
||||||
|
return (
|
||||||
|
<tr key={`dc-${row.dc}`} className="border-b border-border/50 align-top">
|
||||||
|
<td className="py-2.5 pr-3 font-medium tabular-nums">
|
||||||
|
DC {formatNumber(row.dc)}
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 pr-3">
|
||||||
|
<Badge variant={rttTone(row.rtt_ms ?? null)} size="sm">
|
||||||
|
{formatRttMs(row.rtt_ms)}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 pr-3 tabular-nums">
|
||||||
|
{formatNumber(row.alive_writers)} / {formatNumber(row.required_writers)}
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 pr-3 min-w-28">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<Progress
|
||||||
|
value={Math.min(100, Math.max(0, coverage))}
|
||||||
|
className={cn('h-1.5', coverageTone(coverage))}
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-[10px] tabular-nums">
|
||||||
|
{Number.isFinite(coverage) ? `${Math.round(coverage)}%` : '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5 pr-3 tabular-nums">
|
||||||
|
{row.load != null ? formatNumber(row.load) : '—'}
|
||||||
|
</td>
|
||||||
|
<td className="py-2.5">
|
||||||
|
{endpoints.length === 0 ? (
|
||||||
|
<span className="text-muted-foreground">—</span>
|
||||||
|
) : (
|
||||||
|
<ul className="flex flex-col gap-1">
|
||||||
|
{endpoints.slice(0, 4).map((ep) => {
|
||||||
|
const writersOnEp = (row.endpoint_writers ?? []).find(
|
||||||
|
(w) => w.endpoint === ep,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={String(ep)}
|
||||||
|
className="flex flex-wrap items-center gap-2 font-mono text-xs"
|
||||||
|
>
|
||||||
|
<span>{ep}</span>
|
||||||
|
{writersOnEp?.active_writers != null ? (
|
||||||
|
<Badge variant="outline" size="sm">
|
||||||
|
{formatNumber(writersOnEp.active_writers)} w
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{endpoints.length > 4 ? (
|
||||||
|
<li className="text-muted-foreground text-xs">
|
||||||
|
+{endpoints.length - 4} ещё
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</MetricListFrame>
|
||||||
|
|
||||||
|
{/* Per-writer / endpoint latency */}
|
||||||
|
<MetricListFrame
|
||||||
|
title="Writers / endpoints"
|
||||||
|
description="Каждый endpoint в пуле ME и его задержка (RTT EMA)"
|
||||||
|
className="w-full"
|
||||||
|
trailing={
|
||||||
|
<StatusBadge
|
||||||
|
ok={writers.middle_proxy_enabled !== false || writerRows.length > 0}
|
||||||
|
okLabel={`${writerRows.length} writers`}
|
||||||
|
failLabel={writers.reason ? String(writers.reason) : 'Нет writers'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{writerRows.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
{writers.reason ??
|
||||||
|
'Нет данных /v1/stats/me-writers. Включите minimal_runtime_enabled в Telemt API.'}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b text-left">
|
||||||
|
<th className="py-2 pr-3 font-medium">Endpoint</th>
|
||||||
|
<th className="py-2 pr-3 font-medium">DC</th>
|
||||||
|
<th className="py-2 pr-3 font-medium">Состояние</th>
|
||||||
|
<th className="py-2 pr-3 font-medium">RTT</th>
|
||||||
|
<th className="py-2 pr-3 font-medium">Клиенты</th>
|
||||||
|
<th className="py-2 font-medium">Gen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{writerRows.slice(0, 40).map((w) => (
|
||||||
|
<tr
|
||||||
|
key={`${w.writer_id}-${w.endpoint}`}
|
||||||
|
className="border-b border-border/50"
|
||||||
|
>
|
||||||
|
<td className="py-2 pr-3 font-mono text-xs">{w.endpoint ?? '—'}</td>
|
||||||
|
<td className="py-2 pr-3 tabular-nums">
|
||||||
|
{w.dc != null ? formatNumber(w.dc) : '—'}
|
||||||
|
</td>
|
||||||
|
<td className="py-2 pr-3">
|
||||||
|
<WriterStateBadge
|
||||||
|
state={w.state}
|
||||||
|
degraded={w.degraded}
|
||||||
|
draining={w.draining}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="py-2 pr-3">
|
||||||
|
<Badge variant={rttTone(w.rtt_ema_ms ?? null)} size="sm">
|
||||||
|
{formatRttMs(w.rtt_ema_ms)}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="py-2 pr-3 tabular-nums">
|
||||||
|
{formatNumber(w.bound_clients)}
|
||||||
|
</td>
|
||||||
|
<td className="py-2 tabular-nums text-muted-foreground">
|
||||||
|
{formatNumber(w.generation)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{writerRows.length > 40 ? (
|
||||||
|
<p className="text-muted-foreground pt-2 text-xs">
|
||||||
|
Показаны 40 из {writerRows.length} writers (сортировка по RTT).
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</MetricListFrame>
|
||||||
|
|
||||||
|
{(q?.family_states ?? []).length > 0 ? (
|
||||||
|
<MetricListFrame
|
||||||
|
title="Family states"
|
||||||
|
description="Состояние маршрутов IPv4 / IPv6"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b text-left">
|
||||||
|
<th className="py-2 pr-3 font-medium">Family</th>
|
||||||
|
<th className="py-2 pr-3 font-medium">State</th>
|
||||||
|
<th className="py-2 pr-3 font-medium">Fail streak</th>
|
||||||
|
<th className="py-2 font-medium">Recover streak</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{(q?.family_states ?? []).map((f) => (
|
||||||
|
<tr key={f.family} className="border-b border-border/50">
|
||||||
|
<td className="py-2 pr-3 font-medium">{f.family ?? '—'}</td>
|
||||||
|
<td className="py-2 pr-3">
|
||||||
|
<Badge variant="outline" size="sm">
|
||||||
|
{f.state ?? '—'}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="py-2 pr-3 tabular-nums">{formatNumber(f.fail_streak)}</td>
|
||||||
|
<td className="py-2 tabular-nums">
|
||||||
|
{formatNumber(f.recover_success_streak)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</MetricListFrame>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 md:gap-6">
|
<div className="grid gap-4 md:grid-cols-2 md:gap-6">
|
||||||
<MetricListFrame title="Generations" description="Поколения пула ME">
|
<MetricListFrame title="Generations" description="Поколения пула ME">
|
||||||
<MetricRow
|
<MetricRow
|
||||||
|
|||||||
@@ -134,10 +134,29 @@ export interface RuntimeMeQualityDrainGateData {
|
|||||||
updated_at_epoch_secs?: number
|
updated_at_epoch_secs?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RuntimeMeQualityFamilyStateData {
|
||||||
|
family?: string
|
||||||
|
state?: string
|
||||||
|
state_since_epoch_secs?: number
|
||||||
|
suppressed_until_epoch_secs?: number | null
|
||||||
|
fail_streak?: number
|
||||||
|
recover_success_streak?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuntimeMeQualityDcRttData {
|
||||||
|
dc?: number
|
||||||
|
rtt_ema_ms?: number | null
|
||||||
|
alive_writers?: number
|
||||||
|
required_writers?: number
|
||||||
|
coverage_pct?: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface RuntimeMeQualityPayload {
|
export interface RuntimeMeQualityPayload {
|
||||||
counters?: RuntimeMeQualityCountersData
|
counters?: RuntimeMeQualityCountersData
|
||||||
route_drops?: RuntimeMeQualityRouteDropData
|
route_drops?: RuntimeMeQualityRouteDropData
|
||||||
drain_gate?: RuntimeMeQualityDrainGateData
|
drain_gate?: RuntimeMeQualityDrainGateData
|
||||||
|
family_states?: RuntimeMeQualityFamilyStateData[]
|
||||||
|
dc_rtt?: RuntimeMeQualityDcRttData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeMeQualityData {
|
export interface RuntimeMeQualityData {
|
||||||
@@ -147,6 +166,122 @@ export interface RuntimeMeQualityData {
|
|||||||
data?: RuntimeMeQualityPayload | null
|
data?: RuntimeMeQualityPayload | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** GET /v1/stats/dcs — per-DC endpoints + RTT */
|
||||||
|
export interface DcEndpointWriters {
|
||||||
|
endpoint?: string
|
||||||
|
active_writers?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DcStatus {
|
||||||
|
dc?: number
|
||||||
|
endpoints?: string[]
|
||||||
|
endpoint_writers?: DcEndpointWriters[]
|
||||||
|
available_endpoints?: number
|
||||||
|
available_pct?: number
|
||||||
|
required_writers?: number
|
||||||
|
floor_min?: number
|
||||||
|
floor_target?: number
|
||||||
|
floor_max?: number
|
||||||
|
floor_capped?: boolean
|
||||||
|
alive_writers?: number
|
||||||
|
coverage_pct?: number
|
||||||
|
fresh_alive_writers?: number
|
||||||
|
fresh_coverage_pct?: number
|
||||||
|
rtt_ms?: number | null
|
||||||
|
load?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DcStatusData {
|
||||||
|
middle_proxy_enabled?: boolean
|
||||||
|
reason?: string | null
|
||||||
|
generated_at_epoch_secs?: number
|
||||||
|
dcs?: DcStatus[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /v1/stats/me-writers — per-writer/endpoint latency */
|
||||||
|
export interface MeWriterStatus {
|
||||||
|
writer_id?: number
|
||||||
|
dc?: number | null
|
||||||
|
endpoint?: string
|
||||||
|
generation?: number
|
||||||
|
state?: string
|
||||||
|
draining?: boolean
|
||||||
|
degraded?: boolean
|
||||||
|
bound_clients?: number
|
||||||
|
idle_for_secs?: number | null
|
||||||
|
rtt_ema_ms?: number | null
|
||||||
|
matches_active_generation?: boolean
|
||||||
|
in_desired_map?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MeWritersSummary {
|
||||||
|
configured_dc_groups?: number
|
||||||
|
configured_endpoints?: number
|
||||||
|
available_endpoints?: number
|
||||||
|
available_pct?: number
|
||||||
|
required_writers?: number
|
||||||
|
alive_writers?: number
|
||||||
|
coverage_pct?: number
|
||||||
|
fresh_alive_writers?: number
|
||||||
|
fresh_coverage_pct?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MeWritersData {
|
||||||
|
middle_proxy_enabled?: boolean
|
||||||
|
reason?: string | null
|
||||||
|
generated_at_epoch_secs?: number
|
||||||
|
summary?: MeWritersSummary
|
||||||
|
writers?: MeWriterStatus[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseDcStatus(payload: unknown): DcStatusData {
|
||||||
|
const root = asRecord(unwrapData(payload) ?? payload)
|
||||||
|
const nested = asRecord(root.data)
|
||||||
|
if (Array.isArray(root.dcs)) {
|
||||||
|
return root as unknown as DcStatusData
|
||||||
|
}
|
||||||
|
if (Array.isArray(nested.dcs)) {
|
||||||
|
return {
|
||||||
|
middle_proxy_enabled: (nested.middle_proxy_enabled ?? root.middle_proxy_enabled) as
|
||||||
|
| boolean
|
||||||
|
| undefined,
|
||||||
|
reason: (nested.reason ?? root.reason) as string | null | undefined,
|
||||||
|
generated_at_epoch_secs: (nested.generated_at_epoch_secs ??
|
||||||
|
root.generated_at_epoch_secs) as number | undefined,
|
||||||
|
dcs: nested.dcs as DcStatus[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root as unknown as DcStatusData
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMeWriters(payload: unknown): MeWritersData {
|
||||||
|
const root = asRecord(unwrapData(payload) ?? payload)
|
||||||
|
const nested = asRecord(root.data)
|
||||||
|
if (Array.isArray(root.writers) || root.summary) {
|
||||||
|
return root as unknown as MeWritersData
|
||||||
|
}
|
||||||
|
if (Array.isArray(nested.writers) || nested.summary) {
|
||||||
|
return {
|
||||||
|
middle_proxy_enabled: (nested.middle_proxy_enabled ?? root.middle_proxy_enabled) as
|
||||||
|
| boolean
|
||||||
|
| undefined,
|
||||||
|
reason: (nested.reason ?? root.reason) as string | null | undefined,
|
||||||
|
generated_at_epoch_secs: (nested.generated_at_epoch_secs ??
|
||||||
|
root.generated_at_epoch_secs) as number | undefined,
|
||||||
|
summary: nested.summary as MeWritersSummary | undefined,
|
||||||
|
writers: nested.writers as MeWriterStatus[] | undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root as unknown as MeWritersData
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatRttMs(ms: unknown): string {
|
||||||
|
const n = typeof ms === 'number' ? ms : Number(ms)
|
||||||
|
if (!Number.isFinite(n) || n < 0) return '—'
|
||||||
|
if (n < 10) return `${n.toFixed(1)} ms`
|
||||||
|
return `${Math.round(n)} ms`
|
||||||
|
}
|
||||||
|
|
||||||
export function parseMePoolState(payload: unknown): RuntimeMePoolStateData {
|
export function parseMePoolState(payload: unknown): RuntimeMePoolStateData {
|
||||||
const root = asRecord(unwrapData(payload) ?? payload)
|
const root = asRecord(unwrapData(payload) ?? payload)
|
||||||
const nested = asRecord(root.data)
|
const nested = asRecord(root.data)
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import { api } from '@/lib/api-client'
|
|||||||
import {
|
import {
|
||||||
asRecord,
|
asRecord,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
|
parseDcStatus,
|
||||||
parseMePoolState,
|
parseMePoolState,
|
||||||
parseMeQuality,
|
parseMeQuality,
|
||||||
|
parseMeWriters,
|
||||||
unwrapData,
|
unwrapData,
|
||||||
} from '@/lib/telemt'
|
} from '@/lib/telemt'
|
||||||
|
|
||||||
@@ -34,6 +36,16 @@ function RuntimePage() {
|
|||||||
queryFn: () => api('/api/telemt/runtime/me_quality').catch(() => null),
|
queryFn: () => api('/api/telemt/runtime/me_quality').catch(() => null),
|
||||||
refetchInterval: 10_000,
|
refetchInterval: 10_000,
|
||||||
})
|
})
|
||||||
|
const meDcs = useQuery({
|
||||||
|
queryKey: ['telemt', 'stats_dcs'],
|
||||||
|
queryFn: () => api('/api/telemt/stats/dcs').catch(() => null),
|
||||||
|
refetchInterval: 10_000,
|
||||||
|
})
|
||||||
|
const meWriters = useQuery({
|
||||||
|
queryKey: ['telemt', 'stats_me_writers'],
|
||||||
|
queryFn: () => api('/api/telemt/stats/me-writers').catch(() => null),
|
||||||
|
refetchInterval: 10_000,
|
||||||
|
})
|
||||||
const gates = useQuery({
|
const gates = useQuery({
|
||||||
queryKey: ['telemt', 'gates'],
|
queryKey: ['telemt', 'gates'],
|
||||||
queryFn: () => api('/api/telemt/runtime/gates').catch(() => null),
|
queryFn: () => api('/api/telemt/runtime/gates').catch(() => null),
|
||||||
@@ -54,6 +66,8 @@ function RuntimePage() {
|
|||||||
|
|
||||||
const pool = parseMePoolState(mePool.data)
|
const pool = parseMePoolState(mePool.data)
|
||||||
const quality = parseMeQuality(meQuality.data)
|
const quality = parseMeQuality(meQuality.data)
|
||||||
|
const dcs = parseDcStatus(meDcs.data)
|
||||||
|
const writers = parseMeWriters(meWriters.data)
|
||||||
const gateData = asRecord(unwrapData(gates.data) ?? gates.data)
|
const gateData = asRecord(unwrapData(gates.data) ?? gates.data)
|
||||||
const conn = asRecord(unwrapData(connSummary.data) ?? connSummary.data)
|
const conn = asRecord(unwrapData(connSummary.data) ?? connSummary.data)
|
||||||
const connPayload = asRecord(conn.data ?? conn)
|
const connPayload = asRecord(conn.data ?? conn)
|
||||||
@@ -127,7 +141,14 @@ function RuntimePage() {
|
|||||||
<MePoolPanel
|
<MePoolPanel
|
||||||
pool={pool}
|
pool={pool}
|
||||||
quality={quality}
|
quality={quality}
|
||||||
isLoading={mePool.isLoading || meQuality.isLoading}
|
dcs={dcs}
|
||||||
|
writers={writers}
|
||||||
|
isLoading={
|
||||||
|
mePool.isLoading ||
|
||||||
|
meQuality.isLoading ||
|
||||||
|
meDcs.isLoading ||
|
||||||
|
meWriters.isLoading
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 md:gap-6">
|
<div className="grid gap-4 md:grid-cols-2 md:gap-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user