diff --git a/README.md b/README.md index ece1935..b4cf05f 100644 Binary files a/README.md and b/README.md differ diff --git a/api/_lib/graph.ts b/api/_lib/graph.ts index 9d20b09..d287729 100644 --- a/api/_lib/graph.ts +++ b/api/_lib/graph.ts @@ -169,9 +169,9 @@ export class DependencyGraphEngine { const downstream = this.getBlastRadius(nodeId).filter((id) => id !== nodeId); return { upstream, downstream }; } + hasDirectDependency(fromId: string, toId: string): boolean { const deps = this.adjacency.get(fromId); return deps ? deps.has(toId) : false; } - } diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 41b2821..a547bf3 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -22,12 +22,3 @@ dist-ssr *.njsproj *.sln *.sw? - -# Vite -*.tsbuildinfo - -# Env files (Hidden & Ignored) -.env -.env.* -.env.local -!.env.example diff --git a/apps/web/index.html b/apps/web/index.html index b4f9345..8ea13ac 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -11,7 +11,7 @@ - + @@ -31,14 +31,13 @@ - - - + + - + - + - +
diff --git a/apps/web/package.json b/apps/web/package.json index bf6b4ff..d1c6a17 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,18 +13,13 @@ }, "dependencies": { "@fontsource/poppins": "^5.3.0", - "@react-three/drei": "^10.7.9", - "@react-three/fiber": "^9.8.2", "@tailwindcss/vite": "^4.3.3", "@types/marked": "^6.0.0", - "@types/three": "^0.187.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "daisyui": "^5.7.47", "firebase": "^13.0.0", "framer-motion": "^14.0.0", - "gsap": "^3.15.0", - "lenis": "^1.3.27", "lucide-react": "^1.53.0", "marked": "^18.1.0", "react": "^19.2.8", @@ -32,7 +27,6 @@ "react-router": "^8.4.0", "tailwind-merge": "^3.7.0", "tailwindcss": "^4.3.3", - "three": "^0.187.0", "tw-animate-css": "^1.4.0", "zod": "^4.6.5" }, diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg index 0914261..6893eb1 100644 --- a/apps/web/public/favicon.svg +++ b/apps/web/public/favicon.svg @@ -1,79 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/apps/web/src/components/audit/AuditTable.tsx b/apps/web/src/components/audit/AuditTable.tsx index 22c44d2..4476b0b 100644 --- a/apps/web/src/components/audit/AuditTable.tsx +++ b/apps/web/src/components/audit/AuditTable.tsx @@ -52,41 +52,41 @@ export const AuditTable: React.FC = () => { }; return ( - -
+ +
-

+

Immutable Audit Trail & On-Chain Hashes

-

+

Cryptographically anchored state transitions and human commander approvals.

{/* Search & Filters */} -
-
- +
+
+ setSearch(e.target.value)} - className="w-full pl-8 pr-3 py-1.5 rounded-[7px] bg-[#101012] border border-white/[0.08] text-xs text-[#ECECF0] placeholder-[#63636E] focus:outline-none focus:border-[#6366F1]" + className="w-full pl-9 pr-3.5 py-2 rounded-xl skeuo-well text-xs text-[#1A1A1A] placeholder-[#8A7B6D] focus:outline-none focus:ring-2 focus:ring-[#0047AB]/20 transition-all font-medium" />
-
- +
+
@@ -94,30 +94,30 @@ export const AuditTable: React.FC = () => { -
- {/* Modern Ledger Table */} -
+ {/* Modern Porcelain Ledger Table */} +
- - - - - - - + + + + + + + - + {filteredLogs.length === 0 ? ( - @@ -129,32 +129,32 @@ export const AuditTable: React.FC = () => { return ( - - - - - - @@ -166,9 +166,9 @@ export const AuditTable: React.FC = () => {
TimestampActorAction ExecutedDetailsSeverityOn-Chain Vault
TimestampActorAction ExecutedDetailsSeverityOn-Chain Vault
+ {loading ? 'Fetching audit records...' : 'No matching audit records found.'}
+ {new Date(log.timestamp).toLocaleString()} - + + {log.actor} + {log.action} + {log.details || '—'} + {log.severity} - - + + + 0x{log.id.slice(0, 6)}...{log.id.slice(-4)}
-
+
Displaying {filteredLogs.length} verified incident transitions - Smart Contract: HorizonAuditVault.sol + Smart Contract: HorizonAuditVault.sol
); diff --git a/apps/web/src/components/auth/AuthModal.tsx b/apps/web/src/components/auth/AuthModal.tsx index 31715c5..d3e4b53 100644 --- a/apps/web/src/components/auth/AuthModal.tsx +++ b/apps/web/src/components/auth/AuthModal.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Modal } from '../common/Modal'; import { Button } from '../common/Button'; import { useAuth } from '../../context/useAuth'; -import { Lock, Mail, AlertTriangle, Loader2, ExternalLink, Zap } from 'lucide-react'; +import { Lock, Mail, AlertTriangle, Loader2 } from 'lucide-react'; interface AuthModalProps { isOpen: boolean; @@ -10,7 +10,7 @@ interface AuthModalProps { } export const AuthModal: React.FC = ({ isOpen, onClose }) => { - const { login, register, loginWithGoogle, loginAsDemo, errorMessage, clearError } = useAuth(); + const { login, register, loginWithGoogle, errorMessage, clearError } = useAuth(); const [isRegister, setIsRegister] = useState(false); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -37,88 +37,46 @@ export const AuthModal: React.FC = ({ isOpen, onClose }) => { } }; - const activeError = localError || errorMessage; - const isOperationNotAllowed = activeError?.includes('operation-not-allowed'); - return (
- {activeError && ( -
-
- -
- {isOperationNotAllowed ? ( - <> -
- Email/Password Sign-In Disabled in Firebase Console -
-

- Your Firebase project (horizon-e0a3e) requires Email/Password sign-in to be enabled in Firebase Console. -

-
- - - Enable in Console - - -
- - ) : ( - {activeError} - )} -
-
+ {(localError || errorMessage) && ( +
+ + {localError || errorMessage}
)}
-
-
@@ -129,11 +87,11 @@ export const AuthModal: React.FC = ({ isOpen, onClose }) => {
-
- +
+ Or Continue With -
+
- -
- - Enable in Firebase Console - - -
- - ) : ( - <> - Authentication Error -

{localError || errorMessage}

- - )} -
+
+ +
+ Authentication Error +

{localError || errorMessage}

)} - {/* Instant Demo/Bypass Option */} -
-
-
- -
-
-
Instant Demo Commander Clearance
-
Bypass Firebase authentication for local evaluation
-
-
- -
- {/* STEP 1: Firebase Auth Form */} {!user && ( - +
-
-
@@ -332,7 +287,7 @@ export const OnboardingGate: React.FC<{ children: React.ReactNode }> = ({ childr type="submit" variant="primary" disabled={submitting || isLoading} - className="w-full h-11 font-semibold text-xs gap-2" + className="w-full py-2.5 mt-2 font-semibold text-xs gap-2" > {submitting ? ( <> @@ -348,18 +303,18 @@ export const OnboardingGate: React.FC<{ children: React.ReactNode }> = ({ childr
-
- +
+ Or Continue With -
+
-
- MST Testnet RPC: testnetrpc.mstblockchain.com +
+ MST Testnet RPC: testnetrpc.mstblockchain.com
)}
{/* Footer Branding */} -
+
Horizon Autonomous Enterprise Infrastructure Recovery • Protected by BridgeKey & MST Testnet
); }; - -export default OnboardingGate; diff --git a/apps/web/src/components/common/Badge.tsx b/apps/web/src/components/common/Badge.tsx index 9e74b7b..e3fa39f 100644 --- a/apps/web/src/components/common/Badge.tsx +++ b/apps/web/src/components/common/Badge.tsx @@ -3,7 +3,7 @@ import { cn } from '../../lib/utils'; export type BadgeStatus = 'healthy' | 'degraded' | 'down' | 'recovering' | 'info' | 'critical' | 'warning' | 'approved' | 'pending'; -export interface BadgeProps { +interface BadgeProps { status?: BadgeStatus; children: React.ReactNode; className?: string; @@ -16,42 +16,51 @@ export const Badge: React.FC = ({ className = '', pulse = false, }) => { - const statusMap: Record = { + const statusMap: Record = { healthy: { - dot: 'bg-[#10B981]', - text: 'text-[#0A0A0A]', + bg: 'bg-[#EBF7EE] border-[#0F8E52]/25 text-[#0A6C3D] shadow-[inset_0_1px_2px_rgba(15,142,82,0.08)]', + dot: 'bg-emerald-500 shadow-[0_0_6px_#10B981,inset_0_1px_1px_rgba(255,255,255,0.8)]', + text: 'text-[#0A6C3D]', }, degraded: { - dot: 'bg-[#F59E0B]', - text: 'text-[#0A0A0A]', + bg: 'bg-[#FEF6E7] border-[#D97706]/25 text-[#B45309] shadow-[inset_0_1px_2px_rgba(217,119,6,0.08)]', + dot: 'bg-amber-500 shadow-[0_0_6px_#F59E0B,inset_0_1px_1px_rgba(255,255,255,0.8)]', + text: 'text-[#B45309]', }, down: { - dot: 'bg-[#EF4444]', - text: 'text-[#EF4444]', + bg: 'bg-[#FDF2F2] border-[#DC2626]/25 text-[#B91C1C] shadow-[inset_0_1px_2px_rgba(220,38,38,0.08)]', + dot: 'bg-red-500 shadow-[0_0_6px_#EF4444,inset_0_1px_1px_rgba(255,255,255,0.8)]', + text: 'text-[#B91C1C]', }, recovering: { - dot: 'bg-[#2DD4BF]', - text: 'text-[#0A0A0A]', + bg: 'bg-[#EFF6FF] border-[#0047AB]/25 text-[#0047AB] shadow-[inset_0_1px_2px_rgba(0,71,171,0.08)]', + dot: 'bg-[#0047AB] shadow-[0_0_6px_#1E6BFF,inset_0_1px_1px_rgba(255,255,255,0.8)]', + text: 'text-[#0047AB]', }, critical: { - dot: 'bg-[#EF4444]', - text: 'text-[#EF4444]', + bg: 'bg-[#FEE2E2] border-[#EF4444]/35 text-[#991B1B] shadow-[inset_0_1px_2px_rgba(239,68,68,0.1)]', + dot: 'bg-red-600 shadow-[0_0_8px_#EF4444,inset_0_1px_1px_rgba(255,255,255,0.8)]', + text: 'text-[#991B1B]', }, warning: { - dot: 'bg-[#F59E0B]', - text: 'text-[#0A0A0A]', + bg: 'bg-[#FEF3C7] border-[#F59E0B]/30 text-[#92400E]', + dot: 'bg-amber-500 shadow-[0_0_6px_#F59E0B,inset_0_1px_1px_rgba(255,255,255,0.8)]', + text: 'text-[#92400E]', }, approved: { - dot: 'bg-[#2DD4BF]', - text: 'text-[#0A0A0A]', + bg: 'bg-[#F5F3FF] border-[#7C3AED]/25 text-[#6D28D9]', + dot: 'bg-purple-500 shadow-[0_0_6px_#A855F7,inset_0_1px_1px_rgba(255,255,255,0.8)]', + text: 'text-[#6D28D9]', }, pending: { - dot: 'bg-[#A3A3A3]', - text: 'text-[#6B6B6B]', + bg: 'bg-[#F0F9FF] border-[#0284C7]/25 text-[#0369A1]', + dot: 'bg-sky-500', + text: 'text-[#0369A1]', }, info: { - dot: 'bg-[#2DD4BF]', - text: 'text-[#0A0A0A]', + bg: 'bg-[#F4EBE0] border-[rgba(26,26,26,0.12)] text-[#1A1A1A] shadow-[inset_0_1px_2px_rgba(26,26,26,0.05),0_1px_0_#FFFFFF]', + dot: 'bg-[#1A1A1A]', + text: 'text-[#1A1A1A]', }, }; @@ -60,14 +69,14 @@ export const Badge: React.FC = ({ return ( = ({ ); }; - -export default Badge; diff --git a/apps/web/src/components/common/Button.tsx b/apps/web/src/components/common/Button.tsx index ff24af7..c3ce7e7 100644 --- a/apps/web/src/components/common/Button.tsx +++ b/apps/web/src/components/common/Button.tsx @@ -16,27 +16,27 @@ export const Button: React.FC = ({ ...props }) => { const baseStyles = - 'inline-flex items-center justify-center font-medium rounded-full select-none cursor-pointer disabled:opacity-40 disabled:pointer-events-none transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2DD4BF] focus-visible:ring-offset-2'; + 'inline-flex items-center justify-center font-semibold transition-all duration-150 select-none cursor-pointer disabled:opacity-50 disabled:pointer-events-none active:translate-y-[1px]'; const sizeStyles = { - sm: 'px-4 py-1.5 text-xs gap-1.5 h-8', - md: 'px-5 py-2.5 text-xs sm:text-sm font-medium gap-2 h-10', - lg: 'px-7 py-3 text-sm font-semibold gap-2.5 h-12', + sm: 'px-3 py-1.5 text-xs gap-1.5 rounded-lg', + md: 'px-4 py-2 text-sm gap-2 rounded-xl', + lg: 'px-6 py-3 text-base gap-2.5 rounded-2xl', }[size]; const variantStyles = { primary: - 'bg-[#0A0A0A] text-white border border-[#0A0A0A] hover:bg-neutral-800 hover:-translate-y-0.5 hover:shadow-[0_1px_2px_rgba(0,0,0,.04),0_8px_24px_rgba(0,0,0,.04)] active:translate-y-0 active:bg-black', + 'bg-gradient-to-b from-[#1A62D6] to-[#0047AB] text-[#FFF8F0] border border-[#003680] shadow-[inset_0_1px_0_rgba(255,255,255,0.35),0_2px_4px_rgba(0,71,171,0.25),0_6px_16px_-2px_rgba(0,71,171,0.25)] hover:from-[#246DE0] hover:to-[#0050BD] active:shadow-[inset_0_2px_4px_rgba(0,20,60,0.5)]', secondary: - 'bg-white text-[#0A0A0A] border border-[#E5E5E5] hover:bg-[#FAFAF9] hover:border-[#D4D4D4] hover:-translate-y-0.5 hover:shadow-[0_1px_2px_rgba(0,0,0,.04),0_8px_24px_rgba(0,0,0,.04)] active:translate-y-0', + 'bg-gradient-to-b from-[#FFFFFF] to-[#F5EDE2] text-[#1A1A1A] border border-[rgba(26,26,26,0.18)] shadow-[inset_0_1px_0_#FFFFFF,0_1px_3px_rgba(26,26,26,0.08)] hover:from-[#FFFFFF] hover:to-[#EFE5D7] hover:border-[#0047AB]/40 hover:text-[#0047AB] active:bg-[#EAE0D1] active:shadow-[inset_0_2px_3px_rgba(26,26,26,0.12)]', danger: - 'bg-red-50 text-red-600 border border-red-200 hover:bg-red-100 hover:-translate-y-0.5 active:translate-y-0', + 'bg-gradient-to-b from-[#EF4444] to-[#B91C1C] text-[#FFF8F0] border border-[#991B1B] shadow-[inset_0_1px_0_rgba(255,255,255,0.35),0_2px_4px_rgba(220,38,38,0.25)] hover:brightness-105 active:shadow-[inset_0_2px_4px_rgba(60,0,0,0.5)]', warning: - 'bg-amber-50 text-amber-700 border border-amber-200 hover:bg-amber-100 hover:-translate-y-0.5 active:translate-y-0', + 'bg-gradient-to-b from-[#F59E0B] to-[#B45309] text-[#FFF8F0] border border-[#92400E] shadow-[inset_0_1px_0_rgba(255,255,255,0.35),0_2px_4px_rgba(217,119,6,0.25)] hover:brightness-105 active:shadow-[inset_0_2px_4px_rgba(60,30,0,0.5)]', outline: - 'bg-transparent text-[#0A0A0A] border border-[#E5E5E5] hover:bg-white hover:border-[#D4D4D4] hover:-translate-y-0.5 active:translate-y-0', + 'bg-transparent text-[#0047AB] border-2 border-[#0047AB]/40 hover:border-[#0047AB] hover:bg-[#0047AB]/05 active:bg-[#0047AB]/10', ghost: - 'bg-transparent text-[#6B6B6B] hover:text-[#0A0A0A] hover:bg-neutral-100/60 border border-transparent', + 'bg-transparent text-[#555555] hover:text-[#1A1A1A] hover:bg-[#F3E9DD] border border-transparent hover:border-[rgba(26,26,26,0.08)]', }[variant]; return ( @@ -48,5 +48,3 @@ export const Button: React.FC = ({ ); }; - -export default Button; diff --git a/apps/web/src/components/common/Card.tsx b/apps/web/src/components/common/Card.tsx index 1df41f4..0e7f806 100644 --- a/apps/web/src/components/common/Card.tsx +++ b/apps/web/src/components/common/Card.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { cn } from '../../lib/utils'; -export interface CardProps extends React.HTMLAttributes { +interface CardProps extends React.HTMLAttributes { children: React.ReactNode; className?: string; glow?: boolean; @@ -16,16 +16,17 @@ export const Card: React.FC = ({ return (
+ {/* Top subtle porcelain highlight bevel */} +
{children}
); }; - -export default Card; diff --git a/apps/web/src/components/common/Modal.tsx b/apps/web/src/components/common/Modal.tsx index 918e3be..fc568f6 100644 --- a/apps/web/src/components/common/Modal.tsx +++ b/apps/web/src/components/common/Modal.tsx @@ -43,31 +43,31 @@ export const Modal: React.FC = ({
{/* Frosted Backdrop */}
- {/* Surface Dialog */} + {/* Glass Dialog */}
-
-

{title}

+
+ +
+

{title}

-
{children}
+
{children}
); }; - -export default Modal; diff --git a/apps/web/src/components/copilot/CommandBar.tsx b/apps/web/src/components/copilot/CommandBar.tsx index 50bcc1f..b9d9267 100644 --- a/apps/web/src/components/copilot/CommandBar.tsx +++ b/apps/web/src/components/copilot/CommandBar.tsx @@ -29,6 +29,7 @@ export const CommandBar: React.FC = () => { const res = await sarvamAgent.interpretCommand(prompt, nodes); setResult(res); + // Execute cluster mutations directly if prompted if (res.actionType === 'SIMULATE_FAILURE' && res.targetNodeId) { clusterState.setNodeStatus(res.targetNodeId, 'down'); clusterState.addAuditLog({ @@ -63,50 +64,50 @@ export const CommandBar: React.FC = () => { ]; return ( - -
-
-
- + +
+
+
+
-

+

Sarvam AI Autonomous SRE Copilot - + sarvam-105b

-

- Continuous natural language prompts orchestrating autonomous recovery across connected infrastructure. +

+ Continuous natural language prompts orchestrating autonomous agents across infrastructure.

-
- - MULTI-AGENT COPILOT -
+ + + ACTIVE MULTI-AGENT +
- {/* Input bar */} + {/* Input bar in debossed recessed well */}
- + setPrompt(e.target.value)} - className="w-full pl-10 pr-4 h-12 rounded-[12px] bg-white border border-[#E5E5E5] text-xs sm:text-sm text-[#0A0A0A] placeholder-[#A3A3A3] focus:outline-none focus:border-[#2DD4BF] focus:ring-2 focus:ring-[#2DD4BF]/20 transition-all font-mono" + className="w-full pl-10 pr-4 py-2.5 rounded-xl skeuo-well text-xs text-[#1A1A1A] placeholder-[#8A7B6D] focus:outline-none focus:ring-2 focus:ring-[#0047AB]/30 transition-all font-mono font-medium" />
))}
@@ -138,27 +141,27 @@ export const CommandBar: React.FC = () => { {result && ( -
+
- - + + Sarvam AI Agent Reasoning - + Action: {result.actionType}
-

{result.assistantReply}

+

{result.assistantReply}

{result.reasoning && ( -
+
{result.reasoning}
@@ -170,5 +173,3 @@ export const CommandBar: React.FC = () => { ); }; - -export default CommandBar; diff --git a/apps/web/src/components/dag/MiroDagCanvas.tsx b/apps/web/src/components/dag/MiroDagCanvas.tsx index 9e7a17c..12595ad 100644 --- a/apps/web/src/components/dag/MiroDagCanvas.tsx +++ b/apps/web/src/components/dag/MiroDagCanvas.tsx @@ -12,7 +12,10 @@ import { Maximize2, Minimize2, Scan, + Hand, X, + ShieldAlert, + CheckCircle2, } from 'lucide-react'; import { cn } from '../../lib/utils'; @@ -83,6 +86,10 @@ export const MiroDagCanvas: React.FC = ({ }; }, [selectedNodeId, architecture]); + /** + * Fit to View: Automatically calculates bounding box of all nodes + * and centers & scales them with generous padding so 100% of the DAG is visible. + */ const fitToView = useCallback((customPositions?: Record) => { const activePositions = customPositions || positions; const nodeIds = architecture.nodes.map((n) => n.id); @@ -92,122 +99,201 @@ export const MiroDagCanvas: React.FC = ({ if (validPositions.length === 0 || !containerRef.current) return; - const minX = Math.min(...validPositions.map((p) => p.x)); - const maxX = Math.max(...validPositions.map((p) => p.x)); - const minY = Math.min(...validPositions.map((p) => p.y)); - const maxY = Math.max(...validPositions.map((p) => p.y)); + // Node card bounds: width is 185px, height is ~72px, centered at (x, y) + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + + validPositions.forEach((p) => { + minX = Math.min(minX, p.x - 100); + maxX = Math.max(maxX, p.x + 100); + minY = Math.min(minY, p.y - 45); + maxY = Math.max(maxY, p.y + 45); + }); + + const contentWidth = Math.max(200, maxX - minX); + const contentHeight = Math.max(160, maxY - minY); - const nodeWidth = 190; - const nodeHeight = 85; + const containerWidth = containerRef.current.clientWidth || 940; + const containerHeight = containerRef.current.clientHeight || (isFullscreen ? window.innerHeight - 120 : 680); - const boundingW = (maxX - minX) + nodeWidth + 120; - const boundingH = (maxY - minY) + nodeHeight + 140; + const paddingX = 64; + const paddingY = 64; - const containerW = containerRef.current.clientWidth || 900; - const containerH = containerRef.current.clientHeight || 650; + const availableW = Math.max(100, containerWidth - paddingX * 2); + const availableH = Math.max(100, containerHeight - paddingY * 2); - const scaleX = containerW / boundingW; - const scaleY = containerH / boundingH; - const calculatedZoom = Math.min(1.2, Math.max(0.45, Math.min(scaleX, scaleY))); + const scaleX = availableW / contentWidth; + const scaleY = availableH / contentHeight; - const boundingCenterX = (minX + maxX) / 2; - const boundingCenterY = (minY + maxY) / 2; + // Scale clamped between 0.35 and 1.15 to ensure readability while guaranteeing fit + const optimalZoom = Math.min(1.15, Math.max(0.35, Math.min(scaleX, scaleY))); - const panX = containerW / 2 - boundingCenterX * calculatedZoom; - const panY = containerH / 2 - boundingCenterY * calculatedZoom; + // Center content within container + const contentCenterX = (minX + maxX) / 2; + const contentCenterY = (minY + maxY) / 2; - setZoom(calculatedZoom); - setPan({ x: panX, y: panY }); - }, [architecture.nodes, positions]); + const targetPanX = containerWidth / 2 - contentCenterX * optimalZoom; + const targetPanY = containerHeight / 2 - contentCenterY * optimalZoom; + setZoom(optimalZoom); + setPan({ x: targetPanX, y: targetPanY }); + }, [architecture.nodes, positions, isFullscreen]); + + /** + * Initial spacious hierarchical tree layout calculation. + * Uses normalized coordinate system then auto-fits cleanly into viewport. + */ const computeInitialTreeLayout = useCallback(() => { const newPositions: Record = {}; - const levels = architecture.topologicalLevels; - - const levelGapY = 160; - const nodeGapX = 220; - - levels.forEach((levelNodeIds, levelIndex) => { - const y = 80 + levelIndex * levelGapY; - const count = levelNodeIds.length; - const totalWidth = (count - 1) * nodeGapX; - const startX = -totalWidth / 2; - - levelNodeIds.forEach((id, nodeIdx) => { - newPositions[id] = { - x: startX + nodeIdx * nodeGapX + 450, - y, - }; + const levels = architecture.topologicalLevels.slice().reverse(); // Reverse so Ingress is at top + const totalLevels = levels.length; + + // Spacing constants + const levelHeight = 160; + const nodeSpacing = 240; + const centerX = 600; // Reference origin center + const topMargin = 100; + + levels.forEach((tierNodeIds, tierIdx) => { + const y = topMargin + tierIdx * levelHeight; + const count = tierNodeIds.length; + const startX = count === 1 ? centerX : centerX - ((count - 1) * nodeSpacing) / 2; + + tierNodeIds.forEach((nodeId, nodeIdx) => { + const x = count === 1 ? centerX : startX + nodeIdx * nodeSpacing; + newPositions[nodeId] = { x, y }; }); }); setPositions(newPositions); - setTimeout(() => fitToView(newPositions), 40); - }, [architecture.topologicalLevels, fitToView]); + // Automatically fit graph to view once positions are populated + requestAnimationFrame(() => { + fitToView(newPositions); + }); + }, [architecture, fitToView]); + + // Reset or initialize positions when architecture changes useEffect(() => { computeInitialTreeLayout(); - }, [architecture.architectureName, computeInitialTreeLayout]); + }, [computeInitialTreeLayout]); - const handleMouseDownCanvas = (e: React.MouseEvent) => { - if ((e.target as HTMLElement).closest('.miro-dag-canvas button')) return; - setIsPanning(true); - panStartRef.current = { - mouseX: e.clientX, - mouseY: e.clientY, - initialPan: { ...pan }, + // Refit when fullscreen state changes or window resizes + useEffect(() => { + const timer = setTimeout(() => { + fitToView(); + }, 80); + return () => clearTimeout(timer); + }, [isFullscreen, fitToView]); + + useEffect(() => { + const handleResize = () => { + fitToView(); }; - }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [fitToView]); + // Handle ESC key to exit fullscreen + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isFullscreen) { + setIsFullscreen(false); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isFullscreen]); + + // Lock body scroll in fullscreen mode + useEffect(() => { + if (isFullscreen) { + const origOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = origOverflow; + }; + } + }, [isFullscreen]); + + // Node Dragging Handlers const handleMouseDownNode = (e: React.MouseEvent, nodeId: string) => { e.stopPropagation(); + const currentPos = positions[nodeId] || { x: 0, y: 0 }; setDraggingNodeId(nodeId); dragStartRef.current = { mouseX: e.clientX, mouseY: e.clientY, - initialNodePos: { ...positions[nodeId] }, + initialNodePos: { ...currentPos }, + }; + }; + + // Canvas Panning Handlers (Drag Background) + const handleMouseDownCanvas = (e: React.MouseEvent) => { + // Only primary mouse button initiates pan + if (e.button !== 0) return; + setIsPanning(true); + panStartRef.current = { + mouseX: e.clientX, + mouseY: e.clientY, + initialPan: { ...pan }, }; }; + // Global Mouse Move and Mouse Up Listeners useEffect(() => { const handleMouseMove = (e: MouseEvent) => { + // 1. Handling Node Drag + if (draggingNodeId && dragStartRef.current) { + const dx = (e.clientX - dragStartRef.current.mouseX) / zoom; + const dy = (e.clientY - dragStartRef.current.mouseY) / zoom; + const nextX = dragStartRef.current.initialNodePos.x + dx; + const nextY = dragStartRef.current.initialNodePos.y + dy; + + setPositions((prev) => ({ + ...prev, + [draggingNodeId]: { x: nextX, y: nextY }, + })); + return; + } + + // 2. Handling Canvas Pan if (isPanning && panStartRef.current) { const dx = e.clientX - panStartRef.current.mouseX; const dy = e.clientY - panStartRef.current.mouseY; + setPan({ x: panStartRef.current.initialPan.x + dx, y: panStartRef.current.initialPan.y + dy, }); - } else if (draggingNodeId && dragStartRef.current) { - const dx = (e.clientX - dragStartRef.current.mouseX) / zoom; - const dy = (e.clientY - dragStartRef.current.mouseY) / zoom; - setPositions((prev) => ({ - ...prev, - [draggingNodeId]: { - x: dragStartRef.current!.initialNodePos.x + dx, - y: dragStartRef.current!.initialNodePos.y + dy, - }, - })); } }; const handleMouseUp = () => { - setIsPanning(false); setDraggingNodeId(null); - panStartRef.current = null; dragStartRef.current = null; + setIsPanning(false); + panStartRef.current = null; }; - window.addEventListener('mousemove', handleMouseMove); - window.addEventListener('mouseup', handleMouseUp); + if (draggingNodeId || isPanning) { + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + } + return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; - }, [isPanning, draggingNodeId, zoom]); + }, [draggingNodeId, isPanning, zoom]); + // Smooth Interactive Mousewheel Zoom anchored to cursor position const handleWheel = (e: React.WheelEvent) => { e.preventDefault(); + e.stopPropagation(); + if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const cursorX = e.clientX - rect.left; @@ -216,6 +302,7 @@ export const MiroDagCanvas: React.FC = ({ const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9; const nextZoom = Math.min(2.2, Math.max(0.3, zoom * zoomFactor)); + // Anchor canvas coordinates under cursor const canvasX = (cursorX - pan.x) / zoom; const canvasY = (cursorY - pan.y) / zoom; @@ -248,11 +335,11 @@ export const MiroDagCanvas: React.FC = ({
= ({
{/* Left Badge Header */}
-
- - +
+ + {architecture.architectureName} - + ({architecture.nodes.length} nodes • {architecture.topologicalLevels.length} tiers) - + {Math.round(zoom * 100)}%
{/* Right Canvas Controls Toolbar */} -
+
+ {/* Fit to View Button */} -
+
+ {/* Zoom In */} + {/* Zoom Out */} -
+
+ {/* Reset Layout */} -
+
+ {/* Fullscreen Expand Toggle */}
-
- +
+ Upstream Deps - + {blastInfo.upstreamDeps.length} Services
-
- +
+ Blast Radius - + {blastInfo.downstreamBlast.length} Impaired
@@ -407,7 +499,7 @@ export const MiroDagCanvas: React.FC = ({
)} - {/* ── Interactive Canvas Scaled Area ── */} + {/* ── Interactive Canvas Scaled Area (Transformed Container) ── */}
= ({ transition: isPanning || draggingNodeId ? 'none' : 'transform 0.12s cubic-bezier(0.16, 1, 0.3, 1)', }} > + {/* SVG Directed Bezier Curves connecting nodes */} + {/* Standard directional arrow marker */} = ({ markerHeight="6" orient="auto-start-reverse" > - + + {/* Upstream provider highlighted arrow */} = ({ markerHeight="6" orient="auto-start-reverse" > - + + {/* Downstream blast highlighted arrow */} = ({ markerHeight="6" orient="auto-start-reverse" > - + + {/* Render directed dependency edges */} {architecture.nodes.map((node) => { const sourcePos = positions[node.id]; if (!sourcePos) return null; @@ -462,27 +559,31 @@ export const MiroDagCanvas: React.FC = ({ const targetPos = positions[targetId]; if (!targetPos) return null; + // Node dimensions const nodeHalfHeight = 34; + // Calculate start (source bottom) and end (target top) const startX = sourcePos.x; const startY = sourcePos.y + nodeHalfHeight; const endX = targetPos.x; const endY = targetPos.y - nodeHalfHeight; + // Smooth cubic bezier curve control points with generous spacing const deltaY = Math.abs(endY - startY); const controlY1 = startY + Math.max(40, deltaY * 0.45); const controlY2 = endY - Math.max(40, deltaY * 0.45); + // Determine highlights const isUpstreamEdge = selectedNodeId === node.id && blastInfo?.upstreamDeps.includes(targetId); const isDownstreamEdge = selectedNodeId === targetId && blastInfo?.downstreamBlast.includes(node.id); const strokeColor = isUpstreamEdge - ? '#34D399' + ? '#059669' : isDownstreamEdge - ? '#F87171' - : 'rgba(255, 255, 255, 0.2)'; + ? '#E11D48' + : '#B0A294'; const strokeWidth = isUpstreamEdge || isDownstreamEdge ? 2.5 : 1.5; const markerId = isUpstreamEdge @@ -507,7 +608,7 @@ export const MiroDagCanvas: React.FC = ({ })} - {/* ── Draggable Node Cards ── */} + {/* ── Draggable Node Cards (Miro Style) ── */} {architecture.nodes.map((node) => { const pos = positions[node.id]; if (!pos) return null; @@ -531,59 +632,61 @@ export const MiroDagCanvas: React.FC = ({ transform: 'translate(-50%, -50%)', }} className={cn( - 'absolute z-15 w-[185px] p-3 rounded-[12px] border transition-all duration-150 select-none cursor-grab active:cursor-grabbing', - 'shadow-[0_4px_16px_rgba(0,0,0,0.5)]', + 'absolute z-15 w-[185px] p-3 rounded-2xl border transition-all duration-150 select-none cursor-grab active:cursor-grabbing', + 'shadow-[0_4px_16px_rgba(0,0,0,0.06),inset_0_1px_0_rgba(255,255,255,0.8)]', isSelected - ? 'bg-[#18181B] border-[#6366F1] ring-2 ring-[#6366F1] shadow-[0_0_24px_rgba(99,102,241,0.3)] scale-105 z-20' + ? 'bg-white border-[#0047AB] ring-2 ring-[#0047AB] shadow-xl scale-105 z-20' : isDownstream - ? 'bg-[#18181B] border-[#F87171] ring-1 ring-[#F87171] text-[#ECECF0]' + ? 'bg-rose-50/95 border-rose-400 ring-2 ring-rose-400 text-rose-950 shadow-md' : isUpstream - ? 'bg-[#18181B] border-[#34D399] ring-1 ring-[#34D399] text-[#ECECF0]' - : 'bg-[#141416] border-white/[0.08] hover:border-white/[0.18] hover:scale-[1.02]' + ? 'bg-emerald-50/95 border-emerald-400 ring-2 ring-emerald-400 text-emerald-950 shadow-md' + : 'bg-white/95 border-[#DACBB8] hover:border-[#0047AB]/50 hover:shadow-md hover:scale-[1.02]' )} >
- + {node.name} - + {node.type}
+ {/* Health status dot */}
-
+ {/* Dependency indicator footer */} +
{node.dependencies.length === 0 ? 'Root Service' : `Deps: ${node.dependencies.length}`} - + drag @@ -594,11 +697,11 @@ export const MiroDagCanvas: React.FC = ({
{/* ── Miro Canvas Footer Guidance Bar ── */} -
- - 💡 Pan: Click & drag • Zoom: Scroll or ± • Reposition: Drag cards +
+ + 💡 Pan: Click & drag background • Zoom: Scroll or ± • Reposition: Drag cards - + {selectedNodeId ? 'Click background to deselect' : 'Click any node to isolate blast radius'}
@@ -607,3 +710,4 @@ export const MiroDagCanvas: React.FC = ({ return content; }; + diff --git a/apps/web/src/components/dashboard/MetricWidget.tsx b/apps/web/src/components/dashboard/MetricWidget.tsx index de85ec9..eddc510 100644 --- a/apps/web/src/components/dashboard/MetricWidget.tsx +++ b/apps/web/src/components/dashboard/MetricWidget.tsx @@ -4,17 +4,21 @@ import { TrendingUp, TrendingDown } from 'lucide-react'; import { cn } from '../../lib/utils'; interface MetricWidgetProps { - title: string; - value: string | number; - trend?: string; + title: string; + value: string | number; + trend?: string; isPositive?: boolean; - icon?: React.ReactNode; - subtitle?: string; - badge?: string; + icon?: React.ReactNode; + subtitle?: string; + badge?: string; sparkline?: number[]; className?: string; } +/** + * MetricWidget — Tactile porcelain KPI bento card with skeuomorphic sparkline well. + * Cream background, ink black numbers, cobalt blue sparkline & accents. + */ export const MetricWidget: React.FC = ({ title, value, @@ -26,13 +30,15 @@ export const MetricWidget: React.FC = ({ sparkline = [20, 24, 22, 28, 26, 34, 32, 40], className, }) => { - const W = 110; - const H = 32; - const PAD = 2; + /* ── Sparkline SVG ── */ + const W = 120; + const H = 36; + const PAD = 3; - const minV = Math.min(...sparkline); - const maxV = Math.max(...sparkline); + const minV = Math.min(...sparkline); + const maxV = Math.max(...sparkline); const range = maxV - minV || 1; + const gradId = `spark-${title.replace(/\W/g, '')}`; const pts = sparkline.map((v, i) => { const x = PAD + (i / (sparkline.length - 1)) * (W - 2 * PAD); @@ -40,49 +46,65 @@ export const MetricWidget: React.FC = ({ return `${x.toFixed(1)},${y.toFixed(1)}`; }); const lineD = `M ${pts.join(' L ')}`; + const areaD = pts.length + ? `M ${pts[0]} L ${pts.join(' L ')} L ${pts[pts.length - 1].split(',')[0]},${H} L ${pts[0].split(',')[0]},${H} Z` + : ''; - const strokeColor = isPositive ? '#2DD4BF' : '#EF4444'; + const strokeColor = isPositive ? '#0047AB' : '#DC2626'; return ( + {/* Top row: title + icon */}
-
-

{title}

+
+

{title}

{badge && ( - + {badge} )}
{icon && ( -
+
{icon}
)}
-
+ {/* Value + skeuomorphic sparkline well */} +
{value}
-
-
+
-
+ {/* Footer: trend + subtitle */} +
{trend ? (
- {isPositive ? : } + {isPositive + ?
) : ( - {subtitle || 'Live'} + {subtitle || 'Live'} )} {subtitle && trend && ( - {subtitle} + {subtitle} )}
); }; - -export default MetricWidget; diff --git a/apps/web/src/components/dashboard/RecentActivityWidget.tsx b/apps/web/src/components/dashboard/RecentActivityWidget.tsx index fcddbad..f935560 100644 --- a/apps/web/src/components/dashboard/RecentActivityWidget.tsx +++ b/apps/web/src/components/dashboard/RecentActivityWidget.tsx @@ -38,32 +38,32 @@ export const RecentActivityWidget: React.FC = () => { }, []); return ( - +
-
-
-
- +
+
+
+
-

+

Incident Triage Stream

-

+

Cryptographically hashed audit ledger

-
- - LIVE FEED -
+ + + LIVE FEED +
{/* Timeline Stream */} -
+
{logs.length === 0 ? ( -
+
{loading ? 'Streaming audit trail...' : 'No incident logs recorded yet.'}
) : ( @@ -74,53 +74,58 @@ export const RecentActivityWidget: React.FC = () => { const isWarn = log.severity === 'warning'; return ( -
- {/* Timeline Dot */} +
+ {/* Timeline Node Dot */}
+ > +
+
-
+
-
-
- +
+
+
- + {log.action}
{log.severity}
-

+

{log.details || `Triggered by ${log.actor}`}

-
- +
+ {log.actor} - - + + {new Date(log.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
@@ -132,18 +137,16 @@ export const RecentActivityWidget: React.FC = () => {
-
+
View All On-Chain Audits - + - MST Block Confirmed + MST Block Confirmed
); }; - -export default RecentActivityWidget; diff --git a/apps/web/src/components/dashboard/SystemHealthOverview.tsx b/apps/web/src/components/dashboard/SystemHealthOverview.tsx index 82e1032..b4db838 100644 --- a/apps/web/src/components/dashboard/SystemHealthOverview.tsx +++ b/apps/web/src/components/dashboard/SystemHealthOverview.tsx @@ -28,6 +28,7 @@ const iconMap: Record = { payment: CreditCard, }; +// Node metadata for high-fidelity SRE telemetry const nodeTelemetryMap: Record = { 'db-primary': { port: '5432', region: 'us-east-1a', latency: '4ms' }, 'db-replica': { port: '5433', region: 'us-east-1b', latency: '6ms' }, @@ -81,34 +82,34 @@ export const SystemHealthOverview: React.FC = () => { const downCount = nodes.filter((n) => n.status === 'down' || n.status === 'degraded').length; return ( - +
{/* Header with Filters */} -
+
-

+

Infrastructure Services Matrix

- - {nodes.length} Active Nodes + + {nodes.length} Nodes Active
-

- Live continuous probes across database primaries, replica clusters, and distributed gateways. +

+ Live probes across multi-tier database instances, distributed cache tiers, and edge gateways.

- {/* Filter Tabs */} -
+ {/* Skeuomorphic Filter Tabs */} +
@@ -162,38 +162,38 @@ export const SystemHealthOverview: React.FC = () => {
{/* Node Header */}
-
+
- +
-
-

+
+

{node.name}

- + :{telemetry.port} - • - + • + {telemetry.region}
@@ -204,27 +204,27 @@ export const SystemHealthOverview: React.FC = () => {
- {/* Telemetry Well */} -
- - - Latency: + {/* Debossed Telemetry Well */} +
+ + + Probe Latency: - + {isDown ? 'TIMEOUT' : telemetry.latency}
{/* Node Footer Actions */} -
-
+
+
{node.dependencies.length > 0 ? ( ← {node.dependencies.length} deps ) : ( - Root Service + Root Service )}
@@ -232,14 +232,14 @@ export const SystemHealthOverview: React.FC = () => { onClick={() => handleToggleFailure(node)} disabled={mutatingId === node.id} className={cn( - 'text-xs font-medium px-3 py-1 rounded-full transition-all cursor-pointer border select-none', + 'text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer border select-none', isDown - ? 'bg-emerald-600 text-white border-emerald-600 hover:bg-emerald-700' - : 'bg-white text-[#0A0A0A] hover:bg-neutral-50 border-[#E5E5E5]' + ? 'bg-[#EBF7EE] text-[#0A6C3D] border-[#0F8E52]/40 hover:bg-[#D8F0DE] shadow-[0_1px_2px_rgba(15,142,82,0.15)]' + : 'bg-[#FDF2F2] text-[#B91C1C] border-[#DC2626]/25 hover:bg-[#FDE8E8]' )} > {mutatingId === node.id ? ( - + ) : isDown ? ( 'Recover' ) : ( @@ -254,27 +254,25 @@ export const SystemHealthOverview: React.FC = () => {
{/* SRE Chaos Guide Footer */} -
+
- + {downCount > 0 ? ( - {downCount} service(s) impacted — automated DAG recovery loop triggered. + {downCount} service(s) impacted — autonomous recovery sequence initialized. ) : ( - 'Chaos switch active: trigger failover on any node to verify blast radius & Kahn sort.' + 'Chaos engineering switch active: toggle failover on any node to verify blast radius.' )}
- Open Full DAG Topology - + Open Full Dependency DAG +
); }; - -export default SystemHealthOverview; diff --git a/apps/web/src/components/dashboard/WarRoomWidget.tsx b/apps/web/src/components/dashboard/WarRoomWidget.tsx index 94b9a32..522ee30 100644 --- a/apps/web/src/components/dashboard/WarRoomWidget.tsx +++ b/apps/web/src/components/dashboard/WarRoomWidget.tsx @@ -33,25 +33,25 @@ export const WarRoomWidget: React.FC = () => { : messages.filter((m) => m.channel === activeChannel); return ( - +
{/* Header & Controls */} -
-
-
- +
+
+
+
-

+

SRE Incident Broadcast & Team War Room

- + MULTI-CHANNEL
-

- Automated cross-team coordination stream and outbound webhook dispatching. +

+ Automated cross-team coordination channels and outbound webhook dispatching.

@@ -61,7 +61,7 @@ export const WarRoomWidget: React.FC = () => { variant="secondary" size="sm" onClick={() => setIsConfigOpen((prev) => !prev)} - className="gap-1.5 text-xs rounded-full" + className="gap-1.5 text-xs rounded-xl font-bold" title="Configure Slack / Discord Webhook URL" > @@ -72,13 +72,13 @@ export const WarRoomWidget: React.FC = () => { {/* Webhook Settings Drawer */} {isConfigOpen && ( -
+
- - + + Outbound Incident Webhook Configuration - Slack • Discord • Custom HTTP + Slack • Discord • Custom HTTP
{ value={webhookUrlInput} onChange={(e) => setWebhookUrlInput(e.target.value)} placeholder="https://hooks.slack.com/services/... or https://discord.com/api/webhooks/..." - className="flex-1 rounded-[12px] bg-white border border-[#E5E5E5] px-3.5 py-2 text-xs text-[#0A0A0A] placeholder-[#A3A3A3] focus:outline-none focus:border-[#2DD4BF] focus:ring-2 focus:ring-[#2DD4BF]/20" + className="flex-1 rounded-xl bg-white border border-[rgba(26,26,26,0.14)] px-3 py-1.5 text-xs text-[#1A1A1A] placeholder-[#888888] focus:outline-none focus:border-[#0047AB]" />
- {webhookSaved && ( -

- Webhook URL safely stored in localStorage for live incident dispatches. -

- )}
)} {/* Channel Filter Tabs */} -
+
{/* Message Feed Scroll Area */} -
+
{filteredMessages.map((msg) => (
-
- {msg.channel} - • - {msg.sender} +
+ {msg.channel} + • + {msg.sender}
{msg.badge && ( - + {msg.badge} )} - {msg.timestamp} + {msg.timestamp}
-

{msg.text}

+

{msg.text}

))}
-
+
Outbound Webhook: {notificationHub.getWebhookUrl() ? 'Configured (Active)' : 'Local Broadcast Only'} Audit Sync: MST Testnet Block Hash Verified
diff --git a/apps/web/src/components/layout/BentoGrid.tsx b/apps/web/src/components/layout/BentoGrid.tsx index 761c100..b147ca8 100644 --- a/apps/web/src/components/layout/BentoGrid.tsx +++ b/apps/web/src/components/layout/BentoGrid.tsx @@ -1,17 +1,17 @@ import React from 'react'; import { cn } from '../../lib/utils'; -export const BentoGrid = ({ className, children }: { className?: string; children: React.ReactNode }) => { +export const BentoGrid = ({ className, children }: { className?: string, children: React.ReactNode }) => { return ( -
+
{children}
); }; -export const BentoCard = ({ className, children }: { className?: string; children: React.ReactNode }) => { +export const BentoCard = ({ className, children }: { className?: string, children: React.ReactNode }) => { return ( -
+
{children}
); diff --git a/apps/web/src/components/layout/Footer.tsx b/apps/web/src/components/layout/Footer.tsx index 593178d..df73b56 100644 --- a/apps/web/src/components/layout/Footer.tsx +++ b/apps/web/src/components/layout/Footer.tsx @@ -1,123 +1,263 @@ import React from 'react'; import { Link } from 'react-router'; -import { ExternalLink, ArrowUpRight } from 'lucide-react'; -import { HorizonLogo } from '../common/HorizonLogo'; +import { + Terminal, + Sparkles, + ShieldCheck, + Gem, + Network, + RotateCcw, + LayoutDashboard, + ExternalLink, + ArrowUpRight, + Activity, + Lock, +} from 'lucide-react'; import { MST_CONFIG } from '../../engine/mstBlockchain'; +/** + * Aesthetic Antigravity-Style Footer. + * Deep black background (#0A0A0A) with crisp white typography, + * top-left tagline, top-right categorized links, monumental "HORIZON" display title, + * and small copyright line at the very bottom. + */ export const Footer: React.FC = () => { return ( -