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 { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle } from '@/components/reui/frame'
|
||||
import { MetricListFrame, MetricRow, StatusBadge } from '@/components/metric-list'
|
||||
import { cn } from '@telemt/ui/lib/utils'
|
||||
import { Progress } from '@telemt/ui/components/progress'
|
||||
import {
|
||||
formatEpoch,
|
||||
formatNumber,
|
||||
formatRttMs,
|
||||
type DcStatusData,
|
||||
type MeWritersData,
|
||||
type RuntimeMePoolStateData,
|
||||
type RuntimeMeQualityData,
|
||||
} 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.
|
||||
* Preview: https://reui.io/preview/base/stats-7
|
||||
* ME Telemt status — stats-7 + DC/endpoint RTT tables (telemt_panel SoT).
|
||||
* 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
|
||||
*/
|
||||
export function MePoolPanel({
|
||||
pool,
|
||||
quality,
|
||||
dcs,
|
||||
writers,
|
||||
isLoading,
|
||||
}: {
|
||||
pool: RuntimeMePoolStateData
|
||||
quality: RuntimeMeQualityData
|
||||
dcs: DcStatusData
|
||||
writers: MeWritersData
|
||||
isLoading?: boolean
|
||||
}) {
|
||||
const enabled = pool.enabled !== false
|
||||
const writers = pool.data?.writers
|
||||
const poolWriters = pool.data?.writers
|
||||
const generations = pool.data?.generations
|
||||
const hardswap = pool.data?.hardswap
|
||||
const refill = pool.data?.refill
|
||||
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 = [
|
||||
{
|
||||
title: 'Живые writers',
|
||||
subtitle: 'alive_non_draining',
|
||||
value: formatNumber(writers?.alive_non_draining),
|
||||
badge: {
|
||||
text: 'healthy',
|
||||
variant: 'success-light' as const,
|
||||
},
|
||||
subtext: `всего ${formatNumber(writers?.total)} · contour active ${formatNumber(writers?.contour?.active)}`,
|
||||
value: formatNumber(poolWriters?.alive_non_draining ?? writers.summary?.alive_writers),
|
||||
badge: { text: 'healthy', variant: 'success-light' as const },
|
||||
subtext: `всего ${formatNumber(poolWriters?.total ?? writers.summary?.configured_endpoints)} · coverage ${formatNumber(writers.summary?.coverage_pct)}%`,
|
||||
},
|
||||
{
|
||||
title: 'Draining',
|
||||
subtitle: 'draining',
|
||||
value: formatNumber(writers?.draining),
|
||||
badge: {
|
||||
text: 'drain',
|
||||
variant: 'warning-light' as const,
|
||||
},
|
||||
subtext: `contour draining ${formatNumber(writers?.contour?.draining)}`,
|
||||
value: formatNumber(poolWriters?.draining),
|
||||
badge: { text: 'drain', variant: 'warning-light' as const },
|
||||
subtext: `contour draining ${formatNumber(poolWriters?.contour?.draining)}`,
|
||||
},
|
||||
{
|
||||
title: 'Degraded',
|
||||
subtitle: 'degraded',
|
||||
value: formatNumber(writers?.degraded),
|
||||
value: formatNumber(poolWriters?.degraded),
|
||||
badge: {
|
||||
text: writers && (writers.degraded ?? 0) > 0 ? 'attention' : 'ok',
|
||||
text: poolWriters && (poolWriters.degraded ?? 0) > 0 ? 'attention' : 'ok',
|
||||
variant:
|
||||
writers && (writers.degraded ?? 0) > 0
|
||||
poolWriters && (poolWriters.degraded ?? 0) > 0
|
||||
? ('destructive-light' 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">
|
||||
<FrameTitle>ME Pool</FrameTitle>
|
||||
<FrameDescription>
|
||||
Middle-End writers, generations и refill ·{' '}
|
||||
Middle-End writers, DC latency и endpoints ·{' '}
|
||||
{pool.generated_at_epoch_secs
|
||||
? formatEpoch(pool.generated_at_epoch_secs)
|
||||
: isLoading
|
||||
@@ -97,9 +154,9 @@ export function MePoolPanel({
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Writers health: healthy {formatNumber(writers?.health?.healthy)} ·
|
||||
degraded {formatNumber(writers?.health?.degraded)} · draining{' '}
|
||||
{formatNumber(writers?.health?.draining)}
|
||||
Writers health: healthy {formatNumber(poolWriters?.health?.healthy)} ·
|
||||
degraded {formatNumber(poolWriters?.health?.degraded)} · draining{' '}
|
||||
{formatNumber(poolWriters?.health?.draining)}
|
||||
</p>
|
||||
)}
|
||||
</FramePanel>
|
||||
@@ -133,6 +190,220 @@ export function MePoolPanel({
|
||||
</FramePanel>
|
||||
</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">
|
||||
<MetricListFrame title="Generations" description="Поколения пула ME">
|
||||
<MetricRow
|
||||
|
||||
@@ -134,10 +134,29 @@ export interface RuntimeMeQualityDrainGateData {
|
||||
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 {
|
||||
counters?: RuntimeMeQualityCountersData
|
||||
route_drops?: RuntimeMeQualityRouteDropData
|
||||
drain_gate?: RuntimeMeQualityDrainGateData
|
||||
family_states?: RuntimeMeQualityFamilyStateData[]
|
||||
dc_rtt?: RuntimeMeQualityDcRttData[]
|
||||
}
|
||||
|
||||
export interface RuntimeMeQualityData {
|
||||
@@ -147,6 +166,122 @@ export interface RuntimeMeQualityData {
|
||||
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 {
|
||||
const root = asRecord(unwrapData(payload) ?? payload)
|
||||
const nested = asRecord(root.data)
|
||||
|
||||
@@ -9,8 +9,10 @@ import { api } from '@/lib/api-client'
|
||||
import {
|
||||
asRecord,
|
||||
formatNumber,
|
||||
parseDcStatus,
|
||||
parseMePoolState,
|
||||
parseMeQuality,
|
||||
parseMeWriters,
|
||||
unwrapData,
|
||||
} from '@/lib/telemt'
|
||||
|
||||
@@ -34,6 +36,16 @@ function RuntimePage() {
|
||||
queryFn: () => api('/api/telemt/runtime/me_quality').catch(() => null),
|
||||
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({
|
||||
queryKey: ['telemt', 'gates'],
|
||||
queryFn: () => api('/api/telemt/runtime/gates').catch(() => null),
|
||||
@@ -54,6 +66,8 @@ function RuntimePage() {
|
||||
|
||||
const pool = parseMePoolState(mePool.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 conn = asRecord(unwrapData(connSummary.data) ?? connSummary.data)
|
||||
const connPayload = asRecord(conn.data ?? conn)
|
||||
@@ -127,7 +141,14 @@ function RuntimePage() {
|
||||
<MePoolPanel
|
||||
pool={pool}
|
||||
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">
|
||||
|
||||
Reference in New Issue
Block a user