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.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) => (
+
+ ))}
+
+ );
+ }
+
+ 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 };
+}
+