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.'}
+
+ ) : (
+
+
+
+
+ | DC |
+ RTT |
+ Writers |
+ Coverage |
+ Load |
+ Endpoints |
+
+
+
+ {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 (
+
+ |
+ 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.'}
+
+ ) : (
+
+
+
+
+ | Endpoint |
+ DC |
+ Состояние |
+ RTT |
+ Клиенты |
+ Gen |
+
+
+
+ {writerRows.slice(0, 40).map((w) => (
+
+ | {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 ? (
+
+
+
+
+
+ | Family |
+ State |
+ Fail streak |
+ Recover streak |
+
+
+
+ {(q?.family_states ?? []).map((f) => (
+
+ | {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() {