feat: integrate all 10 skills into homelab-topology

- Added api-security-hardening (helmet, rate limits)
- Added nodejs-backend-patterns (error handling)
- Added observability-monitoring (pino logging)
- Added websocket-engineer (socket.io real-time updates)
- Added docker (Multi-stage build, compose)
- Added vitest (testing configuration and store tests)
- Added data-visualizer (MetricsBar and HostChart)
- Added infrastructure-monitoring/proxmox-admin/network-engineer types
- Fixed UI accessibility and styling
- Cleaned up node_modules tracking
This commit is contained in:
2026-02-20 20:35:08 -08:00
parent 3dc5d236a2
commit 6dd679b8e0
14455 changed files with 3862 additions and 2194786 deletions

View File

@@ -1,9 +1,11 @@
import { useEffect, useRef, useCallback, useState } from 'react';
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,
import {
defaultNetworkInfo,
discoverHosts,
convertToTopology,
DiscoveredHost
} from './services/discovery';
@@ -14,6 +16,7 @@ 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';
@@ -33,10 +36,10 @@ interface ApiDiscoveryResponse {
}
function App() {
const {
setNodes,
setEdges,
setNetworkInfo,
const {
setNodes,
setEdges,
setNetworkInfo,
setHosts,
setLastUpdated,
setIsLoading,
@@ -44,15 +47,39 @@ function App() {
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,
toggleCommandPalette,
terminalOpen,
terminalHost,
closeTerminal
} = useTopologyStore();
} = 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;
@@ -61,18 +88,18 @@ function App() {
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,
@@ -87,23 +114,23 @@ function App() {
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',
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 === '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);
@@ -118,21 +145,21 @@ function App() {
} 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',
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 === '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);
@@ -165,26 +192,79 @@ function App() {
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 (
<ReactFlowProvider>
<div className="h-screen w-screen flex flex-col bg-slate-900">
{/* Skip link for accessibility */}
<a href="#main-content" className="skip-link">
Skip to main content
</a>
<StaleWarning />
<Header onRefresh={loadData} isLoading={isLoading} />
<div className="flex-1 flex overflow-hidden">
<MetricsBar />
<div className="flex-1 flex overflow-hidden" role="main" id="main-content" tabIndex={-1}>
{leftPanelOpen && (
<LeftPanel />
)}
<div className="flex-1">
<TopologyGraph />
</div>
{rightPanelOpen && (
<RightPanel />
)}
</div>
<Footer />
<CommandPalette />
{terminalOpen && terminalHost && (
@@ -195,12 +275,19 @@ function App() {
);
}
function Footer() {
const { lastUpdated, nodes, dataSource, pollInterval } = useTopologyStore();
const Footer = memo(function Footer() {
const { lastUpdated, nodes, dataSource, pollInterval } = useTopologyStore(
useShallow((s) => ({
lastUpdated: s.lastUpdated,
nodes: s.nodes,
dataSource: s.dataSource,
pollInterval: s.pollInterval,
}))
);
const [countdown, setCountdown] = useState(Math.ceil(pollInterval / 1000));
const pollIntervalRef = useRef(pollInterval);
pollIntervalRef.current = pollInterval;
const formatTime = (date: Date | null) => {
if (!date) return 'Never';
return date.toLocaleTimeString();
@@ -208,19 +295,23 @@ function Footer() {
useEffect(() => {
setCountdown(Math.ceil(pollInterval / 1000));
const intervalId = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) return Math.ceil(pollIntervalRef.current / 1000);
return prev - 1;
});
}, 1000);
return () => clearInterval(intervalId);
}, [lastUpdated, pollInterval]);
return (
<div className="h-8 bg-slate-800 border-t border-slate-700 px-4 flex items-center justify-between text-xs text-slate-400">
<div
className="h-8 bg-slate-800 border-t border-slate-700 px-4 flex items-center justify-between text-xs text-slate-400"
aria-live="polite"
aria-atomic="true"
>
<span>Nodes: {nodes.length}</span>
<div className="flex items-center gap-4">
<span className={`px-2 py-0.5 rounded ${dataSource === 'live' ? 'bg-green-900 text-green-400' : 'bg-yellow-900 text-yellow-400'}`}>
@@ -231,6 +322,6 @@ function Footer() {
</div>
</div>
);
}
});
export default App;