feat: hierarchical tree topology diagram

Replace grid layout with tree diagram:
Modem → Firewall → Switch → 3 Proxmox servers → VMs/CTs

- Services.yaml restructured with parent/type fields
  from real Proxmox API data (TYAN CAS, Cisco1, DellT430-2)
- NetworkGraph renders vertical hierarchy with branch lines
- ProxmoxColumn shows server card + VM pills grid
- Compact VmPill for VMs/CTs (status dot + name + last octet)
- InfraCard for physical infrastructure nodes
- Other devices section at bottom (AP, printer, iDRACs, PCs)
- Added type/parent fields to NetworkNode TypeScript interface

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 01:44:50 +00:00
parent 4363527c58
commit e3e210a9dc
3 changed files with 500 additions and 385 deletions

View File

@@ -1,26 +1,28 @@
# ============================================================
# Topología de Red - Consultoria AS
# Edita este archivo para agregar credenciales y URLs públicas
# Jerarquía: Modem → Firewall → Switch → Proxmox → VMs
# ============================================================
nodes:
# ── Infraestructura de Red ──────────────────────────────────
- name: "Firewall OPNsense"
ip: "192.168.10.1"
username: "root"
password: "opnsense"
public_url: "https://192.168.10.1:8443"
icon: "firewall"
connections: []
# ── Nivel 1: ISP ───────────────────────────────────────────
- name: "Router Telmex"
ip: "192.168.1.254"
username: "TELMEX"
password: ""
public_url: "http://192.168.1.254"
icon: "router"
connections: ["Firewall OPNsense"]
connections: []
# ── Nivel 2: Firewall ──────────────────────────────────────
- name: "Firewall OPNsense"
ip: "192.168.10.1"
username: "root"
password: "opnsense"
public_url: "https://192.168.10.1:8443"
icon: "firewall"
connections: ["Router Telmex"]
# ── Nivel 3: Switch ────────────────────────────────────────
- name: "Switch Cisco"
ip: "192.168.10.250"
username: ""
@@ -28,355 +30,335 @@ nodes:
icon: "switch"
connections: ["Firewall OPNsense"]
- name: "Switch Mellanox"
# ── Nivel 4: Proxmox Servers ───────────────────────────────
- name: "TYAN CAS"
ip: "192.168.10.3"
username: ""
password: ""
icon: "switch"
connections: ["Firewall OPNsense"]
- name: "Access Point EAP610"
ip: "192.168.10.166"
username: ""
password: ""
icon: "ap"
connections: ["Firewall OPNsense"]
# ── Servidores Dell (iDRAC) ─────────────────────────────────
- name: "iDRAC Servidor 1"
ip: "192.168.10.159"
username: ""
password: ""
username: "root"
password: "Aasi940812"
public_url: "https://192.168.10.3:8006"
icon: "server"
type: "proxmox"
connections: ["Switch Cisco"]
- name: "iDRAC Servidor 2"
ip: "192.168.10.160"
username: ""
password: ""
icon: "server"
connections: ["Switch Cisco"]
- name: "Dell Server .185"
- name: "Cisco1"
ip: "192.168.10.185"
username: ""
password: ""
username: "root"
password: "Aasi940812"
public_url: "https://192.168.10.185:8006"
icon: "server"
type: "proxmox"
connections: ["Switch Cisco"]
- name: "Dell Server .187"
- name: "DellT430-2"
ip: "192.168.10.187"
username: ""
password: ""
username: "root"
password: "Aasi940812"
public_url: "https://192.168.10.187:8006"
icon: "server"
type: "proxmox"
connections: ["Switch Cisco"]
# ── Servicios Principales ───────────────────────────────────
- name: "Servidor Odoo"
# ── VMs de TYAN CAS ────────────────────────────────────────
- name: "OMV"
ip: "192.168.10.5"
icon: "nas"
type: "vm"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "NocoDB"
ip: "192.168.10.134"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "Dashy"
ip: "192.168.10.8"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "Paperless-NGX"
ip: "192.168.10.9"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "Immich"
ip: "192.168.10.10"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "Debian"
ip: "192.168.10.148"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "Dockge"
ip: "192.168.10.8"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "BookLore"
ip: "192.168.10.205"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "n8n"
ip: "192.168.10.14"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "Navidrome"
ip: "192.168.10.202"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "Uptime Kuma"
ip: "192.168.10.16"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "Metabase"
ip: "192.168.10.142"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "Gitea"
ip: "192.168.10.150"
public_url: "https://git.consultoria-as.com"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "AMP"
ip: "192.168.10.151"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "NodeBB"
ip: "192.168.10.191"
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
# ── VMs de Cisco1 ──────────────────────────────────────────
- name: "Ubuntu"
ip: "192.168.10.182"
icon: "device"
type: "vm"
parent: "Cisco1"
connections: ["Cisco1"]
- name: "Tilmatli"
ip: "192.168.10.119"
icon: "device"
type: "vm"
parent: "Cisco1"
connections: ["Cisco1"]
- name: "WhatsappBot Clawbot"
ip: "192.168.10.68"
icon: "device"
type: "vm"
parent: "Cisco1"
connections: ["Cisco1"]
- name: "Ubuntu CT"
ip: "192.168.10.182"
icon: "device"
type: "ct"
parent: "Cisco1"
connections: ["Cisco1"]
- name: "PostgreSQL"
ip: "192.168.10.71"
icon: "device"
type: "ct"
parent: "Cisco1"
connections: ["Cisco1"]
# ── VMs de DellT430-2 ─────────────────────────────────────
- name: "JobHero"
ip: "192.168.10.197"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "Autopartes"
ip: "192.168.10.200"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "ProksBot"
ip: "192.168.10.204"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "CAS PaginasWeb"
ip: "192.168.10.211"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "SIO Mexus"
ip: "192.168.10.207"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "Horux"
ip: "192.168.10.212"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "ATLAS GPS"
ip: "192.168.10.216"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "Trivy"
ip: "192.168.10.217"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "Social Automation"
ip: "192.168.10.218"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "Padel"
ip: "192.168.10.219"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "MSP"
ip: "192.168.10.223"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "VoIP"
ip: "192.168.10.228"
icon: "phone"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "Sistema Hotel"
ip: "192.168.10.229"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "Dashboard TV"
ip: "192.168.10.230"
icon: "device"
type: "vm"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "HoruxDB"
ip: "192.168.10.208"
icon: "device"
type: "ct"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "Odoo"
ip: "192.168.10.188"
username: "root"
password: "Aasi940812"
public_url: "https://crm.consultoria-as.com"
icon: "server"
connections: ["Firewall OPNsense"]
icon: "device"
type: "ct"
parent: "DellT430-2"
connections: ["DellT430-2"]
- name: "Servidor Multimedia (Jellyfin)"
- name: "NodeBB 2"
ip: "192.168.10.192"
icon: "device"
type: "ct"
parent: "DellT430-2"
connections: ["DellT430-2"]
# ── Otros dispositivos (no Proxmox) ────────────────────────
- name: "Access Point EAP610"
ip: "192.168.10.166"
icon: "ap"
connections: ["Switch Cisco"]
- name: "Impresora Epson"
ip: "192.168.10.177"
icon: "printer"
connections: ["Switch Cisco"]
- name: "iDRAC TYAN"
ip: "192.168.10.159"
icon: "server"
connections: ["Switch Cisco"]
- name: "iDRAC Dell"
ip: "192.168.10.160"
icon: "server"
connections: ["Switch Cisco"]
- name: "Jellyfin"
ip: "192.168.10.103"
username: "consultoria-as"
password: "Aasi940812"
public_url: "https://jellyfin.consultoria-as.com"
icon: "server"
connections: ["Firewall OPNsense"]
icon: "device"
type: "ct"
parent: "TYAN CAS"
connections: ["TYAN CAS"]
- name: "Gitea"
ip: "192.168.10.150"
username: ""
password: ""
public_url: "https://git.consultoria-as.com"
icon: "server"
connections: ["Firewall OPNsense"]
- name: "PostgreSQL"
ip: "192.168.10.71"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "OpenMediaVault (NAS)"
ip: "192.168.10.5"
username: ""
password: ""
icon: "nas"
connections: ["Firewall OPNsense"]
# ── Monitoreo y Automatización ──────────────────────────────
- name: "Uptime Kuma"
ip: "192.168.10.16"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "n8n"
ip: "192.168.10.14"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Dashy"
ip: "192.168.10.8"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
# ── Gestión Documental y Datos ──────────────────────────────
- name: "Paperless-NGX"
ip: "192.168.10.9"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "NocoDB"
ip: "192.168.10.134"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Metabase"
ip: "192.168.10.142"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
# ── Multimedia y Contenido ──────────────────────────────────
- name: "Immich (Fotos)"
ip: "192.168.10.10"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Navidrome (Música)"
ip: "192.168.10.202"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "BookLore"
ip: "192.168.10.205"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
# ── Proyectos y Aplicaciones ────────────────────────────────
- name: "Hotel Production"
ip: "192.168.10.200"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Sistema Hotel"
ip: "192.168.10.229"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "HoruxDB"
ip: "192.168.10.208"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Adan Mesh Horux"
ip: "192.168.10.212"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "SIO Complete"
ip: "192.168.10.197"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "ATLAS GPS"
ip: "192.168.10.216"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "CAS Páginas Web"
ip: "192.168.10.211"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "AMP"
ip: "192.168.10.151"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
# ── Comunicaciones ──────────────────────────────────────────
- name: "NodeBB (Foro)"
ip: "192.168.10.191"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "NodeBB 2"
ip: "192.168.10.192"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "VoIP"
ip: "192.168.10.228"
username: ""
password: ""
icon: "phone"
connections: ["Firewall OPNsense"]
- name: "MSP"
ip: "192.168.10.223"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
# ── Servidores Auxiliares ────────────────────────────────────
- name: "Debian Server"
ip: "192.168.10.148"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Ubuntu Server .182"
ip: "192.168.10.182"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Test CAS"
ip: "192.168.10.119"
username: ""
password: ""
icon: "pc"
connections: ["Firewall OPNsense"]
- name: "Server .198"
ip: "192.168.10.198"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Ubuntu .204"
ip: "192.168.10.204"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Ubuntu .207"
ip: "192.168.10.207"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Ubuntu .217"
ip: "192.168.10.217"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Ubuntu .218"
ip: "192.168.10.218"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Ubuntu .219"
ip: "192.168.10.219"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Ubuntu .221"
ip: "192.168.10.221"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
- name: "Ubuntu .222"
ip: "192.168.10.222"
username: ""
password: ""
icon: "server"
connections: ["Firewall OPNsense"]
# ── Equipos de Escritorio ───────────────────────────────────
- name: "HP Consultoria-AS"
ip: "192.168.10.147"
username: ""
password: ""
icon: "pc"
connections: ["Firewall OPNsense"]
- name: "Desktop PC .57"
ip: "192.168.10.57"
username: ""
password: ""
icon: "pc"
connections: ["Firewall OPNsense"]
- name: "Desktop PC .143"
ip: "192.168.10.143"
username: ""
password: ""
icon: "pc"
connections: ["Firewall OPNsense"]
# ── Periféricos ─────────────────────────────────────────────
- name: "Impresora Epson"
ip: "192.168.10.177"
username: ""
password: ""
icon: "printer"
connections: ["Firewall OPNsense"]
# ── Dashboard ───────────────────────────────────────────────
- name: "Dashboard TV"
ip: "192.168.10.230"
username: ""
password: ""
icon: "device"
connections: ["Firewall OPNsense"]
connections: ["Switch Cisco"]
network_scan:
enabled: true

