import { useEffect, useRef, useCallback, useState, memo } from 'react';
import { ReactFlowProvider } from '@xyflow/react';
import { useShallow } from 'zustand/react/shallow';
import { io as ioClient, Socket } from 'socket.io-client';
import { useTopologyStore } from './store/topologyStore';
import {
defaultNetworkInfo,
discoverHosts,
convertToTopology,
DiscoveredHost
} from './services/discovery';
import Header from './components/Header';
import LeftPanel from './components/LeftPanel';
import RightPanel from './components/RightPanel';
import TopologyGraph from './components/Graph/TopologyGraph';
import CommandPalette from './components/CommandPalette';
import StaleWarning from './components/StaleWarning';
import TerminalPanel from './components/TerminalPanel';
import MetricsBar from './components/Dashboard/MetricsBar';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
interface ApiHost {
name: string;
ip: string;
online: boolean;
containers?: string[];
services?: string[];
vms?: Array<{ id: string; name: string; status: string; type: 'lxc' | 'qemu' }>;
}
interface ApiDiscoveryResponse {
hosts: ApiHost[];
timestamp: string;
errors: string[];
}
function App() {
const {
setNodes,
setEdges,
setNetworkInfo,
setHosts,
setLastUpdated,
setIsLoading,
setDataSource,
incrementFailures,
resetFailures,
setLastSuccessfulDiscovery,
} = useTopologyStore(useShallow((s) => ({
setNodes: s.setNodes,
setEdges: s.setEdges,
setNetworkInfo: s.setNetworkInfo,
setHosts: s.setHosts,
setLastUpdated: s.setLastUpdated,
setIsLoading: s.setIsLoading,
setDataSource: s.setDataSource,
incrementFailures: s.incrementFailures,
resetFailures: s.resetFailures,
setLastSuccessfulDiscovery: s.setLastSuccessfulDiscovery,
})));
const setConnectionStatus = useTopologyStore((s) => s.setConnectionStatus);
const {
leftPanelOpen,
rightPanelOpen,
isLoading,
pollInterval,
terminalOpen,
terminalHost,
} = useTopologyStore(useShallow((s) => ({
leftPanelOpen: s.leftPanelOpen,
rightPanelOpen: s.rightPanelOpen,
isLoading: s.isLoading,
pollInterval: s.pollInterval,
terminalOpen: s.terminalOpen,
terminalHost: s.terminalHost,
})));
const toggleCommandPalette = useTopologyStore((s) => s.toggleCommandPalette);
const closeTerminal = useTopologyStore((s) => s.closeTerminal);
const isLoadingRef = useRef(isLoading);
isLoadingRef.current = isLoading;
const pollIntervalRef = useRef(pollInterval);
pollIntervalRef.current = pollInterval;
const loadData = useCallback(async () => {
if (isLoadingRef.current) return;
setIsLoading(true);
try {
const response = await fetch(`${API_BASE_URL}/api/discover`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
const data: ApiDiscoveryResponse = await response.json();
const discoveredHosts: DiscoveredHost[] = data.hosts.map((h: ApiHost) => ({
name: h.name,
ip: h.ip,
online: h.online,
containers: (h.containers || []).map((c: string) => ({
name: c,
image: '',
status: 'running',
ports: [],
created: ''
})),
services: h.services,
vms: h.vms
}));
const { nodes, edges } = convertToTopology(discoveredHosts, defaultNetworkInfo);
const hosts = discoveredHosts.map(h => ({
name: h.name,
ip: h.ip,
type: (h.name === 'ubuntu' ? 'vm' :
h.name === 'proxmox' || h.name === 'truenas' ? 'physical' : 'rpi5') as 'vm' | 'physical' | 'rpi5' | 'container',
role: h.name === 'ubuntu' ? 'Primary Docker Host' :
h.name === 'grizzley' ? 'Edge Services' :
h.name === 'truenas' ? 'Storage (NAS)' :
h.name === 'proxmox' ? 'Hypervisor' : 'Host',
containers: h.containers.map(c => c.name),
services: h.services,
vms: h.vms
}));
setNodes(nodes);
setEdges(edges);
setNetworkInfo(defaultNetworkInfo);
setHosts(hosts);
setLastUpdated(new Date());
setDataSource('live');
setLastSuccessfulDiscovery(new Date());
resetFailures();
} else {
throw new Error(`API error: ${response.status}`);
}
} catch (error) {
console.error('Discovery failed, using simulated data:', error);
const discoveryResult = await discoverHosts(['ubuntu', 'grizzley', 'truenas', 'ice', 'panda', 'proxmox']);
const { nodes, edges } = convertToTopology(discoveryResult.hosts, defaultNetworkInfo);
const hosts = discoveryResult.hosts.map(h => ({
name: h.name,
ip: h.ip,
type: (h.name === 'ubuntu' ? 'vm' :
h.name === 'proxmox' || h.name === 'truenas' ? 'physical' : 'rpi5') as 'vm' | 'physical' | 'rpi5' | 'container',
role: h.name === 'ubuntu' ? 'Primary Docker Host' :
h.name === 'grizzley' ? 'Edge Services' :
h.name === 'truenas' ? 'Storage (NAS)' :
h.name === 'proxmox' ? 'Hypervisor' : 'Host',
containers: h.containers.map(c => c.name)
}));
setNodes(nodes);
setEdges(edges);
setNetworkInfo(defaultNetworkInfo);
setHosts(hosts);
setLastUpdated(new Date());
setDataSource('simulated');
incrementFailures();
} finally {
setIsLoading(false);
}
}, [setNodes, setEdges, setNetworkInfo, setHosts, setLastUpdated, setIsLoading, setDataSource, incrementFailures, resetFailures, setLastSuccessfulDiscovery]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
toggleCommandPalette();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [toggleCommandPalette]);
useEffect(() => {
loadData();
}, []);
useEffect(() => {
const intervalId = setInterval(loadData, pollIntervalRef.current);
return () => clearInterval(intervalId);
}, [loadData]);
// --- WebSocket connection (websocket-engineer skill) ---
useEffect(() => {
const socket: Socket = ioClient(API_BASE_URL, {
transports: ['websocket', 'polling'],
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: Infinity,
});
socket.on('connect', () => {
setConnectionStatus('ws');
});
socket.on('disconnect', () => {
setConnectionStatus('polling');
});
socket.on('connect_error', () => {
setConnectionStatus('polling');
});
// Listen for real-time topology updates
socket.on('topology:update', (data: ApiDiscoveryResponse) => {
if (data?.hosts) {
const discoveredHosts: DiscoveredHost[] = data.hosts.map((h: ApiHost) => ({
name: h.name,
ip: h.ip,
online: h.online,
containers: (h.containers || []).map((c: string) => ({
name: c, image: '', status: 'running', ports: [], created: ''
})),
services: h.services,
vms: h.vms
}));
const { nodes, edges } = convertToTopology(discoveredHosts, defaultNetworkInfo);
setNodes(nodes);
setEdges(edges);
setLastUpdated(new Date());
}
});
return () => {
socket.disconnect();
};
}, [setConnectionStatus, setNodes, setEdges, setLastUpdated]);
return (