diff --git a/apps/web/src/components/runtime/me-pool-panel.tsx b/apps/web/src/components/runtime/me-pool-panel.tsx index b8f352b..c216ce2 100644 --- a/apps/web/src/components/runtime/me-pool-panel.tsx +++ b/apps/web/src/components/runtime/me-pool-panel.tsx @@ -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 draining + if (degraded) return degraded + if (state === 'active') return active + if (state === 'warm') return warm + return {state ?? '—'} +} + /** - * 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({
ME Pool - 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({

) : (

- 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)}

)} @@ -133,6 +190,220 @@ export function MePoolPanel({ + {/* Per-DC servers + RTT — list-11 DNA. Preview: https://reui.io/preview/base/list-11 */} + 0} + okLabel={`${dcRows.length} DC`} + failLabel={dcs.reason ? String(dcs.reason) : 'Нет DC'} + /> + } + > + {dcRows.length === 0 ? ( +

+ {dcs.reason ?? + 'Нет данных /v1/stats/dcs (нужен minimal_runtime_enabled) и нет dc_rtt из me_quality.'} +

+ ) : ( +
+ + + + + + + + + + + + + {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 ( + + + + + + + + + ) + })} + +
DCRTTWritersCoverageLoadEndpoints
+ DC {formatNumber(row.dc)} + + + {formatRttMs(row.rtt_ms)} + + + {formatNumber(row.alive_writers)} / {formatNumber(row.required_writers)} + +
+ + + {Number.isFinite(coverage) ? `${Math.round(coverage)}%` : '—'} + +
+
+ {row.load != null ? formatNumber(row.load) : '—'} + + {endpoints.length === 0 ? ( + + ) : ( +
    + {endpoints.slice(0, 4).map((ep) => { + const writersOnEp = (row.endpoint_writers ?? []).find( + (w) => w.endpoint === ep, + ) + return ( +
  • + {ep} + {writersOnEp?.active_writers != null ? ( + + {formatNumber(writersOnEp.active_writers)} w + + ) : null} +
  • + ) + })} + {endpoints.length > 4 ? ( +
  • + +{endpoints.length - 4} ещё +
  • + ) : null} +
+ )} +
+
+ )} +
+ + {/* Per-writer / endpoint latency */} + 0} + okLabel={`${writerRows.length} writers`} + failLabel={writers.reason ? String(writers.reason) : 'Нет writers'} + /> + } + > + {writerRows.length === 0 ? ( +

+ {writers.reason ?? + 'Нет данных /v1/stats/me-writers. Включите minimal_runtime_enabled в Telemt API.'} +

+ ) : ( +
+ + + + + + + + + + + + + {writerRows.slice(0, 40).map((w) => ( + + + + + + + + + ))} + +
EndpointDCСостояниеRTTКлиентыGen
{w.endpoint ?? '—'} + {w.dc != null ? formatNumber(w.dc) : '—'} + + + + + {formatRttMs(w.rtt_ema_ms)} + + + {formatNumber(w.bound_clients)} + + {formatNumber(w.generation)} +
+ {writerRows.length > 40 ? ( +

+ Показаны 40 из {writerRows.length} writers (сортировка по RTT). +

+ ) : null} +
+ )} +
+ + {(q?.family_states ?? []).length > 0 ? ( + +
+ + + + + + + + + + + {(q?.family_states ?? []).map((f) => ( + + + + + + + ))} + +
FamilyStateFail streakRecover streak
{f.family ?? '—'} + + {f.state ?? '—'} + + {formatNumber(f.fail_streak)} + {formatNumber(f.recover_success_streak)} +
+
+
+ ) : null} +
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() {