From da7b52e9ea2b2a0e520273c5229d30c54d5f6326 Mon Sep 17 00:00:00 2001 From: Ganeshbeldar015 Date: Sun, 1 Nov 2026 02:53:40 +0530 Subject: [PATCH 1/5] feat(ui): redesign autonomous dependency DAG graph representation --- .../components/topology/TopologyGraph.test.ts | 68 ++ .../src/components/topology/TopologyGraph.tsx | 1023 ++++++++++++++--- apps/web/src/index.css | 34 + 3 files changed, 981 insertions(+), 144 deletions(-) create mode 100644 apps/web/src/components/topology/TopologyGraph.test.ts diff --git a/apps/web/src/components/topology/TopologyGraph.test.ts b/apps/web/src/components/topology/TopologyGraph.test.ts new file mode 100644 index 0000000..780d1dc --- /dev/null +++ b/apps/web/src/components/topology/TopologyGraph.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'bun:test'; +import { TopologyGraph } from './TopologyGraph'; +import { clusterState } from '../../engine/state'; + +describe('TopologyGraph Component & Autonomous DAG Suite', () => { + it('exports TopologyGraph as a valid React functional component', () => { + expect(TopologyGraph).toBeDefined(); + expect(typeof TopologyGraph).toBe('function'); + }); + + it('validates 7 enterprise nodes in cluster state', () => { + const nodes = clusterState.getNodes(); + expect(nodes.length).toBeGreaterThanOrEqual(7); + + const ids = nodes.map((n) => n.id); + expect(ids).toContain('db-primary'); + expect(ids).toContain('db-replica'); + expect(ids).toContain('redis-cache'); + expect(ids).toContain('auth-service'); + expect(ids).toContain('payment-service'); + expect(ids).toContain('api-gateway'); + expect(ids).toContain('web-frontend'); + }); + + it('verifies DAG is cycle-free with Kahn topological sort', () => { + const graph = clusterState.getGraph(); + expect(graph.hasCycle()).toBe(false); + + const order = graph.getTopologicalRecoveryOrder(); + expect(order).toBeDefined(); + expect(order.length).toBe(7); + + // Root database must be restored before dependent microservices + const dbIndex = order.indexOf('db-primary'); + const authIndex = order.indexOf('auth-service'); + const gatewayIndex = order.indexOf('api-gateway'); + const webIndex = order.indexOf('web-frontend'); + + expect(dbIndex).toBeLessThan(authIndex); + expect(authIndex).toBeLessThan(gatewayIndex); + expect(gatewayIndex).toBeLessThan(webIndex); + }); + + it('verifies reverse blast radius calculation for root db outage', () => { + const graph = clusterState.getGraph(); + const blast = graph.computeBlastRadius('db-primary'); + + expect(blast.targetNodeId).toBe('db-primary'); + // Primary DB failure cascades to cache, replica, auth, payment, gateway, web + expect(blast.affectedNodeIds).toContain('redis-cache'); + expect(blast.affectedNodeIds).toContain('db-replica'); + expect(blast.affectedNodeIds).toContain('auth-service'); + expect(blast.affectedNodeIds).toContain('payment-service'); + expect(blast.affectedNodeIds).toContain('api-gateway'); + expect(blast.affectedNodeIds).toContain('web-frontend'); + expect(blast.severity).toBe('critical'); + }); + + it('verifies minimal leaf blast radius for web frontend outage', () => { + const graph = clusterState.getGraph(); + const blast = graph.computeBlastRadius('web-frontend'); + + expect(blast.targetNodeId).toBe('web-frontend'); + // Frontend is an edge leaf node; downtime should not cascade to core databases + expect(blast.affectedNodeIds.length).toBe(0); + expect(blast.severity).toBe('low'); + }); +}); diff --git a/apps/web/src/components/topology/TopologyGraph.tsx b/apps/web/src/components/topology/TopologyGraph.tsx index 05a53b0..4470020 100644 --- a/apps/web/src/components/topology/TopologyGraph.tsx +++ b/apps/web/src/components/topology/TopologyGraph.tsx @@ -1,31 +1,285 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { Card } from '../common/Card'; import { Badge } from '../common/Badge'; import { Button } from '../common/Button'; import { fetchNodes, fetchGraph, triggerSimulation } from '../../lib/api'; +import { clusterState } from '../../engine/state'; import type { SystemNode } from '../../types'; +import { cn } from '../../lib/utils'; import { Network, - CheckCircle2, - AlertOctagon, + Database, + Globe, + Zap, + ShieldCheck, + CreditCard, + Monitor, RefreshCw, + AlertOctagon, + CheckCircle2, + AlertTriangle, + ArrowDown, + Layers, + Activity, + Flame, + Radio, + Server, + Play, + RotateCcw, } from 'lucide-react'; -export const TopologyGraph: React.FC = () => { +export interface TopologyGraphProps { + className?: string; +} + +interface NodeLayoutMeta { + id: string; + name: string; + tier: number; + tierLabel: string; + tierName: string; + recoveryStep: number; + type: 'database' | 'application' | 'gateway' | 'cache'; + port: string; + latency: string; + protocol: string; + icon: React.ElementType; + iconColor: string; + iconBg: string; + engine: string; + description: string; + x: number; + y: number; +} + +const STATIC_NODE_METAS: Record = { + 'web-frontend': { + id: 'web-frontend', + name: 'Horizon Web UI', + tier: 3, + tierLabel: 'Tier 3', + tierName: 'Client Ingress', + recoveryStep: 5, + type: 'application', + port: ':3000', + latency: '24ms', + protocol: 'HTTPS / Web', + icon: Monitor, + iconColor: 'text-[#0047AB]', + iconBg: 'bg-blue-50 border-blue-200', + engine: 'React 19 / Vite Edge', + description: 'Client-facing mission control dashboard and operator cockpit.', + x: 400, + y: 56, + }, + 'api-gateway': { + id: 'api-gateway', + name: 'API Gateway', + tier: 2, + tierLabel: 'Tier 2', + tierName: 'Routing & Proxy', + recoveryStep: 4, + type: 'gateway', + port: ':8080', + latency: '4.5ms', + protocol: 'HTTP/2 & gRPC', + icon: Globe, + iconColor: 'text-indigo-600', + iconBg: 'bg-indigo-50 border-indigo-200', + engine: 'Envoy Reverse Proxy', + description: 'TLS termination, rate limiting, and intelligent load routing.', + x: 400, + y: 195, + }, + 'auth-service': { + id: 'auth-service', + name: 'Auth Service', + tier: 1, + tierLabel: 'Tier 1', + tierName: 'Microservice', + recoveryStep: 3, + type: 'application', + port: ':5001', + latency: '12ms', + protocol: 'gRPC / JWT', + icon: ShieldCheck, + iconColor: 'text-emerald-700', + iconBg: 'bg-emerald-50 border-emerald-200', + engine: 'EIP-712 / Go Auth Engine', + description: 'Session state validation, HMAC token signing, and Web3 key authorization.', + x: 260, + y: 340, + }, + 'payment-service': { + id: 'payment-service', + name: 'Payment Service', + tier: 1, + tierLabel: 'Tier 1', + tierName: 'Microservice', + recoveryStep: 3, + type: 'application', + port: ':5002', + latency: '18ms', + protocol: 'REST / Webhook', + icon: CreditCard, + iconColor: 'text-purple-700', + iconBg: 'bg-purple-50 border-purple-200', + engine: 'MST Escrow / Stripe Gateway', + description: 'NFT subscription license checking and fiat/crypto payment settlement.', + x: 540, + y: 340, + }, + 'redis-cache': { + id: 'redis-cache', + name: 'Redis Cache', + tier: 0, + tierLabel: 'Tier 0', + tierName: 'In-Memory Store', + recoveryStep: 2, + type: 'cache', + port: ':6379', + latency: '0.4ms', + protocol: 'RESP3 / TCP', + icon: Zap, + iconColor: 'text-amber-700', + iconBg: 'bg-amber-50 border-amber-200', + engine: 'Redis 7.2 Memory Cluster', + description: 'High-throughput session cache, rate-limit buckets, and pub/sub message bus.', + x: 160, + y: 505, + }, + 'db-primary': { + id: 'db-primary', + name: 'PostgreSQL Primary', + tier: 0, + tierLabel: 'Tier 0', + tierName: 'Primary Database', + recoveryStep: 1, + type: 'database', + port: ':5432', + latency: '1.2ms', + protocol: 'PostgreSQL Wire / TLS', + icon: Database, + iconColor: 'text-[#0047AB]', + iconBg: 'bg-blue-100 border-blue-300', + engine: 'PostgreSQL 16 Primary ACID', + description: 'Foundational system of record with continuous WAL replication archiving.', + x: 400, + y: 505, + }, + 'db-replica': { + id: 'db-replica', + name: 'PostgreSQL Replica', + tier: 0, + tierLabel: 'Tier 0', + tierName: 'Standby Database', + recoveryStep: 2, + type: 'database', + port: ':5433', + latency: '0.8ms', + protocol: 'WAL Streaming Sync', + icon: Database, + iconColor: 'text-sky-700', + iconBg: 'bg-sky-50 border-sky-200', + engine: 'PostgreSQL 16 Hot Standby', + description: 'Real-time asynchronous read replica ready for automated failover promotion.', + x: 640, + y: 505, + }, +}; + +interface GraphEdgeConfig { + id: string; + sourceId: string; + targetId: string; + label: string; + path: string; +} + +const STATIC_EDGES: GraphEdgeConfig[] = [ + { + id: 'edge-gateway-to-web', + sourceId: 'api-gateway', + targetId: 'web-frontend', + label: 'User Traffic Ingress', + path: 'M 400 156 L 400 95', + }, + { + id: 'edge-auth-to-gateway', + sourceId: 'auth-service', + targetId: 'api-gateway', + label: 'Route Auth & JWT', + path: 'M 260 301 C 260 255, 365 265, 365 234', + }, + { + id: 'edge-cache-to-gateway', + sourceId: 'redis-cache', + targetId: 'api-gateway', + label: 'Session & Rate Limits', + path: 'M 160 466 C 120 340, 290 280, 335 234', + }, + { + id: 'edge-cache-to-auth', + sourceId: 'redis-cache', + targetId: 'auth-service', + label: 'Token Caching', + path: 'M 175 466 C 175 420, 235 425, 235 379', + }, + { + id: 'edge-db-to-auth', + sourceId: 'db-primary', + targetId: 'auth-service', + label: 'User Credentials Query', + path: 'M 370 466 C 370 420, 285 425, 285 379', + }, + { + id: 'edge-auth-to-payment', + sourceId: 'auth-service', + targetId: 'payment-service', + label: 'Identity Attestation', + path: 'M 344 340 C 400 340, 400 340, 456 340', + }, + { + id: 'edge-db-to-payment', + sourceId: 'db-primary', + targetId: 'payment-service', + label: 'Ledger Balance Query', + path: 'M 430 466 C 430 420, 540 425, 540 379', + }, + { + id: 'edge-db-to-cache', + sourceId: 'db-primary', + targetId: 'redis-cache', + label: 'Cache Warmup & Sync', + path: 'M 316 505 C 280 505, 280 505, 244 505', + }, + { + id: 'edge-db-to-replica', + sourceId: 'db-primary', + targetId: 'db-replica', + label: 'Continuous WAL Stream', + path: 'M 484 505 C 520 505, 520 505, 556 505', + }, +]; + +export const TopologyGraph: React.FC = ({ className = '' }) => { const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState<{ dependent_id: string; depends_on_id: string }[]>([]); const [selectedNodeId, setSelectedNodeId] = useState('db-primary'); - const [loading, setLoading] = useState(true); - const [blastRadius, setBlastRadius] = useState([]); + const [hoveredNodeId, setHoveredNodeId] = useState(null); + const [hoveredEdgeId, setHoveredEdgeId] = useState(null); + const [loading, setLoading] = useState(true); + const [viewMode, setViewMode] = useState<'traffic' | 'recovery'>('traffic'); + const [tierFilter, setTierFilter] = useState<'all' | 't0' | 't1' | 't2-3'>('all'); - const loadTopology = async () => { + const loadTopology = useCallback(async () => { try { + setLoading(true); const [n, g] = await Promise.all([fetchNodes(), fetchGraph()]); setNodes(n); if (g.edges && g.edges.length > 0) { setEdges(g.edges); } else { - // Fallback default edges based on node dependencies const derivedEdges: { dependent_id: string; depends_on_id: string }[] = []; n.forEach((node: SystemNode) => { node.dependencies.forEach((dep: string) => { @@ -37,25 +291,43 @@ export const TopologyGraph: React.FC = () => { } finally { setLoading(false); } - }; + }, []); useEffect(() => { loadTopology(); - }, []); + const unsubscribe = clusterState.subscribe(() => { + loadTopology(); + }); + return () => unsubscribe(); + }, [loadTopology]); - // Compute blast radius: find all nodes that depend directly or indirectly on targetNode - useEffect(() => { - if (!selectedNodeId) { - setBlastRadius([]); - return; - } + // Selected node metadata + const selectedNode = useMemo(() => { + return nodes.find((n) => n.id === selectedNodeId) || null; + }, [nodes, selectedNodeId]); + + const selectedNodeMeta = useMemo(() => { + if (!selectedNodeId) return null; + return STATIC_NODE_METAS[selectedNodeId] || null; + }, [selectedNodeId]); + + // Compute upstream prerequisites for selected node (nodes it depends on) + const upstreamDependencies = useMemo(() => { + if (!selectedNodeId) return []; + const directPrereqs = edges + .filter((e) => e.dependent_id === selectedNodeId) + .map((e) => e.depends_on_id); + return Array.from(new Set(directPrereqs)); + }, [selectedNodeId, edges]); + // Compute blast radius for selected node (all nodes that depend on it directly or indirectly) + const blastRadius = useMemo(() => { + if (!selectedNodeId) return []; const affected = new Set(); const queue = [selectedNodeId]; while (queue.length > 0) { const current = queue.shift()!; - // Find all nodes that depend on current const dependents = edges .filter((e) => e.depends_on_id === current) .map((e) => e.dependent_id); @@ -67,114 +339,528 @@ export const TopologyGraph: React.FC = () => { } } } - - setBlastRadius(Array.from(affected)); + return Array.from(affected); }, [selectedNodeId, edges]); - const selectedNode = nodes.find((n) => n.id === selectedNodeId); + // Check if any node in cluster is down + const downNodes = useMemo(() => { + return nodes.filter((n) => n.status === 'down'); + }, [nodes]); + + const isSelectedDown = selectedNode?.status === 'down'; + // Toggle simulate failure or restore const handleToggleState = async (nodeId: string, currentStatus: string) => { const nextStatus = currentStatus === 'down' ? 'healthy' : 'down'; await triggerSimulation(nodeId, nextStatus); await loadTopology(); }; - // Group nodes into topological visual tiers - const tier1 = nodes.filter((n) => n.id === 'web-frontend'); - const tier2 = nodes.filter((n) => n.id === 'api-gateway'); - const tier3 = nodes.filter((n) => n.id === 'auth-service' || n.id === 'payment-service'); - const tier4 = nodes.filter((n) => n.id === 'redis-cache'); - const tier5 = nodes.filter((n) => n.id === 'db-primary' || n.id === 'db-replica'); + // One-click restore all nodes to healthy + const handleRestoreAll = async () => { + for (const node of nodes) { + if (node.status === 'down') { + await triggerSimulation(node.id, 'healthy'); + } + } + await loadTopology(); + }; + + // Helper to determine edge relationship with active focus + const getEdgeState = (edge: GraphEdgeConfig) => { + const focusId = hoveredNodeId || selectedNodeId; + if (!focusId) return 'normal'; + + const isDirectUpstream = edge.targetId === focusId; + const isDirectDownstream = edge.sourceId === focusId; + const isCascaded = + blastRadius.includes(edge.targetId) && + (edge.sourceId === selectedNodeId || blastRadius.includes(edge.sourceId)); + + if (isDirectUpstream) return 'upstream'; + if (isDirectDownstream || isCascaded) { + return isSelectedDown ? 'danger' : 'downstream'; + } + return 'dimmed'; + }; return ( -
+
{/* Main Interactive Visualizer */} - + {/* Subtle Network Dot Grid */} -
+
-
+ {/* Header Controls Bar */} +
-
+
-

- Autonomous Dependency DAG -

-

- Topological recovery flow: Foundational Data Layer → Gateway → Web Edge +

+

+ Autonomous Dependency DAG +

+ + O(V + E) + +
+

+ Dynamic multi-tier topology graph showing service dependencies, traffic paths, and cascade risk.

- +
+ {/* View Mode Switcher */} +
+ + +
+ + {/* Refresh DAG */} + +
- {/* Interactive Tiered Canvas */} -
- - {/* Tier 1: Client Edge */} -
- {tier1.map((n) => renderNode(n))} + {/* Sub-toolbar: Tier Filter & Outage Alert Banner */} +
+ {/* Filter pills */} +
+ + Filter: + + {[ + { id: 'all', label: 'All Tiers (7)' }, + { id: 't0', label: 'Tier 0 Data' }, + { id: 't1', label: 'Tier 1 Services' }, + { id: 't2-3', label: 'Tier 2-3 Edge' }, + ].map((filter) => ( + + ))}
- + {/* Outage status pill */} + {downNodes.length > 0 ? ( +
+ + + {downNodes.length} Outage Active: {downNodes.map((n) => n.name).join(', ')} + + +
+ ) : ( +
+ + All 7 Services Operational • Zero Deadlocks +
+ )} +
- {/* Tier 2: Gateway */} -
- {tier2.map((n) => renderNode(n))} -
+ {/* Interactive Topological DAG Canvas */} +
+
+ {/* Background Tier Swimlanes */} +
+ {/* Tier 3: Client Ingress */} +
+
+ + TIER 3 + + CLIENT INGRESS & WEB EDGE +
+ {viewMode === 'recovery' && ( + + Step 5: Client Restoration + + )} +
- + {/* Tier 2: API Gateway */} +
+
+ + TIER 2 + + API GATEWAY & ROUTING +
+ {viewMode === 'recovery' && ( + + Step 4: Gateway Routing + + )} +
- {/* Tier 3: Microservices */} -
- {tier3.map((n) => renderNode(n))} -
+ {/* Tier 1: Core Microservices */} +
+
+ + TIER 1 + + CORE APPLICATION SERVICES +
+ {viewMode === 'recovery' && ( + + Step 3: Business Microservices + + )} +
- + {/* Tier 0: Foundational Data & Storage */} +
+
+ + TIER 0 + + FOUNDATIONAL PERSISTENCE & CACHING (ROOT) +
+ {viewMode === 'recovery' && ( + + Steps 1-2: Root Data & Cache + + )} +
+
- {/* Tier 4 & 5: Cache & Database Foundational */} -
- {tier4.map((n) => renderNode(n))} - {tier5.map((n) => renderNode(n))} + {/* SVG Connectors Canvas */} + + + {/* Default arrow marker */} + + + + + {/* Upstream prerequisite arrow (Cobalt) */} + + + + + {/* Downstream dependent arrow (Amber) */} + + + + + {/* Outage cascading danger arrow (Red) */} + + + + + + {/* Render Directed Bézier Curves */} + {STATIC_EDGES.map((edge) => { + const state = getEdgeState(edge); + const isHovered = hoveredEdgeId === edge.id; + + let strokeColor = '#CBD5E1'; + let strokeWidth = 1.75; + let strokeDash = '5 4'; + let marker = 'url(#arrow-default)'; + let animClass = 'animate-dag-flow'; + let opacity = 1; + + if (state === 'upstream') { + strokeColor = '#0047AB'; + strokeWidth = 2.75; + strokeDash = '6 3'; + marker = 'url(#arrow-upstream)'; + animClass = 'animate-dag-flow-fast'; + } else if (state === 'downstream') { + strokeColor = '#D97706'; + strokeWidth = 2.5; + strokeDash = '6 3'; + marker = 'url(#arrow-downstream)'; + animClass = 'animate-dag-flow-fast'; + } else if (state === 'danger') { + strokeColor = '#EF4444'; + strokeWidth = 3; + strokeDash = '4 4'; + marker = 'url(#arrow-danger)'; + animClass = 'animate-dag-flow-fast'; + } else if (state === 'dimmed') { + opacity = 0.22; + animClass = ''; + } + + if (isHovered) { + strokeWidth = 3.5; + opacity = 1; + } + + return ( + + {/* Invisible wider hit area for hover */} + setHoveredEdgeId(edge.id)} + onMouseLeave={() => setHoveredEdgeId(null)} + /> + {/* Visible directed curve */} + + + ); + })} + + + {/* Edge Label Hover Tooltip Badge */} + {hoveredEdgeId && ( + (() => { + const hoveredEdge = STATIC_EDGES.find((e) => e.id === hoveredEdgeId); + if (!hoveredEdge) return null; + const source = STATIC_NODE_METAS[hoveredEdge.sourceId]; + const target = STATIC_NODE_METAS[hoveredEdge.targetId]; + return ( +
+ {source?.name} + → + {target?.name} + ({hoveredEdge.label}) +
+ ); + })() + )} + + {/* Interactive Node Cards */} + {Object.values(STATIC_NODE_METAS).map((meta) => { + const liveNode = nodes.find((n) => n.id === meta.id); + const status = liveNode?.status || 'healthy'; + const isDown = status === 'down'; + const isSelected = selectedNodeId === meta.id; + const isUpstream = upstreamDependencies.includes(meta.id); + const isDownstream = blastRadius.includes(meta.id); + const isSimulatedBlast = isDownstream && isSelectedDown; + + // Dim card if filter does not match + let isFilteredOut = false; + if (tierFilter === 't0' && meta.tier !== 0) isFilteredOut = true; + if (tierFilter === 't1' && meta.tier !== 1) isFilteredOut = true; + if (tierFilter === 't2-3' && meta.tier < 2) isFilteredOut = true; + + const IconComponent = meta.icon; + + return ( +
setSelectedNodeId(meta.id)} + onMouseEnter={() => setHoveredNodeId(meta.id)} + onMouseLeave={() => setHoveredNodeId(null)} + style={{ + left: `${meta.x}px`, + top: `${meta.y}px`, + }} + className={cn( + 'horizon-dag-node absolute -translate-x-1/2 -translate-y-1/2 w-[168px] rounded-2xl p-2.5 transition-all duration-200 cursor-pointer select-none', + 'shadow-[0_2px_8px_rgba(26,26,26,0.06),0_1px_2px_rgba(26,26,26,0.04)]', + isFilteredOut && 'opacity-25 pointer-events-none', + // Base Card Style + !isSelected && !isDown && !isSimulatedBlast && 'bg-white border-2 border-[rgba(26,26,26,0.12)] hover:border-[#0047AB]/50 hover:shadow-[0_4px_16px_rgba(0,71,171,0.15)] hover:-translate-y-1', + // Selected Style + isSelected && !isDown && 'bg-white border-2 border-[#0047AB] ring-4 ring-[#0047AB]/15 shadow-[0_6px_20px_rgba(0,71,171,0.22)] -translate-y-1 z-20', + // Upstream Prerequisite of Selected Node + isUpstream && !isSelected && 'border-2 border-[#0047AB]/60 bg-blue-50/40', + // Downstream Consumer of Selected Node (Normal Mode) + isDownstream && !isDown && !isSimulatedBlast && !isSelected && 'border-2 border-amber-300 bg-amber-50/30', + // Active Cascaded Risk (when selected target is down) + isSimulatedBlast && 'border-2 border-dashed border-amber-500 bg-amber-50/90 shadow-[0_4px_14px_rgba(217,119,6,0.2)] animate-pulse z-15', + // Target Node Is Down (Primary Failure) + isDown && 'border-2 border-red-500 bg-red-50/90 ring-4 ring-red-500/20 shadow-[0_6px_20px_rgba(239,68,68,0.25)] -translate-y-1 z-20' + )} + > + {/* Top Anchor Dot */} +
+ {/* Bottom Anchor Dot */} +
+ + {/* Mode Tag / Sequence Pill */} +
+ + {viewMode === 'recovery' ? `Step ${meta.recoveryStep}` : `T${meta.tier}`} + + + {/* Status Indicator Pill */} + {isDown ? ( + + + DOWN + + ) : isSimulatedBlast ? ( + + + Risk + + ) : isUpstream ? ( + + Prereq + + ) : ( + + + healthy + + )} +
+ + {/* Node Core Info */} +
+
+ +
+
+
+ {meta.name} +
+
+ {meta.port} • {meta.latency} +
+
+
+
+ ); + })}
{/* Legend Footer */} -
-
+
+
Healthy - Down + Outage (Down) - Blast Radius Affected + Cascade Risk + + + Selected Prerequisite
- Click any node to inspect blast radius + + Click any service to inspect blast radius & playbooks +
{/* Side Impact & Blast Radius Inspection Panel */} - +
+ {/* Panel Header */}
-

- Impact Analysis -

+
+

+ Topology Inspector +

+

+ Service architecture & blast radius +

+
{selectedNode && ( @@ -183,57 +869,129 @@ export const TopologyGraph: React.FC = () => { )}
- {selectedNode ? ( + {selectedNode && selectedNodeMeta ? (
+ {/* Target Service Card */}
- - Target Service - -
- {selectedNode.name} +
+ + Target Service + + + {selectedNodeMeta.tierLabel} • {selectedNodeMeta.tierName} + +
+ +
+
+ {React.createElement(selectedNodeMeta.icon, { + className: cn('w-5 h-5', selectedNodeMeta.iconColor), + })} +
+
+
+ {selectedNodeMeta.name} +
+
+ {selectedNodeMeta.engine} +
+
-
- - ID: {selectedNode.id} + +
+
+ PORT / PROTOCOL + + {selectedNodeMeta.port} ({selectedNodeMeta.protocol}) + +
+
+ TELEMETRY P99 + + {selectedNodeMeta.latency} Latency + +
+
+
+ + {/* Upstream Prerequisites */} +
+
+ + + Upstream Prerequisites - - TYPE: {selectedNode.type} + + {upstreamDependencies.length} Required
+ {upstreamDependencies.length === 0 ? ( +

+ Root foundation service. Has zero upstream dependencies. +

+ ) : ( +
+ {upstreamDependencies.map((depId) => { + const depMeta = STATIC_NODE_METAS[depId]; + const depNode = nodes.find((n) => n.id === depId); + return ( +
setSelectedNodeId(depId)} + className="flex items-center justify-between p-2 rounded-lg bg-[#FAF3EA] border border-[#E5D7C5] hover:border-[#0047AB]/40 cursor-pointer transition-colors text-xs" + > +
+ + + {depMeta?.name || depId} + +
+ + {depNode?.status || 'healthy'} + +
+ ); + })} +
+ )}
- {/* Blast Radius Count - Cobalt Blue Patch */} + {/* Blast Radius Box (Cobalt Patch) */}
- Calculated Blast Radius + + + Cascaded Blast Radius + - {blastRadius.length} Cascaded Services + {blastRadius.length} Downstream

- If {selectedNode.name} experiences downtime, the following downstream systems are impaired: + If {selectedNodeMeta.name} fails, the following downstream systems are impaired:

{blastRadius.length === 0 ? (
- Edge leaf node: 0 downstream dependencies impacted. + Edge leaf node: 0 downstream dependencies cascaded.
) : ( blastRadius.map((depId) => { - const depNode = nodes.find((n) => n.id === depId); + const depMeta = STATIC_NODE_METAS[depId]; return (
setSelectedNodeId(depId)} + className="flex items-center justify-between p-2 rounded-lg bg-black/25 border border-white/10 hover:border-white/30 text-xs cursor-pointer transition-colors" > - {depNode ? depNode.name : depId} + {depMeta ? depMeta.name : depId} - - Impaired + + {isSelectedDown ? 'Impaired' : 'Cascaded Risk'}
); @@ -242,77 +1000,54 @@ export const TopologyGraph: React.FC = () => {
- {/* Recovery Order Recommendation */} + {/* Autonomous Playbook Assignment */}
- - Autonomous Playbook Assignment: - - {selectedNode.type === 'database' - ? 'High-Risk Database Failover to replica. Human Commander approval gate required.' - : selectedNode.type === 'cache' - ? 'Automated Cache Invalidation and connection pool reconnection.' - : 'Zero-Downtime Rolling Service Pod Restart.'} +
+ + Autonomous Playbook Assignment: +
+

+ {selectedNode.type === 'database' + ? 'Playbook #PB-01: High-Risk Database Failover to read replica. EIP-712 Multi-Sig Commander approval required.' + : selectedNode.type === 'cache' + ? 'Playbook #PB-02: Automated Redis cache flush, container restart, and client pool reconnection.' + : selectedNode.type === 'gateway' + ? 'Playbook #PB-03: Zero-downtime Envoy ingress reload and dynamic DNS re-route to healthy pods.' + : 'Playbook #PB-04: Graceful SIGTERM pod termination, Kubernetes rolling restart, and readiness probe poll.'} +

) : ( -
- Select a service from the DAG topology to inspect its blast radius. +
+ Select any service from the DAG topology to inspect its dependencies and blast radius.
)}
+ {/* Action Controls Footer */} {selectedNode && ( -
+
)}
); - - function renderNode(node: SystemNode) { - const isSelected = selectedNodeId === node.id; - const isImpacted = blastRadius.includes(node.id); - const isDown = node.status === 'down'; - - return ( -
setSelectedNodeId(node.id)} - className={`p-3.5 rounded-xl border cursor-pointer transition-all duration-200 min-w-[150px] flex flex-col items-center gap-2 select-none ${ - isDown - ? 'bg-red-50 border-red-300 shadow-md shadow-red-500/10 scale-105' - : isSelected - ? 'bg-blue-50 border-[#0047AB] shadow-md shadow-[#0047AB]/20 scale-105 ring-2 ring-[#0047AB]/20' - : isImpacted - ? 'bg-amber-50/80 border-amber-300 shadow-xs' - : 'bg-white border-[#E5D7C5] shadow-xs hover:border-[#0047AB]/50 hover:shadow-md' - }`} - > - {node.name} - {node.status} - {isImpacted && ( - - • Impacted - - )} -
- ); - } - - function ConnectorArrow({ text }: { text: string }) { - return ( -
-
- {text} -
- ); - } }; diff --git a/apps/web/src/index.css b/apps/web/src/index.css index ca15ab1..268acbe 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -457,3 +457,37 @@ h1.bblacnkherotit.text-3xl.sm\:text-4xl.font-black.text-\[\#1A1A1A\].tracking-ti background-clip: text; filter: brightness(0.65) contrast(2.5); } + + +/* ===================================================================== + DAG SVG ANIMATED DEPENDENCY FLOW + ===================================================================== */ +@keyframes dagFlow { + from { + stroke-dashoffset: 24; + } + to { + stroke-dashoffset: 0; + } +} + +@keyframes dagFlowReverse { + from { + stroke-dashoffset: 0; + } + to { + stroke-dashoffset: 24; + } +} + +.animate-dag-flow { + animation: dagFlow 1.4s linear infinite; +} + +.animate-dag-flow-fast { + animation: dagFlow 0.7s linear infinite; +} + +.animate-dag-flow-reverse { + animation: dagFlowReverse 1.4s linear infinite; +} From 8c45277015066ddb1ff471771383151bba2bcce1 Mon Sep 17 00:00:00 2001 From: Ganeshbeldar015 Date: Sun, 1 Nov 2026 03:00:00 +0530 Subject: [PATCH 2/5] fix(topology): eliminate card hover translation jitter and edge rerender lag --- .../components/topology/TopologyGraph.test.ts | 1 + .../src/components/topology/TopologyGraph.tsx | 162 ++++++++---------- 2 files changed, 74 insertions(+), 89 deletions(-) diff --git a/apps/web/src/components/topology/TopologyGraph.test.ts b/apps/web/src/components/topology/TopologyGraph.test.ts index 780d1dc..d637ec5 100644 --- a/apps/web/src/components/topology/TopologyGraph.test.ts +++ b/apps/web/src/components/topology/TopologyGraph.test.ts @@ -66,3 +66,4 @@ describe('TopologyGraph Component & Autonomous DAG Suite', () => { expect(blast.severity).toBe('low'); }); }); + diff --git a/apps/web/src/components/topology/TopologyGraph.tsx b/apps/web/src/components/topology/TopologyGraph.tsx index 4470020..ade163a 100644 --- a/apps/web/src/components/topology/TopologyGraph.tsx +++ b/apps/web/src/components/topology/TopologyGraph.tsx @@ -267,7 +267,6 @@ export const TopologyGraph: React.FC = ({ className = '' }) const [edges, setEdges] = useState<{ dependent_id: string; depends_on_id: string }[]>([]); const [selectedNodeId, setSelectedNodeId] = useState('db-primary'); const [hoveredNodeId, setHoveredNodeId] = useState(null); - const [hoveredEdgeId, setHoveredEdgeId] = useState(null); const [loading, setLoading] = useState(true); const [viewMode, setViewMode] = useState<'traffic' | 'recovery'>('traffic'); const [tierFilter, setTierFilter] = useState<'all' | 't0' | 't1' | 't2-3'>('all'); @@ -637,7 +636,8 @@ export const TopologyGraph: React.FC = ({ className = '' }) {/* Render Directed Bézier Curves */} {STATIC_EDGES.map((edge) => { const state = getEdgeState(edge); - const isHovered = hoveredEdgeId === edge.id; + const source = STATIC_NODE_METAS[edge.sourceId]; + const target = STATIC_NODE_METAS[edge.targetId]; let strokeColor = '#CBD5E1'; let strokeWidth = 1.75; @@ -669,22 +669,15 @@ export const TopologyGraph: React.FC = ({ className = '' }) animClass = ''; } - if (isHovered) { - strokeWidth = 3.5; - opacity = 1; - } - return ( - + {/* Invisible wider hit area for hover */} setHoveredEdgeId(edge.id)} - onMouseLeave={() => setHoveredEdgeId(null)} + strokeWidth={14} + className="pointer-events-auto" /> {/* Visible directed curve */} = ({ className = '' }) strokeDasharray={strokeDash} markerEnd={viewMode === 'recovery' ? marker : undefined} markerStart={viewMode === 'traffic' ? marker : undefined} - className={cn('transition-all duration-200', animClass)} + className={cn('transition-all duration-150', animClass, 'group-hover:stroke-[#0047AB] group-hover:stroke-[3px]')} style={{ opacity }} /> + {`${source?.name} → ${target?.name} (${edge.label})`} ); })} - {/* Edge Label Hover Tooltip Badge */} - {hoveredEdgeId && ( - (() => { - const hoveredEdge = STATIC_EDGES.find((e) => e.id === hoveredEdgeId); - if (!hoveredEdge) return null; - const source = STATIC_NODE_METAS[hoveredEdge.sourceId]; - const target = STATIC_NODE_METAS[hoveredEdge.targetId]; - return ( -
- {source?.name} - → - {target?.name} - ({hoveredEdge.label}) -
- ); - })() - )} - {/* Interactive Node Cards */} {Object.values(STATIC_NODE_METAS).map((meta) => { const liveNode = nodes.find((n) => n.id === meta.id); @@ -742,76 +718,84 @@ export const TopologyGraph: React.FC = ({ className = '' }) return (
setSelectedNodeId(meta.id)} - onMouseEnter={() => setHoveredNodeId(meta.id)} - onMouseLeave={() => setHoveredNodeId(null)} style={{ left: `${meta.x}px`, top: `${meta.y}px`, + transform: 'translate(-50%, -50%)', }} className={cn( - 'horizon-dag-node absolute -translate-x-1/2 -translate-y-1/2 w-[168px] rounded-2xl p-2.5 transition-all duration-200 cursor-pointer select-none', - 'shadow-[0_2px_8px_rgba(26,26,26,0.06),0_1px_2px_rgba(26,26,26,0.04)]', - isFilteredOut && 'opacity-25 pointer-events-none', - // Base Card Style - !isSelected && !isDown && !isSimulatedBlast && 'bg-white border-2 border-[rgba(26,26,26,0.12)] hover:border-[#0047AB]/50 hover:shadow-[0_4px_16px_rgba(0,71,171,0.15)] hover:-translate-y-1', - // Selected Style - isSelected && !isDown && 'bg-white border-2 border-[#0047AB] ring-4 ring-[#0047AB]/15 shadow-[0_6px_20px_rgba(0,71,171,0.22)] -translate-y-1 z-20', - // Upstream Prerequisite of Selected Node - isUpstream && !isSelected && 'border-2 border-[#0047AB]/60 bg-blue-50/40', - // Downstream Consumer of Selected Node (Normal Mode) - isDownstream && !isDown && !isSimulatedBlast && !isSelected && 'border-2 border-amber-300 bg-amber-50/30', - // Active Cascaded Risk (when selected target is down) - isSimulatedBlast && 'border-2 border-dashed border-amber-500 bg-amber-50/90 shadow-[0_4px_14px_rgba(217,119,6,0.2)] animate-pulse z-15', - // Target Node Is Down (Primary Failure) - isDown && 'border-2 border-red-500 bg-red-50/90 ring-4 ring-red-500/20 shadow-[0_6px_20px_rgba(239,68,68,0.25)] -translate-y-1 z-20' + 'absolute pointer-events-auto select-none transition-opacity duration-150', + isSelected ? 'z-20' : 'z-10', + isFilteredOut && 'opacity-25 pointer-events-none' )} > - {/* Top Anchor Dot */} -
- {/* Bottom Anchor Dot */} -
- - {/* Mode Tag / Sequence Pill */} -
- - {viewMode === 'recovery' ? `Step ${meta.recoveryStep}` : `T${meta.tier}`} - +
setSelectedNodeId(meta.id)} + onMouseEnter={() => setHoveredNodeId(meta.id)} + onMouseLeave={() => setHoveredNodeId(null)} + className={cn( + 'horizon-dag-node w-[168px] rounded-2xl p-2.5 transition-all duration-150 cursor-pointer select-none', + 'shadow-[0_2px_8px_rgba(26,26,26,0.06),0_1px_2px_rgba(26,26,26,0.04)]', + // Base Card Style (Rock solid, zero translation movement!) + !isSelected && !isDown && !isSimulatedBlast && 'bg-white border-2 border-[rgba(26,26,26,0.12)] hover:border-[#0047AB]/70 hover:shadow-[0_4px_16px_rgba(0,71,171,0.18)] hover:ring-2 hover:ring-[#0047AB]/20', + // Selected Style + isSelected && !isDown && 'bg-white border-2 border-[#0047AB] ring-4 ring-[#0047AB]/20 shadow-[0_6px_20px_rgba(0,71,171,0.22)]', + // Upstream Prerequisite of Selected Node + isUpstream && !isSelected && 'border-2 border-[#0047AB]/60 bg-blue-50/40', + // Downstream Consumer of Selected Node (Normal Mode) + isDownstream && !isDown && !isSimulatedBlast && !isSelected && 'border-2 border-amber-300 bg-amber-50/30', + // Active Cascaded Risk (when selected target is down) + isSimulatedBlast && 'border-2 border-dashed border-amber-500 bg-amber-50/90 shadow-[0_4px_14px_rgba(217,119,6,0.2)]', + // Target Node Is Down (Primary Failure) + isDown && 'border-2 border-red-500 bg-red-50/90 ring-4 ring-red-500/20 shadow-[0_6px_20px_rgba(239,68,68,0.25)]' + )} + > + {/* Top Anchor Dot */} +
+ {/* Bottom Anchor Dot */} +
- {/* Status Indicator Pill */} - {isDown ? ( - - - DOWN - - ) : isSimulatedBlast ? ( - - - Risk - - ) : isUpstream ? ( - - Prereq + {/* Mode Tag / Sequence Pill */} +
+ + {viewMode === 'recovery' ? `Step ${meta.recoveryStep}` : `T${meta.tier}`} - ) : ( - - - healthy - - )} -
- {/* Node Core Info */} -
-
- + {/* Status Indicator Pill */} + {isDown ? ( + + + DOWN + + ) : isSimulatedBlast ? ( + + + Risk + + ) : isUpstream ? ( + + Prereq + + ) : ( + + + healthy + + )}
-
-
- {meta.name} + + {/* Node Core Info */} +
+
+
-
- {meta.port} • {meta.latency} +
+
+ {meta.name} +
+
+ {meta.port} • {meta.latency} +
From aa64798ce7aad145df206ee4ca73ee6136f593b3 Mon Sep 17 00:00:00 2001 From: Ganeshbeldar015 Date: Sun, 1 Nov 2026 03:28:43 +0530 Subject: [PATCH 3/5] feat(topology): add full-width canvas with left rail gutter, slide-out drawer, and AI tech typography --- .../src/components/topology/TopologyGraph.tsx | 1081 +++++++++-------- 1 file changed, 557 insertions(+), 524 deletions(-) diff --git a/apps/web/src/components/topology/TopologyGraph.tsx b/apps/web/src/components/topology/TopologyGraph.tsx index ade163a..dc3d45f 100644 --- a/apps/web/src/components/topology/TopologyGraph.tsx +++ b/apps/web/src/components/topology/TopologyGraph.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '../common/Card'; import { Badge } from '../common/Badge'; import { Button } from '../common/Button'; @@ -23,9 +24,9 @@ import { Activity, Flame, Radio, - Server, - Play, RotateCcw, + X, + SlidersHorizontal, } from 'lucide-react'; export interface TopologyGraphProps { @@ -35,9 +36,9 @@ export interface TopologyGraphProps { interface NodeLayoutMeta { id: string; name: string; - tier: number; - tierLabel: string; - tierName: string; + layer: number; + layerLabel: string; + layerName: string; recoveryStep: number; type: 'database' | 'application' | 'gateway' | 'cache'; port: string; @@ -56,9 +57,9 @@ const STATIC_NODE_METAS: Record = { 'web-frontend': { id: 'web-frontend', name: 'Horizon Web UI', - tier: 3, - tierLabel: 'Tier 3', - tierName: 'Client Ingress', + layer: 3, + layerLabel: 'L3', + layerName: 'Client Ingress', recoveryStep: 5, type: 'application', port: ':3000', @@ -69,15 +70,15 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-blue-50 border-blue-200', engine: 'React 19 / Vite Edge', description: 'Client-facing mission control dashboard and operator cockpit.', - x: 400, - y: 56, + x: 470, + y: 55, }, 'api-gateway': { id: 'api-gateway', name: 'API Gateway', - tier: 2, - tierLabel: 'Tier 2', - tierName: 'Routing & Proxy', + layer: 2, + layerLabel: 'L2', + layerName: 'Routing & Proxy', recoveryStep: 4, type: 'gateway', port: ':8080', @@ -88,15 +89,15 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-indigo-50 border-indigo-200', engine: 'Envoy Reverse Proxy', description: 'TLS termination, rate limiting, and intelligent load routing.', - x: 400, - y: 195, + x: 470, + y: 185, }, 'auth-service': { id: 'auth-service', name: 'Auth Service', - tier: 1, - tierLabel: 'Tier 1', - tierName: 'Microservice', + layer: 1, + layerLabel: 'L1', + layerName: 'Microservice', recoveryStep: 3, type: 'application', port: ':5001', @@ -107,15 +108,15 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-emerald-50 border-emerald-200', engine: 'EIP-712 / Go Auth Engine', description: 'Session state validation, HMAC token signing, and Web3 key authorization.', - x: 260, - y: 340, + x: 310, + y: 325, }, 'payment-service': { id: 'payment-service', name: 'Payment Service', - tier: 1, - tierLabel: 'Tier 1', - tierName: 'Microservice', + layer: 1, + layerLabel: 'L1', + layerName: 'Microservice', recoveryStep: 3, type: 'application', port: ':5002', @@ -126,15 +127,15 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-purple-50 border-purple-200', engine: 'MST Escrow / Stripe Gateway', description: 'NFT subscription license checking and fiat/crypto payment settlement.', - x: 540, - y: 340, + x: 630, + y: 325, }, 'redis-cache': { id: 'redis-cache', name: 'Redis Cache', - tier: 0, - tierLabel: 'Tier 0', - tierName: 'In-Memory Store', + layer: 0, + layerLabel: 'L0', + layerName: 'In-Memory Store', recoveryStep: 2, type: 'cache', port: ':6379', @@ -145,15 +146,15 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-amber-50 border-amber-200', engine: 'Redis 7.2 Memory Cluster', description: 'High-throughput session cache, rate-limit buckets, and pub/sub message bus.', - x: 160, - y: 505, + x: 210, + y: 480, }, 'db-primary': { id: 'db-primary', name: 'PostgreSQL Primary', - tier: 0, - tierLabel: 'Tier 0', - tierName: 'Primary Database', + layer: 0, + layerLabel: 'L0', + layerName: 'Primary Database', recoveryStep: 1, type: 'database', port: ':5432', @@ -164,15 +165,15 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-blue-100 border-blue-300', engine: 'PostgreSQL 16 Primary ACID', description: 'Foundational system of record with continuous WAL replication archiving.', - x: 400, - y: 505, + x: 470, + y: 480, }, 'db-replica': { id: 'db-replica', name: 'PostgreSQL Replica', - tier: 0, - tierLabel: 'Tier 0', - tierName: 'Standby Database', + layer: 0, + layerLabel: 'L0', + layerName: 'Standby Database', recoveryStep: 2, type: 'database', port: ':5433', @@ -183,8 +184,8 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-sky-50 border-sky-200', engine: 'PostgreSQL 16 Hot Standby', description: 'Real-time asynchronous read replica ready for automated failover promotion.', - x: 640, - y: 505, + x: 730, + y: 480, }, }; @@ -202,74 +203,75 @@ const STATIC_EDGES: GraphEdgeConfig[] = [ sourceId: 'api-gateway', targetId: 'web-frontend', label: 'User Traffic Ingress', - path: 'M 400 156 L 400 95', + path: 'M 470 148 L 470 92', }, { id: 'edge-auth-to-gateway', sourceId: 'auth-service', targetId: 'api-gateway', label: 'Route Auth & JWT', - path: 'M 260 301 C 260 255, 365 265, 365 234', + path: 'M 310 288 C 310 245, 430 255, 430 222', }, { id: 'edge-cache-to-gateway', sourceId: 'redis-cache', targetId: 'api-gateway', label: 'Session & Rate Limits', - path: 'M 160 466 C 120 340, 290 280, 335 234', + path: 'M 210 443 C 170 330, 360 270, 400 222', }, { id: 'edge-cache-to-auth', sourceId: 'redis-cache', targetId: 'auth-service', label: 'Token Caching', - path: 'M 175 466 C 175 420, 235 425, 235 379', + path: 'M 220 443 C 220 400, 280 405, 280 362', }, { id: 'edge-db-to-auth', sourceId: 'db-primary', targetId: 'auth-service', label: 'User Credentials Query', - path: 'M 370 466 C 370 420, 285 425, 285 379', + path: 'M 430 443 C 430 400, 340 405, 340 362', }, { id: 'edge-auth-to-payment', sourceId: 'auth-service', targetId: 'payment-service', label: 'Identity Attestation', - path: 'M 344 340 C 400 340, 400 340, 456 340', + path: 'M 400 325 C 470 325, 470 325, 540 325', }, { id: 'edge-db-to-payment', sourceId: 'db-primary', targetId: 'payment-service', label: 'Ledger Balance Query', - path: 'M 430 466 C 430 420, 540 425, 540 379', + path: 'M 510 443 C 510 400, 630 405, 630 362', }, { id: 'edge-db-to-cache', sourceId: 'db-primary', targetId: 'redis-cache', label: 'Cache Warmup & Sync', - path: 'M 316 505 C 280 505, 280 505, 244 505', + path: 'M 380 480 C 340 480, 340 480, 300 480', }, { id: 'edge-db-to-replica', sourceId: 'db-primary', targetId: 'db-replica', label: 'Continuous WAL Stream', - path: 'M 484 505 C 520 505, 520 505, 556 505', + path: 'M 560 480 C 600 480, 600 480, 640 480', }, ]; export const TopologyGraph: React.FC = ({ className = '' }) => { const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState<{ dependent_id: string; depends_on_id: string }[]>([]); - const [selectedNodeId, setSelectedNodeId] = useState('db-primary'); + const [selectedNodeId, setSelectedNodeId] = useState(null); const [hoveredNodeId, setHoveredNodeId] = useState(null); const [loading, setLoading] = useState(true); const [viewMode, setViewMode] = useState<'traffic' | 'recovery'>('traffic'); - const [tierFilter, setTierFilter] = useState<'all' | 't0' | 't1' | 't2-3'>('all'); + const [filter, setFilter] = useState<'all' | 'data' | 'services' | 'edge'>('all'); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); const loadTopology = useCallback(async () => { try { @@ -310,7 +312,7 @@ export const TopologyGraph: React.FC = ({ className = '' }) return STATIC_NODE_METAS[selectedNodeId] || null; }, [selectedNodeId]); - // Compute upstream prerequisites for selected node (nodes it depends on) + // Compute upstream prerequisites for selected node const upstreamDependencies = useMemo(() => { if (!selectedNodeId) return []; const directPrereqs = edges @@ -319,7 +321,7 @@ export const TopologyGraph: React.FC = ({ className = '' }) return Array.from(new Set(directPrereqs)); }, [selectedNodeId, edges]); - // Compute blast radius for selected node (all nodes that depend on it directly or indirectly) + // Compute blast radius for selected node const blastRadius = useMemo(() => { if (!selectedNodeId) return []; const affected = new Set(); @@ -348,6 +350,15 @@ export const TopologyGraph: React.FC = ({ className = '' }) const isSelectedDown = selectedNode?.status === 'down'; + const handleSelectNode = (nodeId: string) => { + setSelectedNodeId(nodeId); + setIsDrawerOpen(true); + }; + + const handleCloseDrawer = () => { + setIsDrawerOpen(false); + }; + // Toggle simulate failure or restore const handleToggleState = async (nodeId: string, currentStatus: string) => { const nextStatus = currentStatus === 'down' ? 'healthy' : 'down'; @@ -384,9 +395,9 @@ export const TopologyGraph: React.FC = ({ className = '' }) }; return ( -
- {/* Main Interactive Visualizer */} - +
+ {/* Full Width Visualizer Card */} + {/* Subtle Network Dot Grid */}
@@ -402,12 +413,12 @@ export const TopologyGraph: React.FC = ({ className = '' })

Autonomous Dependency DAG

- + O(V + E)

- Dynamic multi-tier topology graph showing service dependencies, traffic paths, and cascade risk. + Multi-tier service dependency topology with reverse blast radius resolution.

@@ -443,6 +454,18 @@ export const TopologyGraph: React.FC = ({ className = '' })
+ {/* Drawer Toggle (if node selected) */} + {selectedNodeId && !isDrawerOpen && ( + + )} + {/* Refresh DAG */}
- {/* Sub-toolbar: Tier Filter & Outage Alert Banner */} + {/* Sub-toolbar: Non-Repetitive Filter & Outage Alert Banner */}
- {/* Filter pills */} + {/* Filter pills without repeating "Tier" */}
- + Filter: {[ - { id: 'all', label: 'All Tiers (7)' }, - { id: 't0', label: 'Tier 0 Data' }, - { id: 't1', label: 'Tier 1 Services' }, - { id: 't2-3', label: 'Tier 2-3 Edge' }, - ].map((filter) => ( + { id: 'all', label: 'All (7)' }, + { id: 'data', label: 'L0 Data' }, + { id: 'services', label: 'L1 Services' }, + { id: 'edge', label: 'L2-3 Edge' }, + ].map((f) => ( ))}
@@ -489,548 +512,558 @@ export const TopologyGraph: React.FC = ({ className = '' }) {downNodes.length > 0 ? (
- - {downNodes.length} Outage Active: {downNodes.map((n) => n.name).join(', ')} + + {downNodes.length} OUTAGE ACTIVE: {downNodes.map((n) => n.name).join(', ')}
) : ( -
+
- All 7 Services Operational • Zero Deadlocks + ALL 7 NODES OPERATIONAL • 0 CYCLES
)}
- {/* Interactive Topological DAG Canvas */} -
-
- {/* Background Tier Swimlanes */} -
- {/* Tier 3: Client Ingress */} -
-
- - TIER 3 + {/* Interactive Topological DAG Canvas with Left Rail */} +
+
+ {/* Dedicated Left-Side Rail Gutter (No text behind cards!) */} +
+ {/* L3: Ingress */} +
+
+ + L3 - CLIENT INGRESS & WEB EDGE -
- {viewMode === 'recovery' && ( - - Step 5: Client Restoration + + Ingress - )} +
+ + Client Edge +
- {/* Tier 2: API Gateway */} -
-
- - TIER 2 + {/* L2: Gateway */} +
+
+ + L2 - API GATEWAY & ROUTING -
- {viewMode === 'recovery' && ( - - Step 4: Gateway Routing + + Gateway - )} +
+ + Route & TLS +
- {/* Tier 1: Core Microservices */} -
-
- - TIER 1 + {/* L1: Microservices */} +
+
+ + L1 - CORE APPLICATION SERVICES -
- {viewMode === 'recovery' && ( - - Step 3: Business Microservices + + Core - )} +
+ + Application +
- {/* Tier 0: Foundational Data & Storage */} -
-
- - TIER 0 + {/* L0: Storage */} +
+
+ + L0 - FOUNDATIONAL PERSISTENCE & CACHING (ROOT) -
- {viewMode === 'recovery' && ( - - Steps 1-2: Root Data & Cache + + Data - )} +
+ + Root ACID +
- {/* SVG Connectors Canvas */} - - - {/* Default arrow marker */} - - - + {/* Main Node Canvas Area */} +
+ {/* Subtle Horizontal Divider Lines (Zero text collisions!) */} +
+
+
+
+
+
- {/* Upstream prerequisite arrow (Cobalt) */} - - - + {/* SVG Connectors Canvas */} + + + + + - {/* Downstream dependent arrow (Amber) */} - - - + + + - {/* Outage cascading danger arrow (Red) */} - - - - + + + - {/* Render Directed Bézier Curves */} - {STATIC_EDGES.map((edge) => { - const state = getEdgeState(edge); - const source = STATIC_NODE_METAS[edge.sourceId]; - const target = STATIC_NODE_METAS[edge.targetId]; + + + + - let strokeColor = '#CBD5E1'; - let strokeWidth = 1.75; - let strokeDash = '5 4'; - let marker = 'url(#arrow-default)'; - let animClass = 'animate-dag-flow'; - let opacity = 1; + {/* Render Directed Bézier Curves */} + {STATIC_EDGES.map((edge) => { + const state = getEdgeState(edge); + const source = STATIC_NODE_METAS[edge.sourceId]; + const target = STATIC_NODE_METAS[edge.targetId]; - if (state === 'upstream') { - strokeColor = '#0047AB'; - strokeWidth = 2.75; - strokeDash = '6 3'; - marker = 'url(#arrow-upstream)'; - animClass = 'animate-dag-flow-fast'; - } else if (state === 'downstream') { - strokeColor = '#D97706'; - strokeWidth = 2.5; - strokeDash = '6 3'; - marker = 'url(#arrow-downstream)'; - animClass = 'animate-dag-flow-fast'; - } else if (state === 'danger') { - strokeColor = '#EF4444'; - strokeWidth = 3; - strokeDash = '4 4'; - marker = 'url(#arrow-danger)'; - animClass = 'animate-dag-flow-fast'; - } else if (state === 'dimmed') { - opacity = 0.22; - animClass = ''; - } + let strokeColor = '#CBD5E1'; + let strokeWidth = 1.75; + let strokeDash = '5 4'; + let marker = 'url(#arrow-default)'; + let animClass = 'animate-dag-flow'; + let opacity = 1; - return ( - - {/* Invisible wider hit area for hover */} - - {/* Visible directed curve */} - - {`${source?.name} → ${target?.name} (${edge.label})`} - - ); - })} - + if (state === 'upstream') { + strokeColor = '#0047AB'; + strokeWidth = 2.75; + strokeDash = '6 3'; + marker = 'url(#arrow-upstream)'; + animClass = 'animate-dag-flow-fast'; + } else if (state === 'downstream') { + strokeColor = '#D97706'; + strokeWidth = 2.5; + strokeDash = '6 3'; + marker = 'url(#arrow-downstream)'; + animClass = 'animate-dag-flow-fast'; + } else if (state === 'danger') { + strokeColor = '#EF4444'; + strokeWidth = 3; + strokeDash = '4 4'; + marker = 'url(#arrow-danger)'; + animClass = 'animate-dag-flow-fast'; + } else if (state === 'dimmed') { + opacity = 0.22; + animClass = ''; + } - {/* Interactive Node Cards */} - {Object.values(STATIC_NODE_METAS).map((meta) => { - const liveNode = nodes.find((n) => n.id === meta.id); - const status = liveNode?.status || 'healthy'; - const isDown = status === 'down'; - const isSelected = selectedNodeId === meta.id; - const isUpstream = upstreamDependencies.includes(meta.id); - const isDownstream = blastRadius.includes(meta.id); - const isSimulatedBlast = isDownstream && isSelectedDown; + return ( + + + + {`${source?.name} → ${target?.name} (${edge.label})`} + + ); + })} + - // Dim card if filter does not match - let isFilteredOut = false; - if (tierFilter === 't0' && meta.tier !== 0) isFilteredOut = true; - if (tierFilter === 't1' && meta.tier !== 1) isFilteredOut = true; - if (tierFilter === 't2-3' && meta.tier < 2) isFilteredOut = true; + {/* Interactive Node Cards */} + {Object.values(STATIC_NODE_METAS).map((meta) => { + const liveNode = nodes.find((n) => n.id === meta.id); + const status = liveNode?.status || 'healthy'; + const isDown = status === 'down'; + const isSelected = selectedNodeId === meta.id; + const isUpstream = upstreamDependencies.includes(meta.id); + const isDownstream = blastRadius.includes(meta.id); + const isSimulatedBlast = isDownstream && isSelectedDown; - const IconComponent = meta.icon; + // Dim card if filter does not match + let isFilteredOut = false; + if (filter === 'data' && meta.layer !== 0) isFilteredOut = true; + if (filter === 'services' && meta.layer !== 1) isFilteredOut = true; + if (filter === 'edge' && meta.layer < 2) isFilteredOut = true; - return ( -
+ const IconComponent = meta.icon; + // Compensate for left gutter offset (100px) + const adjustedX = meta.x - 100; + + return (
setSelectedNodeId(meta.id)} - onMouseEnter={() => setHoveredNodeId(meta.id)} - onMouseLeave={() => setHoveredNodeId(null)} + key={meta.id} + style={{ + left: `${adjustedX}px`, + top: `${meta.y}px`, + transform: 'translate(-50%, -50%)', + }} className={cn( - 'horizon-dag-node w-[168px] rounded-2xl p-2.5 transition-all duration-150 cursor-pointer select-none', - 'shadow-[0_2px_8px_rgba(26,26,26,0.06),0_1px_2px_rgba(26,26,26,0.04)]', - // Base Card Style (Rock solid, zero translation movement!) - !isSelected && !isDown && !isSimulatedBlast && 'bg-white border-2 border-[rgba(26,26,26,0.12)] hover:border-[#0047AB]/70 hover:shadow-[0_4px_16px_rgba(0,71,171,0.18)] hover:ring-2 hover:ring-[#0047AB]/20', - // Selected Style - isSelected && !isDown && 'bg-white border-2 border-[#0047AB] ring-4 ring-[#0047AB]/20 shadow-[0_6px_20px_rgba(0,71,171,0.22)]', - // Upstream Prerequisite of Selected Node - isUpstream && !isSelected && 'border-2 border-[#0047AB]/60 bg-blue-50/40', - // Downstream Consumer of Selected Node (Normal Mode) - isDownstream && !isDown && !isSimulatedBlast && !isSelected && 'border-2 border-amber-300 bg-amber-50/30', - // Active Cascaded Risk (when selected target is down) - isSimulatedBlast && 'border-2 border-dashed border-amber-500 bg-amber-50/90 shadow-[0_4px_14px_rgba(217,119,6,0.2)]', - // Target Node Is Down (Primary Failure) - isDown && 'border-2 border-red-500 bg-red-50/90 ring-4 ring-red-500/20 shadow-[0_6px_20px_rgba(239,68,68,0.25)]' + 'absolute pointer-events-auto select-none transition-opacity duration-150', + isSelected ? 'z-20' : 'z-10', + isFilteredOut && 'opacity-25 pointer-events-none' )} > - {/* Top Anchor Dot */} -
- {/* Bottom Anchor Dot */} -
- - {/* Mode Tag / Sequence Pill */} -
- - {viewMode === 'recovery' ? `Step ${meta.recoveryStep}` : `T${meta.tier}`} - +
handleSelectNode(meta.id)} + onMouseEnter={() => setHoveredNodeId(meta.id)} + onMouseLeave={() => setHoveredNodeId(null)} + className={cn( + 'horizon-dag-node w-[184px] rounded-2xl p-2.5 transition-all duration-150 cursor-pointer select-none', + 'shadow-[0_2px_8px_rgba(26,26,26,0.06),0_1px_2px_rgba(26,26,26,0.04)]', + // Base Card Style (No movement on hover!) + !isSelected && !isDown && !isSimulatedBlast && 'bg-white border-2 border-[rgba(26,26,26,0.12)] hover:border-[#0047AB]/70 hover:shadow-[0_4px_16px_rgba(0,71,171,0.18)] hover:ring-2 hover:ring-[#0047AB]/20', + // Selected Style + isSelected && !isDown && 'bg-white border-2 border-[#0047AB] ring-4 ring-[#0047AB]/20 shadow-[0_6px_20px_rgba(0,71,171,0.22)]', + // Upstream Prerequisite of Selected Node + isUpstream && !isSelected && 'border-2 border-[#0047AB]/60 bg-blue-50/40', + // Active Cascaded Risk (when selected target is down) + isSimulatedBlast && 'border-2 border-dashed border-amber-500 bg-amber-50/90 shadow-[0_4px_14px_rgba(217,119,6,0.2)]', + // Target Node Is Down (Primary Failure) + isDown && 'border-2 border-red-500 bg-red-50/90 ring-4 ring-red-500/20 shadow-[0_6px_20px_rgba(239,68,68,0.25)]' + )} + > + {/* Top Anchor Dot */} +
+ {/* Bottom Anchor Dot */} +
- {/* Status Indicator Pill */} - {isDown ? ( - - - DOWN + {/* Mode Tag / Sequence Pill */} +
+ + {viewMode === 'recovery' ? `STEP ${meta.recoveryStep}` : meta.layerLabel} - ) : isSimulatedBlast ? ( - - - Risk - - ) : isUpstream ? ( - - Prereq - - ) : ( - - - healthy - - )} -
- {/* Node Core Info */} -
-
- + {/* Status Indicator Pill */} + {isDown ? ( + + + DOWN + + ) : isSimulatedBlast ? ( + + + RISK + + ) : isUpstream ? ( + + PREREQ + + ) : ( + + + HEALTHY + + )}
-
-
- {meta.name} + + {/* Node Core Info */} +
+
+
-
- {meta.port} • {meta.latency} +
+
+ {meta.name} +
+
+ {meta.port} • {meta.latency} +
-
- ); - })} + ); + })} +
{/* Legend Footer */}
-
- +
+ Healthy - - Outage (Down) + + Outage - + Cascade Risk - - Selected Prerequisite + + Prerequisite
- - Click any service to inspect blast radius & playbooks + + Click any service to slide out the impact inspector
- - {/* Side Impact & Blast Radius Inspection Panel */} - -
- {/* Panel Header */} -
-
-
- -
+ {/* Slide-out Topology Inspector Drawer */} + + {isDrawerOpen && selectedNode && selectedNodeMeta && ( +
-

- Topology Inspector -

-

- Service architecture & blast radius -

-
-
- {selectedNode && ( - - {selectedNode.status} - - )} -
- - {selectedNode && selectedNodeMeta ? ( -
- {/* Target Service Card */} -
-
- - Target Service - - - {selectedNodeMeta.tierLabel} • {selectedNodeMeta.tierName} - -
- -
-
- {React.createElement(selectedNodeMeta.icon, { - className: cn('w-5 h-5', selectedNodeMeta.iconColor), - })} -
-
-
- {selectedNodeMeta.name} + {/* Drawer Header */} +
+
+
+
-
- {selectedNodeMeta.engine} +
+

+ Service Inspector +

+

+ Topological Impact Analysis +

+
-
-
- PORT / PROTOCOL - - {selectedNodeMeta.port} ({selectedNodeMeta.protocol}) - -
-
- TELEMETRY P99 - - {selectedNodeMeta.latency} Latency - -
-
-
+ {/* Target Service Card */} +
+
+
+ + Target Service + + + {selectedNodeMeta.layerLabel} • {selectedNodeMeta.layerName} + +
- {/* Upstream Prerequisites */} -
-
- - - Upstream Prerequisites - - - {upstreamDependencies.length} Required - -
- {upstreamDependencies.length === 0 ? ( -

- Root foundation service. Has zero upstream dependencies. -

- ) : ( -
- {upstreamDependencies.map((depId) => { - const depMeta = STATIC_NODE_METAS[depId]; - const depNode = nodes.find((n) => n.id === depId); - return ( -
setSelectedNodeId(depId)} - className="flex items-center justify-between p-2 rounded-lg bg-[#FAF3EA] border border-[#E5D7C5] hover:border-[#0047AB]/40 cursor-pointer transition-colors text-xs" - > -
- - - {depMeta?.name || depId} - -
- - {depNode?.status || 'healthy'} - +
+
+ {React.createElement(selectedNodeMeta.icon, { + className: cn('w-5 h-5', selectedNodeMeta.iconColor), + })} +
+
+
+ {selectedNodeMeta.name}
- ); - })} -
- )} -
+
+ {selectedNodeMeta.engine} +
+
+
- {/* Blast Radius Box (Cobalt Patch) */} -
-
- - - Cascaded Blast Radius - - - {blastRadius.length} Downstream - -
+
+
+ PORT / PROTOCOL + + {selectedNodeMeta.port} ({selectedNodeMeta.protocol}) + +
+
+ TELEMETRY P99 + + {selectedNodeMeta.latency} + +
+
+
-

- If {selectedNodeMeta.name} fails, the following downstream systems are impaired: -

+ {/* Upstream Prerequisites */} +
+
+ + + Upstream Prerequisites + + + {upstreamDependencies.length} REQUIRED + +
+ {upstreamDependencies.length === 0 ? ( +

+ Root foundation service. Has zero upstream dependencies. +

+ ) : ( +
+ {upstreamDependencies.map((depId) => { + const depMeta = STATIC_NODE_METAS[depId]; + const depNode = nodes.find((n) => n.id === depId); + return ( +
setSelectedNodeId(depId)} + className="flex items-center justify-between p-2 rounded-lg bg-[#FAF3EA] border border-[#E5D7C5] hover:border-[#0047AB]/40 cursor-pointer transition-colors text-xs" + > +
+ + + {depMeta?.name || depId} + +
+ + {depNode?.status || 'healthy'} + +
+ ); + })} +
+ )} +
-
- {blastRadius.length === 0 ? ( -
- - Edge leaf node: 0 downstream dependencies cascaded. + {/* Blast Radius Box (Cobalt Patch) */} +
+
+ + + Cascaded Blast Radius + + + {blastRadius.length} DOWNSTREAM +
- ) : ( - blastRadius.map((depId) => { - const depMeta = STATIC_NODE_METAS[depId]; - return ( -
setSelectedNodeId(depId)} - className="flex items-center justify-between p-2 rounded-lg bg-black/25 border border-white/10 hover:border-white/30 text-xs cursor-pointer transition-colors" - > - - {depMeta ? depMeta.name : depId} - - - {isSelectedDown ? 'Impaired' : 'Cascaded Risk'} - + +

+ If {selectedNodeMeta.name} fails, the following downstream systems are impaired: +

+ +
+ {blastRadius.length === 0 ? ( +
+ + Edge leaf node: 0 downstream dependencies cascaded.
- ); - }) - )} + ) : ( + blastRadius.map((depId) => { + const depMeta = STATIC_NODE_METAS[depId]; + return ( +
setSelectedNodeId(depId)} + className="flex items-center justify-between p-2 rounded-lg bg-black/25 border border-white/10 hover:border-white/30 text-xs cursor-pointer transition-colors" + > + + {depMeta ? depMeta.name : depId} + + + {isSelectedDown ? 'IMPAIRED' : 'AT RISK'} + +
+ ); + }) + )} +
+
+ + {/* Autonomous Playbook Assignment */} +
+
+ + Autonomous Playbook Assignment: +
+

+ {selectedNode.type === 'database' + ? 'Playbook #PB-01: High-Risk Database Failover to read replica. EIP-712 Multi-Sig Commander approval required.' + : selectedNode.type === 'cache' + ? 'Playbook #PB-02: Automated Redis cache flush, container restart, and client pool reconnection.' + : selectedNode.type === 'gateway' + ? 'Playbook #PB-03: Zero-downtime Envoy ingress reload and dynamic DNS re-route to healthy pods.' + : 'Playbook #PB-04: Graceful SIGTERM pod termination, Kubernetes rolling restart, and readiness probe poll.'} +

+
- {/* Autonomous Playbook Assignment */} -
-
- - Autonomous Playbook Assignment: -
-

- {selectedNode.type === 'database' - ? 'Playbook #PB-01: High-Risk Database Failover to read replica. EIP-712 Multi-Sig Commander approval required.' - : selectedNode.type === 'cache' - ? 'Playbook #PB-02: Automated Redis cache flush, container restart, and client pool reconnection.' - : selectedNode.type === 'gateway' - ? 'Playbook #PB-03: Zero-downtime Envoy ingress reload and dynamic DNS re-route to healthy pods.' - : 'Playbook #PB-04: Graceful SIGTERM pod termination, Kubernetes rolling restart, and readiness probe poll.'} -

+ {/* Action Controls Footer */} +
+
-
- ) : ( -
- Select any service from the DAG topology to inspect its dependencies and blast radius. -
+ )} -
- - {/* Action Controls Footer */} - {selectedNode && ( -
- -
- )} +
); From 400c7fd656fa647e97bdb292cb89a89999e9fe5b Mon Sep 17 00:00:00 2001 From: Ganeshbeldar015 Date: Sun, 1 Nov 2026 03:41:12 +0530 Subject: [PATCH 4/5] feat(topology): show TIER in left rail badges and streamline DAG layout --- .../src/components/topology/TopologyGraph.tsx | 599 +++++++++--------- 1 file changed, 284 insertions(+), 315 deletions(-) diff --git a/apps/web/src/components/topology/TopologyGraph.tsx b/apps/web/src/components/topology/TopologyGraph.tsx index dc3d45f..4a9930b 100644 --- a/apps/web/src/components/topology/TopologyGraph.tsx +++ b/apps/web/src/components/topology/TopologyGraph.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; import { Card } from '../common/Card'; import { Badge } from '../common/Badge'; import { Button } from '../common/Button'; @@ -25,8 +24,6 @@ import { Flame, Radio, RotateCcw, - X, - SlidersHorizontal, } from 'lucide-react'; export interface TopologyGraphProps { @@ -36,9 +33,9 @@ export interface TopologyGraphProps { interface NodeLayoutMeta { id: string; name: string; - layer: number; - layerLabel: string; - layerName: string; + tier: number; + tierLabel: string; + tierName: string; recoveryStep: number; type: 'database' | 'application' | 'gateway' | 'cache'; port: string; @@ -57,9 +54,9 @@ const STATIC_NODE_METAS: Record = { 'web-frontend': { id: 'web-frontend', name: 'Horizon Web UI', - layer: 3, - layerLabel: 'L3', - layerName: 'Client Ingress', + tier: 3, + tierLabel: 'TIER 3', + tierName: 'Client Ingress', recoveryStep: 5, type: 'application', port: ':3000', @@ -70,15 +67,15 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-blue-50 border-blue-200', engine: 'React 19 / Vite Edge', description: 'Client-facing mission control dashboard and operator cockpit.', - x: 470, - y: 55, + x: 300, + y: 50, }, 'api-gateway': { id: 'api-gateway', name: 'API Gateway', - layer: 2, - layerLabel: 'L2', - layerName: 'Routing & Proxy', + tier: 2, + tierLabel: 'TIER 2', + tierName: 'Routing & Proxy', recoveryStep: 4, type: 'gateway', port: ':8080', @@ -89,15 +86,15 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-indigo-50 border-indigo-200', engine: 'Envoy Reverse Proxy', description: 'TLS termination, rate limiting, and intelligent load routing.', - x: 470, - y: 185, + x: 300, + y: 165, }, 'auth-service': { id: 'auth-service', name: 'Auth Service', - layer: 1, - layerLabel: 'L1', - layerName: 'Microservice', + tier: 1, + tierLabel: 'TIER 1', + tierName: 'Microservice', recoveryStep: 3, type: 'application', port: ':5001', @@ -108,15 +105,15 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-emerald-50 border-emerald-200', engine: 'EIP-712 / Go Auth Engine', description: 'Session state validation, HMAC token signing, and Web3 key authorization.', - x: 310, - y: 325, + x: 180, + y: 295, }, 'payment-service': { id: 'payment-service', name: 'Payment Service', - layer: 1, - layerLabel: 'L1', - layerName: 'Microservice', + tier: 1, + tierLabel: 'TIER 1', + tierName: 'Microservice', recoveryStep: 3, type: 'application', port: ':5002', @@ -127,15 +124,15 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-purple-50 border-purple-200', engine: 'MST Escrow / Stripe Gateway', description: 'NFT subscription license checking and fiat/crypto payment settlement.', - x: 630, - y: 325, + x: 420, + y: 295, }, 'redis-cache': { id: 'redis-cache', name: 'Redis Cache', - layer: 0, - layerLabel: 'L0', - layerName: 'In-Memory Store', + tier: 0, + tierLabel: 'TIER 0', + tierName: 'In-Memory Store', recoveryStep: 2, type: 'cache', port: ':6379', @@ -146,15 +143,15 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-amber-50 border-amber-200', engine: 'Redis 7.2 Memory Cluster', description: 'High-throughput session cache, rate-limit buckets, and pub/sub message bus.', - x: 210, - y: 480, + x: 105, + y: 435, }, 'db-primary': { id: 'db-primary', name: 'PostgreSQL Primary', - layer: 0, - layerLabel: 'L0', - layerName: 'Primary Database', + tier: 0, + tierLabel: 'TIER 0', + tierName: 'Primary Database', recoveryStep: 1, type: 'database', port: ':5432', @@ -165,15 +162,15 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-blue-100 border-blue-300', engine: 'PostgreSQL 16 Primary ACID', description: 'Foundational system of record with continuous WAL replication archiving.', - x: 470, - y: 480, + x: 300, + y: 435, }, 'db-replica': { id: 'db-replica', name: 'PostgreSQL Replica', - layer: 0, - layerLabel: 'L0', - layerName: 'Standby Database', + tier: 0, + tierLabel: 'TIER 0', + tierName: 'Standby Database', recoveryStep: 2, type: 'database', port: ':5433', @@ -184,8 +181,8 @@ const STATIC_NODE_METAS: Record = { iconBg: 'bg-sky-50 border-sky-200', engine: 'PostgreSQL 16 Hot Standby', description: 'Real-time asynchronous read replica ready for automated failover promotion.', - x: 730, - y: 480, + x: 495, + y: 435, }, }; @@ -203,75 +200,74 @@ const STATIC_EDGES: GraphEdgeConfig[] = [ sourceId: 'api-gateway', targetId: 'web-frontend', label: 'User Traffic Ingress', - path: 'M 470 148 L 470 92', + path: 'M 300 129 L 300 86', }, { id: 'edge-auth-to-gateway', sourceId: 'auth-service', targetId: 'api-gateway', label: 'Route Auth & JWT', - path: 'M 310 288 C 310 245, 430 255, 430 222', + path: 'M 180 259 C 180 220, 270 230, 270 201', }, { id: 'edge-cache-to-gateway', sourceId: 'redis-cache', targetId: 'api-gateway', label: 'Session & Rate Limits', - path: 'M 210 443 C 170 330, 360 270, 400 222', + path: 'M 105 399 C 75 300, 220 250, 245 201', }, { id: 'edge-cache-to-auth', sourceId: 'redis-cache', targetId: 'auth-service', label: 'Token Caching', - path: 'M 220 443 C 220 400, 280 405, 280 362', + path: 'M 120 399 C 120 360, 155 365, 155 331', }, { id: 'edge-db-to-auth', sourceId: 'db-primary', targetId: 'auth-service', label: 'User Credentials Query', - path: 'M 430 443 C 430 400, 340 405, 340 362', + path: 'M 275 399 C 275 360, 205 365, 205 331', }, { id: 'edge-auth-to-payment', sourceId: 'auth-service', targetId: 'payment-service', label: 'Identity Attestation', - path: 'M 400 325 C 470 325, 470 325, 540 325', + path: 'M 260 295 C 300 295, 300 295, 340 295', }, { id: 'edge-db-to-payment', sourceId: 'db-primary', targetId: 'payment-service', label: 'Ledger Balance Query', - path: 'M 510 443 C 510 400, 630 405, 630 362', + path: 'M 325 399 C 325 360, 420 365, 420 331', }, { id: 'edge-db-to-cache', sourceId: 'db-primary', targetId: 'redis-cache', label: 'Cache Warmup & Sync', - path: 'M 380 480 C 340 480, 340 480, 300 480', + path: 'M 220 435 C 200 435, 200 435, 185 435', }, { id: 'edge-db-to-replica', sourceId: 'db-primary', targetId: 'db-replica', label: 'Continuous WAL Stream', - path: 'M 560 480 C 600 480, 600 480, 640 480', + path: 'M 380 435 C 400 435, 400 435, 415 435', }, ]; export const TopologyGraph: React.FC = ({ className = '' }) => { const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState<{ dependent_id: string; depends_on_id: string }[]>([]); - const [selectedNodeId, setSelectedNodeId] = useState(null); + const [selectedNodeId, setSelectedNodeId] = useState('db-primary'); const [hoveredNodeId, setHoveredNodeId] = useState(null); const [loading, setLoading] = useState(true); const [viewMode, setViewMode] = useState<'traffic' | 'recovery'>('traffic'); const [filter, setFilter] = useState<'all' | 'data' | 'services' | 'edge'>('all'); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); const loadTopology = useCallback(async () => { try { @@ -350,15 +346,6 @@ export const TopologyGraph: React.FC = ({ className = '' }) const isSelectedDown = selectedNode?.status === 'down'; - const handleSelectNode = (nodeId: string) => { - setSelectedNodeId(nodeId); - setIsDrawerOpen(true); - }; - - const handleCloseDrawer = () => { - setIsDrawerOpen(false); - }; - // Toggle simulate failure or restore const handleToggleState = async (nodeId: string, currentStatus: string) => { const nextStatus = currentStatus === 'down' ? 'healthy' : 'down'; @@ -395,9 +382,9 @@ export const TopologyGraph: React.FC = ({ className = '' }) }; return ( -
- {/* Full Width Visualizer Card */} - +
+ {/* Main Interactive Visualizer (Left Column, 8-span) */} + {/* Subtle Network Dot Grid */}
@@ -450,22 +437,10 @@ export const TopologyGraph: React.FC = ({ className = '' }) )} > - Kahn Recovery Order + Kahn Order
- {/* Drawer Toggle (if node selected) */} - {selectedNodeId && !isDrawerOpen && ( - - )} - {/* Refresh DAG */}
- {/* Interactive Topological DAG Canvas with Left Rail */} -
-
+ {/* Interactive Topological DAG Canvas with Dedicated Left Rail */} +
+
{/* Dedicated Left-Side Rail Gutter (No text behind cards!) */} -
- {/* L3: Ingress */} -
+
+ {/* TIER 3: Ingress */} +
- - L3 + + TIER 3 - Ingress + INGRESS
- + Client Edge
- {/* L2: Gateway */} -
+ {/* TIER 2: Gateway */} +
- - L2 + + TIER 2 - Gateway + GATEWAY
- + Route & TLS
- {/* L1: Microservices */} -
+ {/* TIER 1: Core */} +
- - L1 + + TIER 1 - Core + CORE
- + Application
- {/* L0: Storage */} -
+ {/* TIER 0: Data */} +
- - L0 + + TIER 0 - Data + DATA
- + Root ACID
- {/* Main Node Canvas Area */} -
+ {/* Main Node Canvas Area (600px width) */} +
{/* Subtle Horizontal Divider Lines (Zero text collisions!) */}
-
+
+
-
-
+
{/* SVG Connectors Canvas */} = ({ className = '' }) // Dim card if filter does not match let isFilteredOut = false; - if (filter === 'data' && meta.layer !== 0) isFilteredOut = true; - if (filter === 'services' && meta.layer !== 1) isFilteredOut = true; - if (filter === 'edge' && meta.layer < 2) isFilteredOut = true; + if (filter === 'data' && meta.tier !== 0) isFilteredOut = true; + if (filter === 'services' && meta.tier !== 1) isFilteredOut = true; + if (filter === 'edge' && meta.tier < 2) isFilteredOut = true; const IconComponent = meta.icon; - // Compensate for left gutter offset (100px) - const adjustedX = meta.x - 100; return (
= ({ className = '' }) )} >
handleSelectNode(meta.id)} + onClick={() => setSelectedNodeId(meta.id)} onMouseEnter={() => setHoveredNodeId(meta.id)} onMouseLeave={() => setHoveredNodeId(null)} className={cn( - 'horizon-dag-node w-[184px] rounded-2xl p-2.5 transition-all duration-150 cursor-pointer select-none', + 'horizon-dag-node w-[160px] rounded-2xl p-2.5 transition-all duration-150 cursor-pointer select-none', 'shadow-[0_2px_8px_rgba(26,26,26,0.06),0_1px_2px_rgba(26,26,26,0.04)]', // Base Card Style (No movement on hover!) !isSelected && !isDown && !isSimulatedBlast && 'bg-white border-2 border-[rgba(26,26,26,0.12)] hover:border-[#0047AB]/70 hover:shadow-[0_4px_16px_rgba(0,71,171,0.18)] hover:ring-2 hover:ring-[#0047AB]/20', @@ -786,7 +758,7 @@ export const TopologyGraph: React.FC = ({ className = '' }) {/* Mode Tag / Sequence Pill */}
- {viewMode === 'recovery' ? `STEP ${meta.recoveryStep}` : meta.layerLabel} + {viewMode === 'recovery' ? `STEP ${meta.recoveryStep}` : meta.tierLabel} {/* Status Indicator Pill */} @@ -852,218 +824,215 @@ export const TopologyGraph: React.FC = ({ className = '' })
- Click any service to slide out the impact inspector + Click any service to inspect topology & blast radius
+ - {/* Slide-out Topology Inspector Drawer */} - - {isDrawerOpen && selectedNode && selectedNodeMeta && ( - + {/* Persistent Side-by-Side Service Inspector Column (Right Column, 4-span) */} + +
+ {/* Panel Header */} +
+
+
+ +
- {/* Drawer Header */} -
-
-
- -
-
-

- Service Inspector -

-

- Topological Impact Analysis -

-
-
- -
+

+ Service Inspector +

+

+ Topological Impact Analysis +

+
+
+ {selectedNode && ( + + {selectedNode.status} + + )} +
- {/* Target Service Card */} -
-
-
- - Target Service - - - {selectedNodeMeta.layerLabel} • {selectedNodeMeta.layerName} - -
+ {/* Scrollable Inspector Body with Constant Stable Container Height */} + {selectedNode && selectedNodeMeta ? ( +
+ {/* Target Service Overview Card */} +
+
+ + Target Service + + + {selectedNodeMeta.tierLabel} • {selectedNodeMeta.tierName} + +
-
-
- {React.createElement(selectedNodeMeta.icon, { - className: cn('w-5 h-5', selectedNodeMeta.iconColor), - })} -
-
-
- {selectedNodeMeta.name} -
-
- {selectedNodeMeta.engine} -
-
+
+
+ {React.createElement(selectedNodeMeta.icon, { + className: cn('w-5 h-5', selectedNodeMeta.iconColor), + })} +
+
+
+ {selectedNodeMeta.name}
- -
-
- PORT / PROTOCOL - - {selectedNodeMeta.port} ({selectedNodeMeta.protocol}) - -
-
- TELEMETRY P99 - - {selectedNodeMeta.latency} - -
+
+ {selectedNodeMeta.engine}
+
- {/* Upstream Prerequisites */} -
-
- - - Upstream Prerequisites - - - {upstreamDependencies.length} REQUIRED - -
- {upstreamDependencies.length === 0 ? ( -

- Root foundation service. Has zero upstream dependencies. -

- ) : ( -
- {upstreamDependencies.map((depId) => { - const depMeta = STATIC_NODE_METAS[depId]; - const depNode = nodes.find((n) => n.id === depId); - return ( -
setSelectedNodeId(depId)} - className="flex items-center justify-between p-2 rounded-lg bg-[#FAF3EA] border border-[#E5D7C5] hover:border-[#0047AB]/40 cursor-pointer transition-colors text-xs" - > -
- - - {depMeta?.name || depId} - -
- - {depNode?.status || 'healthy'} - -
- ); - })} -
- )} +
+
+ PORT / PROTOCOL + + {selectedNodeMeta.port} ({selectedNodeMeta.protocol}) +
+
+ TELEMETRY P99 + + {selectedNodeMeta.latency} + +
+
+
- {/* Blast Radius Box (Cobalt Patch) */} -
-
- - - Cascaded Blast Radius - - - {blastRadius.length} DOWNSTREAM - -
- -

- If {selectedNodeMeta.name} fails, the following downstream systems are impaired: -

- -
- {blastRadius.length === 0 ? ( -
- - Edge leaf node: 0 downstream dependencies cascaded. + {/* Upstream Prerequisites */} +
+
+ + + Upstream Prerequisites + + + {upstreamDependencies.length} REQUIRED + +
+ {upstreamDependencies.length === 0 ? ( +

+ Root foundation service. Has zero upstream dependencies. +

+ ) : ( +
+ {upstreamDependencies.map((depId) => { + const depMeta = STATIC_NODE_METAS[depId]; + const depNode = nodes.find((n) => n.id === depId); + return ( +
setSelectedNodeId(depId)} + className="flex items-center justify-between p-2 rounded-lg bg-[#FAF3EA] border border-[#E5D7C5] hover:border-[#0047AB]/40 cursor-pointer transition-colors text-xs" + > +
+ + + {depMeta?.name || depId} + +
+ + {depNode?.status || 'healthy'} +
- ) : ( - blastRadius.map((depId) => { - const depMeta = STATIC_NODE_METAS[depId]; - return ( -
setSelectedNodeId(depId)} - className="flex items-center justify-between p-2 rounded-lg bg-black/25 border border-white/10 hover:border-white/30 text-xs cursor-pointer transition-colors" - > - - {depMeta ? depMeta.name : depId} - - - {isSelectedDown ? 'IMPAIRED' : 'AT RISK'} - -
- ); - }) - )} -
+ ); + })}
+ )} +
- {/* Autonomous Playbook Assignment */} -
-
- - Autonomous Playbook Assignment: -
-

- {selectedNode.type === 'database' - ? 'Playbook #PB-01: High-Risk Database Failover to read replica. EIP-712 Multi-Sig Commander approval required.' - : selectedNode.type === 'cache' - ? 'Playbook #PB-02: Automated Redis cache flush, container restart, and client pool reconnection.' - : selectedNode.type === 'gateway' - ? 'Playbook #PB-03: Zero-downtime Envoy ingress reload and dynamic DNS re-route to healthy pods.' - : 'Playbook #PB-04: Graceful SIGTERM pod termination, Kubernetes rolling restart, and readiness probe poll.'} -

-
+ {/* Blast Radius Box (Cobalt Patch) */} +
+
+ + + Cascaded Blast Radius + + + {blastRadius.length} DOWNSTREAM +
-
- {/* Action Controls Footer */} -
- +
+
+ + {/* Autonomous Playbook Assignment */} +
+
+ + Autonomous Playbook Assignment: +
+

+ {selectedNode.type === 'database' + ? 'Playbook #PB-01: High-Risk Database Failover to read replica. EIP-712 Multi-Sig Commander approval required.' + : selectedNode.type === 'cache' + ? 'Playbook #PB-02: Automated Redis cache flush, container restart, and client pool reconnection.' + : selectedNode.type === 'gateway' + ? 'Playbook #PB-03: Zero-downtime Envoy ingress reload and dynamic DNS re-route to healthy pods.' + : 'Playbook #PB-04: Graceful SIGTERM pod termination, Kubernetes rolling restart, and readiness probe poll.'} +

- +
+ ) : ( +
+ Select any service from the DAG topology to inspect its dependencies and blast radius. +
)} - +
+ + {/* Action Controls Footer (Always Pinned and 100% Visible, Never Cut Off!) */} + {selectedNode && ( +
+ +
+ )}
); From 944e1bc147f7eaa2390a815eee0d3f3f52a42ed7 Mon Sep 17 00:00:00 2001 From: Ganeshbeldar015 Date: Sun, 1 Nov 2026 04:28:25 +0530 Subject: [PATCH 5/5] feat(topology): add right slide-over inspector, interactive tier parameters, and Silvana font for docs --- apps/web/index.html | 5 +- .../public/fonts/silvana/Silvana-Bold.woff | Bin 0 -> 9292 bytes .../public/fonts/silvana/Silvana-Italic.woff | Bin 0 -> 9716 bytes .../public/fonts/silvana/Silvana-Light.woff | Bin 0 -> 8788 bytes .../public/fonts/silvana/Silvana-Medium.woff | Bin 0 -> 9304 bytes .../public/fonts/silvana/Silvana-Regular.woff | Bin 0 -> 9208 bytes apps/web/src/components/layout/RootLayout.tsx | 8 +- .../src/components/topology/TopologyGraph.tsx | 369 +++++++++++------- apps/web/src/index.css | 102 +++++ apps/web/src/pages/DocsPage.tsx | 18 +- apps/web/src/pages/TopologyPage.tsx | 18 +- 11 files changed, 355 insertions(+), 165 deletions(-) create mode 100644 apps/web/public/fonts/silvana/Silvana-Bold.woff create mode 100644 apps/web/public/fonts/silvana/Silvana-Italic.woff create mode 100644 apps/web/public/fonts/silvana/Silvana-Light.woff create mode 100644 apps/web/public/fonts/silvana/Silvana-Medium.woff create mode 100644 apps/web/public/fonts/silvana/Silvana-Regular.woff diff --git a/apps/web/index.html b/apps/web/index.html index 8ea13ac..c09e476 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -34,10 +34,11 @@ - + - + +