View File

@@ -1,50 +1,137 @@
import { NodeCard } from "./NodeCard";
import { useState } from "react";
import type { NetworkNode } from "../../types";
interface NetworkGraphProps {
nodes: NetworkNode[];
}
const CATEGORY_ORDER: [string, string[]][] = [
["Infraestructura", ["firewall", "router", "switch", "ap"]],
["Servidores", ["server"]],
["Almacenamiento", ["nas"]],
["Equipos", ["pc"]],
["Periféricos", ["printer", "phone", "camera"]],
["Otros", ["device"]],
];
const ICON_MAP: Record<string, string> = {
router: "🌐", firewall: "🛡️", server: "🖥️", switch: "🔀",
ap: "📡", pc: "💻", nas: "💾", printer: "🖨️",
phone: "📞", camera: "📷", device: "📱",
};
function categorizeNodes(nodes: NetworkNode[]) {
const categorized: { label: string; nodes: NetworkNode[] }[] = [];
const assigned = new Set<string>();
/* ── Compact VM pill ─────────────────────────────────────── */
function VmPill({ node }: { node: NetworkNode }) {
const isUp = node.status === "up";
const dotColor = isUp ? "bg-success" : "bg-danger";
for (const [label, icons] of CATEGORY_ORDER) {
const matching = nodes.filter(
(n) => icons.includes(n.icon) && !assigned.has(n.ip)
);
if (matching.length > 0) {
categorized.push({ label, nodes: matching });
for (const n of matching) assigned.add(n.ip);
return (
<div className="flex items-center gap-2 bg-bg-card border border-border rounded-lg px-3 py-2">
<span className={`w-2.5 h-2.5 rounded-full ${dotColor} shrink-0`} />
<span className="text-base font-medium text-text-primary truncate">{node.name}</span>
<span className="text-sm font-mono text-text-muted">{node.ip.split(".").pop()}</span>
</div>
);
}
/* ── Full node card (infrastructure + proxmox) ───────────── */
function InfraCard({ node, isCenter }: { node: NetworkNode; isCenter?: boolean }) {
const [showPass, setShowPass] = useState(false);
const isUp = node.status === "up";
const borderColor = isUp ? "border-success/40" : "border-danger/40";
const bgColor = isUp ? "bg-success-dim" : "bg-danger-dim";
const dotColor = isUp ? "bg-success" : "bg-danger";
return (
<div
className={`${bgColor} border-2 ${borderColor} rounded-2xl px-6 py-5 cursor-pointer transition-all hover:brightness-110 ${isCenter ? "min-w-[280px]" : ""}`}
onClick={() => setShowPass((p) => !p)}
>
<div className="flex items-center gap-3 mb-1">
<span className="text-3xl">{ICON_MAP[node.icon] || "📦"}</span>
<div className="flex-1 min-w-0">
<p className="text-xl font-bold text-text-primary leading-tight">{node.name}</p>
<p className="text-base font-mono text-text-secondary">{node.ip}</p>
</div>
<span className={`w-4 h-4 rounded-full ${dotColor} shrink-0`} />
</div>
{node.username && (
<p className="text-sm text-text-secondary mt-1">
{node.username}
{node.password && (
<span className="ml-1 font-mono text-warning">
{showPass ? ` / ${node.password}` : " / ••••••"}
</span>
)}
</p>
)}
{node.public_url && (
<p className="text-sm text-accent mt-0.5">{node.public_url}</p>
)}
</div>
);
}
/* ── Proxmox server with its VMs ─────────────────────────── */
function ProxmoxColumn({ server, vms }: { server: NetworkNode; vms: NetworkNode[] }) {
const vmCount = vms.length;
const upCount = vms.filter((v) => v.status === "up").length;
return (
<div className="flex flex-col items-center gap-0">
{/* Server card */}
<InfraCard node={server} />
{/* Vertical connector */}
<div className="w-0.5 h-6 bg-border-light" />
{/* VM count badge */}
<div className="flex items-center gap-2 bg-bg-secondary border border-border rounded-full px-4 py-1.5 mb-2">
<span className="text-sm text-text-muted">
{upCount}/{vmCount} VMs
</span>
</div>
{/* VM grid */}
<div className="grid grid-cols-2 gap-2 max-w-[520px]">
{vms.map((vm) => (
<VmPill key={vm.ip + vm.name} node={vm} />
))}
</div>
</div>
);
}
/* ── Vertical connector line ─────────────────────────────── */
function VLine() {
return <div className="w-0.5 h-8 bg-border-light mx-auto" />;
}
/* ── Main topology graph ─────────────────────────────────── */
export function NetworkGraph({ nodes }: NetworkGraphProps) {
// Find nodes by role
const findByName = (name: string) => nodes.find((n) => n.name === name);
const modem = findByName("Router Telmex");
const firewall = findByName("Firewall OPNsense");
const switchNode = findByName("Switch Cisco");
const proxmoxServers = nodes.filter((n) => n.type === "proxmox");
const otherDevices = nodes.filter(
(n) =>
!n.type &&
n.name !== "Router Telmex" &&
n.name !== "Firewall OPNsense" &&
n.name !== "Switch Cisco"
);
// Group VMs by parent
const vmsByParent = new Map<string, NetworkNode[]>();
for (const node of nodes) {
if ((node.type === "vm" || node.type === "ct") && node.parent) {
const list = vmsByParent.get(node.parent) || [];
list.push(node);
vmsByParent.set(node.parent, list);
}
}
const remaining = nodes.filter((n) => !assigned.has(n.ip));
if (remaining.length > 0) {
categorized.push({ label: "Otros", nodes: remaining });
}
return categorized;
}
export function NetworkGraph({ nodes }: NetworkGraphProps) {
const categories = categorizeNodes(nodes);
const onlineCount = nodes.filter((n) => n.status === "up").length;
const total = nodes.length;
return (
<div className="flex flex-col h-full">
{/* Summary bar */}
<div className="flex items-center gap-10 px-16 py-5 bg-bg-secondary border-b border-border">
<div className="flex items-center gap-10 px-16 py-4 bg-bg-secondary border-b border-border">
<div className="flex items-center gap-3">
<span className="w-4 h-4 rounded-full bg-success" />
<span className="text-xl text-text-secondary">
@@ -59,30 +146,74 @@ export function NetworkGraph({ nodes }: NetworkGraphProps) {
</div>
<span className="text-border-light text-2xl">|</span>
<span className="text-xl text-text-secondary">
<span className="font-bold text-text-primary">{total}</span> dispositivos
<span className="font-bold text-text-primary">{proxmoxServers.length}</span> Proxmox
<span className="mx-2">·</span>
<span className="font-bold text-text-primary">
{nodes.filter((n) => n.type === "vm" || n.type === "ct").length}
</span>{" "}
VMs/CTs
</span>
</div>
{/* Scrollable grid */}
<div className="flex-1 overflow-y-auto px-16 py-8 space-y-8">
{categories.map((cat) => (
<section key={cat.label}>
<h3 className="text-lg font-bold text-text-muted uppercase tracking-widest mb-4">
{cat.label}
<span className="ml-3 font-normal">({cat.nodes.length})</span>
</h3>
<div
className="grid gap-4"
style={{
gridTemplateColumns: "repeat(auto-fill, minmax(520px, 1fr))",
}}
>
{cat.nodes.map((node) => (
<NodeCard key={node.ip} node={node} />
))}
{/* Diagram area */}
<div className="flex-1 overflow-y-auto">
<div className="flex flex-col items-center py-8 px-8">
{/* ── Level 1: Modem ── */}
{modem && <InfraCard node={modem} isCenter />}
<VLine />
{/* ── Level 2: Firewall ── */}
{firewall && <InfraCard node={firewall} isCenter />}
<VLine />
{/* ── Level 3: Switch ── */}
{switchNode && <InfraCard node={switchNode} isCenter />}
<VLine />
{/* ── Branch lines to Proxmox servers ── */}
<div className="flex items-start justify-center w-full">
{/* Horizontal bar spanning all columns */}
<div className="relative flex justify-center" style={{ width: "100%", maxWidth: "3600px" }}>
{/* Horizontal line */}
<div
className="absolute top-0 h-0.5 bg-border-light"
style={{
left: `${100 / (proxmoxServers.length * 2)}%`,
right: `${100 / (proxmoxServers.length * 2)}%`,
}}
/>
{/* Proxmox columns */}
<div className="flex justify-center gap-8 w-full pt-0">
{proxmoxServers.map((server) => {
const vms = vmsByParent.get(server.name) || [];
return (
<div key={server.ip} className="flex-1 flex flex-col items-center max-w-[600px]">
{/* Vertical connector from horizontal bar */}
<div className="w-0.5 h-8 bg-border-light" />
<ProxmoxColumn server={server} vms={vms} />
</div>
);
})}
</div>
</div>
</section>
))}
</div>
{/* ── Other devices ── */}
{otherDevices.length > 0 && (
<div className="mt-8 w-full max-w-[3600px]">
<h3 className="text-base font-bold text-text-muted uppercase tracking-widest mb-4 text-center">
Otros dispositivos conectados al switch
</h3>
<div className="flex justify-center gap-4 flex-wrap">
{otherDevices.map((node) => (
<InfraCard key={node.ip} node={node} />
))}
</div>
</div>
)}
</div>
</div>
</div>
);

View File

@@ -7,6 +7,8 @@ export interface NetworkNode {
icon: string;
status: "up" | "down" | "unknown";
connections: string[];
type?: "proxmox" | "vm" | "ct" | string;
parent?: string;
auto_discovered?: boolean;
vendor?: string;
}