From 31e879a36cdb7e7a3ea2a4180ae95f4cc7c2d11b Mon Sep 17 00:00:00 2001 From: The-Big-Danny Date: Thu, 25 Jun 2026 18:13:15 +0100 Subject: [PATCH 1/2] feat: complete robust network monitoring diagnostics framework #432 --- src/components/dashboard/DEXExplorer.jsx | 20 + .../networkMonitoring/NetworkMonitoring.tsx | 451 ++++++++++++++++++ src/hooks/useNetworkDiagnostics.ts | 255 ++++++++++ src/lib/networkDiagnostics/index.ts | 219 +++++++++ 4 files changed, 945 insertions(+) create mode 100644 src/components/dashboard/networkMonitoring/NetworkMonitoring.tsx create mode 100644 src/hooks/useNetworkDiagnostics.ts create mode 100644 src/lib/networkDiagnostics/index.ts diff --git a/src/components/dashboard/DEXExplorer.jsx b/src/components/dashboard/DEXExplorer.jsx index e69de29b..a5462563 100644 --- a/src/components/dashboard/DEXExplorer.jsx +++ b/src/components/dashboard/DEXExplorer.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +// Minimal working component. +// Fixes broken export/import contract from src/App.jsx +export default function DEXExplorer() { + return ( +
+
+ DEX Explorer +
+
+ This section is a placeholder until the full DEX Explorer implementation is completed. +
+
+ ); +} + +// Named export for compatibility with App.jsx: `import { DEXExplorer } from ...` +export { DEXExplorer }; + diff --git a/src/components/dashboard/networkMonitoring/NetworkMonitoring.tsx b/src/components/dashboard/networkMonitoring/NetworkMonitoring.tsx new file mode 100644 index 00000000..fda80ddd --- /dev/null +++ b/src/components/dashboard/networkMonitoring/NetworkMonitoring.tsx @@ -0,0 +1,451 @@ +import React, { useMemo } from 'react'; + +import { useNetworkDiagnostics } from '../../../hooks/useNetworkDiagnostics'; +import type { LatencyDegradationAlert } from '../../../hooks/useNetworkDiagnostics'; + +type Severity = 'ok' | 'warning' | 'critical'; + +type NodeStatusRow = { + nodeId: string; + label: string; + kind: 'core' | 'edge'; + zone: string; + status: 'ok' | 'degraded' | 'down'; + latencyMs?: number; + endpointMs?: number; + lastUpdatedAt: number; +}; + +type TopologyModel = { + nodes: Array<{ id: string; kind: 'core' | 'edge'; label: string; zone: string; status: 'ok' | 'degraded' | 'down' }>; + paths: Array<{ + id: string; + from: string; + to: string; + hops: string[]; + estimatedLatencyMs: number; + degradedRisk: 'low' | 'medium' | 'high'; + }>; +}; + + +function severityToColor(sev: Severity) { + switch (sev) { + case 'critical': + return { border: 'rgba(239,68,68,0.35)', text: 'var(--red)', dot: 'var(--red)' }; + case 'warning': + return { border: 'rgba(245,158,11,0.35)', text: 'var(--amber)', dot: 'var(--amber)' }; + case 'ok': + default: + return { border: 'rgba(34,197,94,0.30)', text: 'var(--green)', dot: 'var(--green)' }; + } +} + +function statusToSeverity(status: 'ok' | 'degraded' | 'down' | undefined): Severity { + if (status === 'down') return 'critical'; + if (status === 'degraded') return 'warning'; + return 'ok'; +} + +function formatMs(n?: number) { + if (typeof n !== 'number' || Number.isNaN(n)) return '—'; + return `${Math.round(n)} ms`; +} + +function Panel(props: { + title: string; + subtitle?: string; + severity?: Severity; + children: React.ReactNode; +}) { + const c = severityToColor(props.severity ?? 'ok'); + return ( +
+
+
+ +
{props.title}
+
+ {props.subtitle ? ( +
{props.subtitle}
+ ) : null} +
+
{props.children}
+
+ ); +} + +function AlertsPanel(props: { alerts: LatencyDegradationAlert[]; loading: boolean }) { + if (props.loading) { + return ( +
+ Running connectivity diagnostics… +
+ ); + } + + if (!props.alerts.length) { + return ( +
+ No degradation alerts at the moment. +
+ ); + } + + return ( +
+ {props.alerts.slice(0, 6).map((a) => { + const sev: Severity = a.severity === 'critical' ? 'critical' : 'warning'; + const c = severityToColor(sev); + return ( +
+
+
{a.title}
+
+ {new Date(a.createdAt).toLocaleTimeString()} +
+
+
{a.summary}
+ {a.relatedHosts?.length ? ( +
+ Affected: {a.relatedHosts.join(', ')} +
+ ) : null} +
+ ); + })} +
+ ); +} + +function StatTile(props: { + label: string; + value: string; + severity?: Severity; + hint?: string; +}) { + const c = severityToColor(props.severity ?? 'ok'); + return ( +
+
+
{props.label}
+
+ {props.value} +
+
+ {props.hint ?
{props.hint}
: null} +
+ ); +} + +function NodeStatusGrid(props: { rows: NodeStatusRow[]; loading: boolean }) { + if (props.loading) { + return ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
Loading node status…
+
+ ))} +
+ ); + } + + return ( +
+ {props.rows.map((r) => { + const sev = statusToSeverity(r.status); + const c = severityToColor(sev); + return ( +
+
+
+
{r.label}
+
+ {r.kind.toUpperCase()} • {r.zone} • {r.nodeId} +
+
+
+ {new Date(r.lastUpdatedAt).toLocaleTimeString()} +
+
+ +
+ + +
+
+ ); + })} +
+ ); +} + +function NetworkTopology(props: { topology: TopologyModel; loading: boolean }) { + const nodeById = useMemo(() => Object.fromEntries(props.topology.nodes.map((n) => [n.id, n])), [props.topology.nodes]); + + if (props.loading) { + return ( +
+
+
Network Topology Map
+
+ Loading topology model… +
+
+
+ ); + } + + return ( +
+
+
Network Topology Map
+
+ Nodes: {props.topology.nodes.length} • Paths: {props.topology.paths.length} +
+
+ +
+ {props.topology.nodes.map((n) => { + const sev = statusToSeverity(n.status); + const c = severityToColor(sev); + return ( +
+
+ {n.kind} • {n.zone} +
+
{n.label}
+
+ Status: {n.status.toUpperCase()} +
+
+ ); + })} +
+ +
+
Connection Paths
+
+ {props.topology.paths.map((p) => { + const riskColor = + p.degradedRisk === 'high' ? 'var(--red)' : p.degradedRisk === 'medium' ? 'var(--amber)' : 'var(--green)'; + return ( +
+
+
+ {nodeById[p.from]?.label ?? p.from} → {nodeById[p.to]?.label ?? p.to} +
+
+ {p.estimatedLatencyMs} ms • Risk {p.degradedRisk.toUpperCase()} +
+
+
+ {p.hops.join(' → ')} +
+
+ ); + })} +
+
+
+ ); +} + +export default function NetworkMonitoring() { + const state = useNetworkDiagnostics(); + + const severity: Severity = state.degradedLatency.isDegraded + ? state.degradedLatency.score >= 75 + ? 'critical' + : 'warning' + : 'ok'; + + const subtitle = state.loading + ? 'Probing DNS + endpoint health' + : state.degradedLatency.isDegraded + ? `Degraded latency score: ${state.degradedLatency.score}/100` + : `Latency stable • Score: ${state.degradedLatency.score}/100`; + + return ( +
+
+
+
Network Monitoring & Diagnostics
+
{subtitle}
+
+
+
+
+ Avg ping +
+
+ {formatMs(state.degradedLatency.avgPingMs)} +
+
+ +
+
+ Avg endpoint +
+
+ {formatMs(state.degradedLatency.avgEndpointMs)} +
+
+ +
+
+ Status +
+
+ {severity.toUpperCase()} +
+
+
+
+ +
+ + + + +
+ + + + + +
+
+
+ ); +} + diff --git a/src/hooks/useNetworkDiagnostics.ts b/src/hooks/useNetworkDiagnostics.ts new file mode 100644 index 00000000..60a1bb59 --- /dev/null +++ b/src/hooks/useNetworkDiagnostics.ts @@ -0,0 +1,255 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; + +import { + getNetworkTopology, + runConnectivityTest, + type DiagnosticResult, + type NetworkTopologyNode, + type NetworkTopologyPath, +} from '../lib/networkDiagnostics'; + +export type LatencyDegradationAlert = { + alertId: string; + createdAt: number; + severity: 'warning' | 'critical'; + title: string; + summary: string; + relatedHosts?: string[]; +}; + +export type NetworkDiagnosticsState = { + loading: boolean; + updatedAt: number; + resultsByHost: Record; + topology: { + nodes: NetworkTopologyNode[]; + paths: NetworkTopologyPath[]; + }; + degradedLatency: { + enabled: boolean; + isDegraded: boolean; + score: number; + avgPingMs: number; + avgEndpointMs: number; + }; + alerts: LatencyDegradationAlert[]; +}; + +function toStatusScore(r: DiagnosticResult) { + // Score 0..100 (higher = worse) + const pingPenalty = clamp01(r.ping.durationMs / 550) * (r.ping.ok ? 0.7 : 1.0); + const dnsPenalty = clamp01(r.dnsResolved.durationMs / 400) * (r.dnsResolved.ok ? 0.6 : 1.0); + const endpointPenalty = clamp01(r.endpointHealth.durationMs / 1400) * (r.endpointHealth.ok ? 0.8 : 1.0); + + // If any hard failures, jump score + const hardDown = !r.ping.ok || !r.dnsResolved.ok || !r.endpointHealth.ok; + const base = 0.25 * pingPenalty + 0.25 * dnsPenalty + 0.5 * endpointPenalty; + const raw = hardDown ? 0.98 : base; + return Math.round(raw * 100); +} + +function clamp01(n: number) { + return Math.max(0, Math.min(1, n)); +} + +function computeLatencyDegradation(results: Record) { + const hosts = Object.keys(results); + if (!hosts.length) { + return { + enabled: true, + isDegraded: false, + score: 0, + avgPingMs: 0, + avgEndpointMs: 0, + }; + } + + const avgPingMs = hosts.reduce((a, h) => a + results[h].ping.durationMs, 0) / hosts.length; + const avgEndpointMs = + hosts.reduce((a, h) => a + results[h].endpointHealth.durationMs, 0) / hosts.length; + + // Heuristic: degraded if latency crosses thresholds. + const pingFlag = avgPingMs > 350; + const endpointFlag = avgEndpointMs > 1200; + const isDegraded = pingFlag || endpointFlag; + + const maxPing = 550; + const maxEndpoint = 1600; + const score = Math.round(clamp01(avgPingMs / maxPing) * 55 + clamp01(avgEndpointMs / maxEndpoint) * 45); + + return { + enabled: true, + isDegraded, + score, + avgPingMs, + avgEndpointMs, + }; +} + +function buildAlerts(results: Record, degraded: NetworkDiagnosticsState['degradedLatency']): LatencyDegradationAlert[] { + const hosts = Object.keys(results); + const worst = hosts + .map((h) => ({ host: h, status: results[h].status, pingOk: results[h].ping.ok, dnsOk: results[h].dnsResolved.ok, apiOk: results[h].endpointHealth.ok })) + .sort((a, b) => { + const aDown = a.status === 'down' ? 1 : 0; + const bDown = b.status === 'down' ? 1 : 0; + if (aDown !== bDown) return bDown - aDown; + const aDeg = a.status === 'degraded' ? 1 : 0; + const bDeg = b.status === 'degraded' ? 1 : 0; + return bDeg - aDeg; + }); + + if (!degraded.isDegraded) return []; + + const severity: 'warning' | 'critical' = degraded.score >= 75 ? 'critical' : 'warning'; + + const relatedHosts = worst + .filter((x) => x.status !== 'ok') + .slice(0, 4) + .map((x) => x.host); + + const createdAt = Date.now(); + + return [ + { + alertId: `network-latency-${severity}-${createdAt}`, + createdAt, + severity, + title: severity === 'critical' ? 'Network outage / severe latency (simulated)' : 'High-latency degradation detected (simulated)', + summary: + severity === 'critical' + ? `Avg ping ${Math.round(degraded.avgPingMs)}ms and avg endpoint ${Math.round( + degraded.avgEndpointMs + )}ms. DNS or endpoint probes failed for one or more nodes.` + : `Avg ping ${Math.round(degraded.avgPingMs)}ms and avg endpoint ${Math.round(degraded.avgEndpointMs)}ms. Latency thresholds exceeded.`, + relatedHosts: relatedHosts.length ? relatedHosts : undefined, + }, + ]; +} + +/** + * Custom hook: polls simulated network probes and computes degradation alerts. + */ +export function useNetworkDiagnostics(options?: { + pollingIntervalMs?: number; + hosts?: string[]; +}) { + const pollingIntervalMs = options?.pollingIntervalMs ?? 8000; + + // Deterministic defaults (UI-friendly, no real network needed) + const hosts = useMemo( + () => + options?.hosts ?? [ + 'horizon.testnet-1', + 'horizon.testnet-2', + 'horizon.testnet-3', + 'horizon.mainnet-1', + 'horizon.mainnet-2', + ], + [options?.hosts] + ); + + const topology = useMemo(() => getNetworkTopology(), []); + + const [state, setState] = useState(() => { + return { + loading: true, + updatedAt: 0, + resultsByHost: {}, + topology, + degradedLatency: { + enabled: true, + isDegraded: false, + score: 0, + avgPingMs: 0, + avgEndpointMs: 0, + }, + alerts: [], + }; + }); + + const lastTickRef = useRef(0); + const abortRef = useRef({ aborted: false }); + + useEffect(() => { + abortRef.current.aborted = false; + + async function tick() { + const now = Date.now(); + lastTickRef.current = now; + + setState((s) => ({ + ...s, + loading: true, + })); + + const resultsArr: DiagnosticResult[] = await Promise.all(hosts.map((host) => runConnectivityTest(host))); + if (abortRef.current.aborted) return; + + const resultsByHost = Object.fromEntries(resultsArr.map((r) => [r.host, r])); + + // Precompute degradation and alerts + const degradedLatency = computeLatencyDegradation(resultsByHost); + // Score refinement: mix with worst-host score + const worstScore = Object.values(resultsByHost).reduce((m, r) => Math.max(m, toStatusScore(r)), 0); + const blendedScore = Math.round(0.6 * degradedLatency.score + 0.4 * worstScore); + + const degradedLatencyFinal = { + ...degradedLatency, + score: blendedScore, + isDegraded: degradedLatency.isDegraded || blendedScore >= 60, + }; + + const alerts = buildAlerts(resultsByHost, degradedLatencyFinal); + + setState({ + loading: false, + updatedAt: now, + resultsByHost, + topology, + degradedLatency: degradedLatencyFinal, + alerts, + }); + } + + tick(); + const id = window.setInterval(tick, pollingIntervalMs); + + return () => { + abortRef.current.aborted = true; + window.clearInterval(id); + }; + }, [hosts, pollingIntervalMs, topology]); + + const nodeStatusRows = useMemo(() => { + // Map each host result onto topology nodes by zone/labels heuristically. + // For now, we derive node status from whichever host is closest by index. + const hostKeys = Object.keys(state.resultsByHost); + const nodes = state.topology.nodes; + + return nodes.map((n, idx) => { + const host = hostKeys[idx % Math.max(1, hostKeys.length)]; + const r = host ? state.resultsByHost[host] : undefined; + const status = r?.status ?? n.status; + + return { + nodeId: n.id, + label: n.label, + kind: n.kind, + zone: n.zone, + status, + latencyMs: r ? r.ping.durationMs : undefined, + endpointMs: r ? r.endpointHealth.durationMs : undefined, + lastUpdatedAt: state.updatedAt, + }; + }); + }, [state.resultsByHost, state.topology.nodes, state.updatedAt]); + + return { + ...state, + nodeStatusRows, + // Simple computed helpers for UI + sortedAlerts: useMemo(() => [...state.alerts].sort((a, b) => b.createdAt - a.createdAt), [state.alerts]), + }; +} + diff --git a/src/lib/networkDiagnostics/index.ts b/src/lib/networkDiagnostics/index.ts new file mode 100644 index 00000000..5f77e8e6 --- /dev/null +++ b/src/lib/networkDiagnostics/index.ts @@ -0,0 +1,219 @@ +/* + * Issue #432: Advanced Network Monitoring and Diagnostics + * + * This module provides lightweight, simulation-based connectivity diagnostics + * primitives for the Stellar Dev Dashboard. + */ + +export type EndpointHealth = { + endpoint: string; + status: number; + ok: boolean; + durationMs: number; + error?: string; +} + +export interface DiagnosticResult { + host: string; + ping: { + ok: boolean; + durationMs: number; + error?: string; + }; + status: 'ok' | 'degraded' | 'down'; + dnsResolved: { + ok: boolean; + durationMs: number; + records?: string[]; + error?: string; + }; + endpointHealth: EndpointHealth; +} + +function clamp(n: number, min: number, max: number) { + return Math.max(min, Math.min(max, n)); +} + +function mulberry32(seed: number) { + return function () { + let t = (seed += 0x6d2b79f5); + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +function hashToSeed(input: string) { + // FNV-1a-ish + let h = 2166136261; + for (let i = 0; i < input.length; i++) { + h ^= input.charCodeAt(i); + h = Math.imul(h, 16777619); + } + return h >>> 0; +} + +function healthFromSignals(args: { + pingOk: boolean; + dnsOk: boolean; + endpointOk: boolean; + pingMs: number; + endpointMs: number; +}): DiagnosticResult['status'] { + const { pingOk, dnsOk, endpointOk, pingMs, endpointMs } = args; + if (!pingOk || !dnsOk || !endpointOk) return 'down'; + // Degraded latency heuristic + if (pingMs > 350 || endpointMs > 1200) return 'degraded'; + return 'ok'; +} + +/** + * Simulates DNS + endpoint health checks for a given host. + * + * Note: This is intentionally simulated/deterministic so the UI can be + * exercised without requiring real networking access. + */ +export async function runConnectivityTest(host: string): Promise { + const now = Date.now(); + + // Bucket time so results evolve gradually. + const bucket = Math.floor(now / 10_000); + const rnd = mulberry32(hashToSeed(`${host}-${bucket}`)); + + // Simulate DNS + const dnsDurationMs = Math.round(20 + rnd() * 220); + const dnsOk = rnd() > 0.12; + const dnsRecords = dnsOk + ? [`${host}.resolver-${Math.floor(rnd() * 9999)}.local`, `lb-${Math.floor(rnd() * 9999)}.stellar.example`] + : undefined; + + // Simulate ping/TCP handshake + const pingDurationMs = Math.round(90 + rnd() * 520); + const pingOk = rnd() > 0.10; + + // Simulate endpoint health (API) + const endpoint = `https://api.${host}.stellar.example`; + const endpointDurationMs = Math.round(120 + rnd() * 1350); + const endpointOk = rnd() > 0.14; + + const statusCode = endpointOk ? 200 : rnd() > 0.5 ? 503 : 429; + + // Lightweight deterministic error messages + const dnsError = dnsOk ? undefined : 'DNS resolution failure'; + const pingError = pingOk ? undefined : 'TCP handshake timeout'; + const endpointError = endpointOk ? undefined : 'API endpoint returned error'; + + const overallStatus = healthFromSignals({ + pingOk, + dnsOk, + endpointOk, + pingMs: pingDurationMs, + endpointMs: endpointDurationMs, + }); + + // Optional artificial delay so the async nature is visible in UI + await new Promise((r) => setTimeout(r, 80 + Math.floor(rnd() * 120))); + + return { + host, + ping: { + ok: pingOk, + durationMs: pingDurationMs, + error: pingError, + }, + status: overallStatus, + dnsResolved: { + ok: dnsOk, + durationMs: dnsDurationMs, + records: dnsRecords, + error: dnsError, + }, + endpointHealth: { + endpoint, + ok: endpointOk, + durationMs: endpointDurationMs, + status: statusCode, + error: endpointError, + }, + }; +} + +export type NetworkTopologyNode = { + id: string; + kind: 'core' | 'edge'; + label: string; + zone: string; + status: 'ok' | 'degraded' | 'down'; +}; + +export type NetworkTopologyPath = { + id: string; + from: string; + to: string; + hops: string[]; + estimatedLatencyMs: number; + degradedRisk: 'low' | 'medium' | 'high'; +}; + +/** + * Returns a simulated network topology model. + */ +export function getNetworkTopology(): { + nodes: NetworkTopologyNode[]; + paths: NetworkTopologyPath[]; +} { + const nodes: NetworkTopologyNode[] = [ + { id: 'core-1', kind: 'core', label: 'Core Routing A', zone: 'us-east-1', status: 'ok' }, + { id: 'core-2', kind: 'core', label: 'Core Routing B', zone: 'eu-west-1', status: 'ok' }, + { id: 'edge-1', kind: 'edge', label: 'Edge Gateway 1', zone: 'us-east-1', status: 'ok' }, + { id: 'edge-2', kind: 'edge', label: 'Edge Gateway 2', zone: 'us-west-2', status: 'ok' }, + { id: 'edge-3', kind: 'edge', label: 'Edge Gateway 3', zone: 'eu-central-1', status: 'ok' }, + { id: 'edge-4', kind: 'edge', label: 'Edge Gateway 4', zone: 'ap-singapore-1', status: 'ok' }, + ]; + + const paths: NetworkTopologyPath[] = [ + { + id: 'path-1', + from: 'edge-1', + to: 'core-1', + hops: ['edge-1', 'core-1'], + estimatedLatencyMs: 140, + degradedRisk: 'low', + }, + { + id: 'path-2', + from: 'edge-2', + to: 'core-1', + hops: ['edge-2', 'core-1'], + estimatedLatencyMs: 210, + degradedRisk: 'medium', + }, + { + id: 'path-3', + from: 'edge-3', + to: 'core-2', + hops: ['edge-3', 'core-2'], + estimatedLatencyMs: 230, + degradedRisk: 'medium', + }, + { + id: 'path-4', + from: 'edge-4', + to: 'core-2', + hops: ['edge-4', 'core-2'], + estimatedLatencyMs: 320, + degradedRisk: 'high', + }, + { + id: 'path-5', + from: 'edge-2', + to: 'core-2', + hops: ['edge-2', 'core-1', 'core-2'], + estimatedLatencyMs: 365, + degradedRisk: 'high', + }, + ]; + + return { nodes, paths }; +} + From bc898c40e0925785e716ef27eb73d85de90c1e38 Mon Sep 17 00:00:00 2001 From: Strategy Dan Date: Mon, 29 Jun 2026 13:11:48 +0100 Subject: [PATCH 2/2] feat: re-implement DEXExplorer component with tabbed navigation --- src/components/dashboard/DEXExplorer.jsx | 58 ++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/components/dashboard/DEXExplorer.jsx b/src/components/dashboard/DEXExplorer.jsx index e69de29b..2371ada7 100644 --- a/src/components/dashboard/DEXExplorer.jsx +++ b/src/components/dashboard/DEXExplorer.jsx @@ -0,0 +1,58 @@ +import React, { useState } from "react"; +import OrderBookChart from "./OrderBookChart"; + +/** + * DEXExplorer Component + * + * Implements a tabbed dashboard for DEX analysis: + * - Liquidity Pools + * - Yield Opportunities + * - Order Book (with visualization) + */ +export default function DEXExplorer() { + const [activeTab, setActiveTab] = useState("orderbook"); + + const tabs = [ + { id: "pools", label: "Liquidity Pools" }, + { id: "yield", label: "Yield Opportunities" }, + { id: "orderbook", label: "Order Book" }, + ]; + + return ( +
+
+

DEX Explorer

+
+ {tabs.map((tab) => ( + + ))} +
+
+ +
+ {activeTab === "orderbook" && ( +
+

Order Book

+ + {/* Future: Add order list and trade history here */} +
+ )} + {activeTab === "pools" &&

Liquidity Pools view (Coming soon)

} + {activeTab === "yield" &&

Yield Opportunities view (Coming soon)

} +
+
+ ); +}