fix: reduce font size and compact all views to fit screen
Base font: clamp(10px, 0.75vw, 16px) — much smaller scaling. At 1920px viewport = ~14px, at 3840px = 16px (capped). All components heavily compacted: - Header: minimal padding, smaller text - Topology: tight InfraCards, small VmPills, 3-col VM grid - Kanban: 3-col project grid, compact cards - Calendar: tighter spacing - Summary bars: single-line, text-xs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,76 +16,50 @@ export function CalendarView({ events }: CalendarViewProps) {
|
|||||||
(e) => !e.start.startsWith(today) && !e.start.startsWith(tomorrow)
|
(e) => !e.start.startsWith(today) && !e.start.startsWith(tomorrow)
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatDate = (dateStr: string) => {
|
const fmt = (d: string) =>
|
||||||
return new Date(dateStr).toLocaleDateString("es-MX", {
|
new Date(d).toLocaleDateString("es-MX", { weekday: "long", month: "long", day: "numeric" });
|
||||||
weekday: "long",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasEvents = events.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex items-center gap-6 px-8 py-2 bg-bg-secondary border-b border-border shrink-0">
|
<div className="flex items-center gap-4 px-6 py-1.5 bg-bg-secondary border-b border-border shrink-0 text-xs">
|
||||||
<span className="text-base text-text-secondary">
|
<span className="text-text-secondary">
|
||||||
<span className="font-bold text-text-primary">{events.length}</span> eventos esta semana
|
<span className="font-bold text-text-primary">{events.length}</span> eventos esta semana
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||||
<div className="flex-1 overflow-y-auto px-8 py-6">
|
{events.length === 0 ? (
|
||||||
{!hasEvents ? (
|
<div className="flex flex-col items-center justify-center h-full gap-3">
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-4">
|
<span className="text-5xl opacity-30">📅</span>
|
||||||
<span className="text-6xl opacity-30">📅</span>
|
<p className="text-xl font-semibold text-text-secondary">Sin eventos programados</p>
|
||||||
<p className="text-2xl font-semibold text-text-secondary">
|
<p className="text-base text-text-muted">Los eventos de Odoo aparecerán aquí</p>
|
||||||
Sin eventos programados
|
|
||||||
</p>
|
|
||||||
<p className="text-lg text-text-muted">
|
|
||||||
Los próximos eventos del calendario de Odoo aparecerán aquí
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-3 gap-8 h-full">
|
<div className="grid grid-cols-3 gap-6 h-full">
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-xl font-bold text-text-primary mb-1">Hoy</h2>
|
<h2 className="text-lg font-bold text-text-primary mb-1">Hoy</h2>
|
||||||
<p className="text-sm text-text-muted capitalize mb-4">{formatDate(today)}</p>
|
<p className="text-xs text-text-muted capitalize mb-3">{fmt(today)}</p>
|
||||||
{todayEvents.length === 0 ? (
|
{todayEvents.length === 0
|
||||||
<p className="text-base text-text-secondary">Sin eventos</p>
|
? <p className="text-sm text-text-secondary">Sin eventos</p>
|
||||||
) : (
|
: <div className="space-y-2">{todayEvents.map((e) => <EventCard key={e.id} event={e} />)}</div>}
|
||||||
<div className="space-y-3">
|
|
||||||
{todayEvents.map((e) => <EventCard key={e.id} event={e} />)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-xl font-bold text-text-primary mb-1">Mañana</h2>
|
<h2 className="text-lg font-bold text-text-primary mb-1">Mañana</h2>
|
||||||
<p className="text-sm text-text-muted capitalize mb-4">{formatDate(tomorrow)}</p>
|
<p className="text-xs text-text-muted capitalize mb-3">{fmt(tomorrow)}</p>
|
||||||
{tomorrowEvents.length === 0 ? (
|
{tomorrowEvents.length === 0
|
||||||
<p className="text-base text-text-secondary">Sin eventos</p>
|
? <p className="text-sm text-text-secondary">Sin eventos</p>
|
||||||
) : (
|
: <div className="space-y-2">{tomorrowEvents.map((e) => <EventCard key={e.id} event={e} />)}</div>}
|
||||||
<div className="space-y-3">
|
|
||||||
{tomorrowEvents.map((e) => <EventCard key={e.id} event={e} />)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-xl font-bold text-text-primary mb-1">Esta semana</h2>
|
<h2 className="text-lg font-bold text-text-primary mb-1">Esta semana</h2>
|
||||||
<p className="text-sm text-text-muted mb-4">Próximos días</p>
|
<p className="text-xs text-text-muted mb-3">Próximos días</p>
|
||||||
{laterEvents.length === 0 ? (
|
{laterEvents.length === 0
|
||||||
<p className="text-base text-text-secondary">Sin eventos</p>
|
? <p className="text-sm text-text-secondary">Sin eventos</p>
|
||||||
) : (
|
: <div className="space-y-1.5">{laterEvents.map((e) => (
|
||||||
<div className="space-y-2">
|
<div key={e.id} className="flex gap-2 text-xs bg-bg-card rounded px-2 py-1.5 border border-border">
|
||||||
{laterEvents.map((e) => (
|
<span className="font-mono text-text-muted capitalize">{fmt(e.start)}</span>
|
||||||
<div key={e.id} className="flex gap-3 text-sm bg-bg-card rounded-lg px-3 py-2 border border-border">
|
|
||||||
<span className="font-mono text-text-muted min-w-[10rem] capitalize">
|
|
||||||
{formatDate(e.start)}
|
|
||||||
</span>
|
|
||||||
<span className="text-text-primary truncate">{e.name}</span>
|
<span className="text-text-primary truncate">{e.name}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}</div>}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,25 +27,18 @@ export function Header({ viewName, connected }: HeaderProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="flex items-center justify-between px-8 py-3 bg-bg-secondary border-b border-border shrink-0">
|
<header className="flex items-center justify-between px-6 py-2 bg-bg-secondary border-b border-border shrink-0">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-3">
|
||||||
<h1 className="text-2xl font-bold tracking-tight text-text-primary">
|
<h1 className="text-xl font-bold tracking-tight text-text-primary">Consultoria AS</h1>
|
||||||
Consultoria AS
|
|
||||||
</h1>
|
|
||||||
<span className="text-text-muted">|</span>
|
<span className="text-text-muted">|</span>
|
||||||
<span className="text-lg font-medium text-accent">{viewName}</span>
|
<span className="text-base font-medium text-accent">{viewName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-base text-text-secondary capitalize">{dateStr}</span>
|
<span className="text-sm text-text-secondary capitalize">{dateStr}</span>
|
||||||
<span className="text-2xl font-mono font-bold tabular-nums">{timeStr}</span>
|
<span className="text-xl font-mono font-bold tabular-nums">{timeStr}</span>
|
||||||
<div className="flex items-center gap-2">
|
<span
|
||||||
<span
|
className={`w-2.5 h-2.5 rounded-full ${connected ? "bg-success animate-pulse" : "bg-danger"}`}
|
||||||
className={`w-3 h-3 rounded-full ${connected ? "bg-success animate-pulse" : "bg-danger"}`}
|
/>
|
||||||
/>
|
|
||||||
<span className="text-xs text-text-secondary">
|
|
||||||
{connected ? "Conectado" : "Sin conexión"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,57 +5,37 @@ interface KanbanBoardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STAGE_COLORS = [
|
const STAGE_COLORS = [
|
||||||
"bg-blue-500",
|
"bg-blue-500", "bg-emerald-500", "bg-amber-500", "bg-purple-500",
|
||||||
"bg-emerald-500",
|
"bg-rose-500", "bg-cyan-500", "bg-orange-500", "bg-teal-500",
|
||||||
"bg-amber-500",
|
|
||||||
"bg-purple-500",
|
|
||||||
"bg-rose-500",
|
|
||||||
"bg-cyan-500",
|
|
||||||
"bg-orange-500",
|
|
||||||
"bg-teal-500",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function ProjectRow({ project }: { project: Project }) {
|
function ProjectRow({ project }: { project: Project }) {
|
||||||
const stages = Object.entries(project.stages);
|
const stages = Object.entries(project.stages);
|
||||||
const totalTasks = stages.reduce((sum, [, tasks]) => sum + tasks.length, 0);
|
const totalTasks = stages.reduce((sum, [, tasks]) => sum + tasks.length, 0);
|
||||||
|
|
||||||
if (totalTasks === 0) return null;
|
if (totalTasks === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-bg-card border border-border rounded-xl p-4 flex flex-col gap-2.5">
|
<div className="bg-bg-card border border-border rounded-lg p-3 flex flex-col gap-1.5">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h3 className="text-lg font-bold text-text-primary leading-snug">
|
<h3 className="text-sm font-bold text-text-primary leading-tight truncate">
|
||||||
{project.name}
|
{project.name}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="text-sm font-mono text-text-secondary shrink-0">
|
<span className="text-xs font-mono text-text-muted shrink-0">{totalTasks}</span>
|
||||||
{totalTasks}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex h-2 rounded-full overflow-hidden bg-bg-primary">
|
||||||
<div className="flex h-3 rounded-full overflow-hidden bg-bg-primary">
|
{stages.map(([name, tasks], i) => {
|
||||||
{stages.map(([stageName, tasks], i) => {
|
|
||||||
const pct = (tasks.length / totalTasks) * 100;
|
const pct = (tasks.length / totalTasks) * 100;
|
||||||
if (pct < 1) return null;
|
if (pct < 1) return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={name} className={STAGE_COLORS[i % STAGE_COLORS.length]} style={{ width: `${pct}%` }} />
|
||||||
key={stageName}
|
|
||||||
className={`${STAGE_COLORS[i % STAGE_COLORS.length]}`}
|
|
||||||
style={{ width: `${pct}%` }}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
<div className="flex flex-wrap gap-1.5">
|
{stages.map(([name, tasks], i) => (
|
||||||
{stages.map(([stageName, tasks], i) => (
|
<span key={name} className="inline-flex items-center gap-1 text-xs text-text-secondary bg-bg-secondary rounded px-1.5 py-0.5">
|
||||||
<span
|
<span className={`w-1.5 h-1.5 rounded-full ${STAGE_COLORS[i % STAGE_COLORS.length]}`} />
|
||||||
key={stageName}
|
<span className="truncate max-w-[10rem]">{name}</span>
|
||||||
className="inline-flex items-center gap-1.5 text-sm text-text-secondary bg-bg-secondary rounded-md px-2.5 py-1"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`w-2 h-2 rounded-full shrink-0 ${STAGE_COLORS[i % STAGE_COLORS.length]}`}
|
|
||||||
/>
|
|
||||||
<span className="truncate max-w-[12rem]">{stageName}</span>
|
|
||||||
<span className="font-mono font-bold text-text-primary">{tasks.length}</span>
|
<span className="font-mono font-bold text-text-primary">{tasks.length}</span>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -65,33 +45,27 @@ function ProjectRow({ project }: { project: Project }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function KanbanBoard({ projects }: KanbanBoardProps) {
|
export function KanbanBoard({ projects }: KanbanBoardProps) {
|
||||||
const activeProjects = projects.filter((p) => {
|
const active = projects.filter((p) =>
|
||||||
const total = Object.values(p.stages).reduce((s, t) => s + t.length, 0);
|
Object.values(p.stages).reduce((s, t) => s + t.length, 0) > 0
|
||||||
return total > 0;
|
);
|
||||||
});
|
const total = active.reduce(
|
||||||
|
(sum, p) => sum + Object.values(p.stages).reduce((s, t) => s + t.length, 0), 0
|
||||||
const totalTasks = activeProjects.reduce(
|
|
||||||
(sum, p) => sum + Object.values(p.stages).reduce((s, t) => s + t.length, 0),
|
|
||||||
0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex items-center gap-6 px-8 py-2 bg-bg-secondary border-b border-border shrink-0">
|
<div className="flex items-center gap-4 px-6 py-1.5 bg-bg-secondary border-b border-border shrink-0 text-xs">
|
||||||
<span className="text-base text-text-secondary">
|
<span className="text-text-secondary">
|
||||||
<span className="font-bold text-text-primary">{activeProjects.length}</span> proyectos
|
<span className="font-bold text-text-primary">{active.length}</span> proyectos
|
||||||
</span>
|
</span>
|
||||||
<span className="text-border-light">|</span>
|
<span className="text-border-light">|</span>
|
||||||
<span className="text-base text-text-secondary">
|
<span className="text-text-secondary">
|
||||||
<span className="font-bold text-text-primary">{totalTasks}</span> tareas
|
<span className="font-bold text-text-primary">{total}</span> tareas
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 py-3">
|
||||||
<div className="flex-1 overflow-y-auto px-8 py-4">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
<div className="grid grid-cols-3 gap-4">
|
{active.map((p) => <ProjectRow key={p.id} project={p} />)}
|
||||||
{activeProjects.map((project) => (
|
|
||||||
<ProjectRow key={project.id} project={project} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,171 +11,137 @@ const ICON_MAP: Record<string, string> = {
|
|||||||
phone: "📞", camera: "📷", device: "📱",
|
phone: "📞", camera: "📷", device: "📱",
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── Compact VM pill ─────────────────────────────────────── */
|
|
||||||
function VmPill({ node }: { node: NetworkNode }) {
|
function VmPill({ node }: { node: NetworkNode }) {
|
||||||
const isUp = node.status === "up";
|
const dot = node.status === "up" ? "bg-success" : "bg-danger";
|
||||||
const dotColor = isUp ? "bg-success" : "bg-danger";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5 bg-bg-card border border-border rounded-md px-2 py-1">
|
<div className="flex items-center gap-1 bg-bg-card/80 border border-border rounded px-1.5 py-0.5">
|
||||||
<span className={`w-2 h-2 rounded-full ${dotColor} shrink-0`} />
|
<span className={`w-1.5 h-1.5 rounded-full ${dot} shrink-0`} />
|
||||||
<span className="text-sm font-medium text-text-primary truncate">{node.name}</span>
|
<span className="text-xs text-text-primary truncate">{node.name}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Infrastructure card ─────────────────────────────────── */
|
|
||||||
function InfraCard({ node }: { node: NetworkNode }) {
|
function InfraCard({ node }: { node: NetworkNode }) {
|
||||||
const [showPass, setShowPass] = useState(false);
|
const [showPass, setShowPass] = useState(false);
|
||||||
const isUp = node.status === "up";
|
const isUp = node.status === "up";
|
||||||
const borderColor = isUp ? "border-success/40" : "border-danger/40";
|
const border = isUp ? "border-success/30" : "border-danger/30";
|
||||||
const bgColor = isUp ? "bg-success-dim" : "bg-danger-dim";
|
const bg = isUp ? "bg-success-dim" : "bg-danger-dim";
|
||||||
const dotColor = isUp ? "bg-success" : "bg-danger";
|
const dot = isUp ? "bg-success" : "bg-danger";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${bgColor} border ${borderColor} rounded-xl px-4 py-3 cursor-pointer transition-all hover:brightness-110`}
|
className={`${bg} border ${border} rounded-lg px-3 py-2 cursor-pointer hover:brightness-110 transition-all`}
|
||||||
onClick={() => setShowPass((p) => !p)}
|
onClick={() => setShowPass((p) => !p)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-2xl">{ICON_MAP[node.icon] || "📦"}</span>
|
<span className="text-lg">{ICON_MAP[node.icon] || "📦"}</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-lg font-bold text-text-primary leading-tight">{node.name}</p>
|
<p className="text-sm font-bold text-text-primary leading-none">{node.name}</p>
|
||||||
<p className="text-sm font-mono text-text-secondary">{node.ip}</p>
|
<p className="text-xs font-mono text-text-secondary leading-none mt-0.5">{node.ip}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className={`w-3 h-3 rounded-full ${dotColor} shrink-0`} />
|
<span className={`w-2.5 h-2.5 rounded-full ${dot} shrink-0`} />
|
||||||
</div>
|
</div>
|
||||||
{showPass && node.username && (
|
{showPass && node.username && (
|
||||||
<p className="text-xs text-text-secondary mt-1">
|
<p className="text-xs text-text-muted mt-1">
|
||||||
{node.username}
|
{node.username}
|
||||||
{node.password && <span className="font-mono text-warning"> / {node.password}</span>}
|
{node.password && <span className="text-warning font-mono"> / {node.password}</span>}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{showPass && node.public_url && (
|
{showPass && node.public_url && (
|
||||||
<p className="text-xs text-accent mt-0.5">{node.public_url}</p>
|
<p className="text-xs text-accent mt-0.5 truncate">{node.public_url}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Proxmox server column ───────────────────────────────── */
|
function ProxmoxCol({ server, vms }: { server: NetworkNode; vms: NetworkNode[] }) {
|
||||||
function ProxmoxColumn({ server, vms }: { server: NetworkNode; vms: NetworkNode[] }) {
|
const up = vms.filter((v) => v.status === "up").length;
|
||||||
const upCount = vms.filter((v) => v.status === "up").length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center flex-1">
|
||||||
|
<div className="w-px h-3 bg-border-light" />
|
||||||
<InfraCard node={server} />
|
<InfraCard node={server} />
|
||||||
<div className="w-px h-4 bg-border-light" />
|
<div className="w-px h-2 bg-border-light" />
|
||||||
<span className="text-xs text-text-muted mb-2">
|
<span className="text-xs text-text-muted mb-1">{up}/{vms.length}</span>
|
||||||
{upCount}/{vms.length} activos
|
<div className="grid grid-cols-3 gap-1 w-full">
|
||||||
</span>
|
{vms.map((vm) => <VmPill key={vm.ip + vm.name} node={vm} />)}
|
||||||
<div className="grid grid-cols-3 gap-1.5 w-full">
|
|
||||||
{vms.map((vm) => (
|
|
||||||
<VmPill key={vm.ip + vm.name} node={vm} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Vertical line ───────────────────────────────────────── */
|
|
||||||
function VLine() {
|
|
||||||
return <div className="w-px h-5 bg-border-light mx-auto" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Main topology ───────────────────────────────────────── */
|
|
||||||
export function NetworkGraph({ nodes }: NetworkGraphProps) {
|
export function NetworkGraph({ nodes }: NetworkGraphProps) {
|
||||||
const findByName = (name: string) => nodes.find((n) => n.name === name);
|
const find = (name: string) => nodes.find((n) => n.name === name);
|
||||||
const modem = findByName("Router Telmex");
|
const modem = find("Router Telmex");
|
||||||
const firewall = findByName("Firewall OPNsense");
|
const firewall = find("Firewall OPNsense");
|
||||||
const switchNode = findByName("Switch Cisco");
|
const sw = find("Switch Cisco");
|
||||||
|
|
||||||
const proxmoxServers = nodes.filter((n) => n.type === "proxmox");
|
const pve = nodes.filter((n) => n.type === "proxmox");
|
||||||
const otherDevices = nodes.filter(
|
const vms = new Map<string, NetworkNode[]>();
|
||||||
(n) =>
|
for (const n of nodes) {
|
||||||
!n.type &&
|
if ((n.type === "vm" || n.type === "ct") && n.parent) {
|
||||||
n.name !== "Router Telmex" &&
|
const list = vms.get(n.parent) || [];
|
||||||
n.name !== "Firewall OPNsense" &&
|
list.push(n);
|
||||||
n.name !== "Switch Cisco"
|
vms.set(n.parent, list);
|
||||||
);
|
|
||||||
|
|
||||||
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 other = nodes.filter(
|
||||||
|
(n) => !n.type && !["Router Telmex", "Firewall OPNsense", "Switch Cisco"].includes(n.name)
|
||||||
|
);
|
||||||
|
|
||||||
const onlineCount = nodes.filter((n) => n.status === "up").length;
|
const online = nodes.filter((n) => n.status === "up").length;
|
||||||
const total = nodes.length;
|
const vmTotal = nodes.filter((n) => n.type === "vm" || n.type === "ct").length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Summary bar */}
|
{/* Stats */}
|
||||||
<div className="flex items-center gap-6 px-8 py-2 bg-bg-secondary border-b border-border">
|
<div className="flex items-center gap-4 px-6 py-1.5 bg-bg-secondary border-b border-border shrink-0 text-xs">
|
||||||
<div className="flex items-center gap-2">
|
<span className="flex items-center gap-1.5">
|
||||||
<span className="w-3 h-3 rounded-full bg-success" />
|
<span className="w-2 h-2 rounded-full bg-success" />
|
||||||
<span className="text-base text-text-secondary">
|
<span className="font-bold text-text-primary">{online}</span>
|
||||||
<span className="font-bold text-text-primary">{onlineCount}</span> online
|
<span className="text-text-secondary">online</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span className="flex items-center gap-1.5">
|
||||||
<div className="flex items-center gap-2">
|
<span className="w-2 h-2 rounded-full bg-danger" />
|
||||||
<span className="w-3 h-3 rounded-full bg-danger" />
|
<span className="font-bold text-text-primary">{nodes.length - online}</span>
|
||||||
<span className="text-base text-text-secondary">
|
<span className="text-text-secondary">offline</span>
|
||||||
<span className="font-bold text-text-primary">{total - onlineCount}</span> offline
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-border-light">|</span>
|
<span className="text-border-light">|</span>
|
||||||
<span className="text-base text-text-secondary">
|
<span className="text-text-secondary">
|
||||||
<span className="font-bold text-text-primary">{proxmoxServers.length}</span> Proxmox ·{" "}
|
<span className="font-bold text-text-primary">{pve.length}</span> Proxmox ·{" "}
|
||||||
<span className="font-bold text-text-primary">
|
<span className="font-bold text-text-primary">{vmTotal}</span> VMs/CTs
|
||||||
{nodes.filter((n) => n.type === "vm" || n.type === "ct").length}
|
|
||||||
</span>{" "}
|
|
||||||
VMs/CTs
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Diagram */}
|
{/* Tree */}
|
||||||
<div className="flex-1 overflow-y-auto flex flex-col items-center px-6 py-4">
|
<div className="flex-1 flex flex-col items-center justify-start px-4 py-3 overflow-hidden">
|
||||||
{/* Top chain: Modem → Firewall → Switch */}
|
{/* Top chain */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
{modem && <InfraCard node={modem} />}
|
{modem && <InfraCard node={modem} />}
|
||||||
<span className="text-border-light text-xl">→</span>
|
<span className="text-border-light text-base">→</span>
|
||||||
{firewall && <InfraCard node={firewall} />}
|
{firewall && <InfraCard node={firewall} />}
|
||||||
<span className="text-border-light text-xl">→</span>
|
<span className="text-border-light text-base">→</span>
|
||||||
{switchNode && <InfraCard node={switchNode} />}
|
{sw && <InfraCard node={sw} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VLine />
|
{/* Branch down */}
|
||||||
|
<div className="w-px h-3 bg-border-light" />
|
||||||
{/* Horizontal branch line */}
|
<div className="h-px bg-border-light" style={{ width: "80%" }} />
|
||||||
<div className="w-3/4 h-px bg-border-light" />
|
|
||||||
|
|
||||||
{/* Proxmox columns */}
|
{/* Proxmox columns */}
|
||||||
<div className="flex gap-6 w-full px-4 mt-0">
|
<div className="flex gap-4 w-full">
|
||||||
{proxmoxServers.map((server) => {
|
{pve.map((s) => (
|
||||||
const vms = vmsByParent.get(server.name) || [];
|
<ProxmoxCol key={s.ip} server={s} vms={vms.get(s.name) || []} />
|
||||||
return (
|
))}
|
||||||
<div key={server.ip} className="flex-1 flex flex-col items-center">
|
|
||||||
<div className="w-px h-4 bg-border-light" />
|
|
||||||
<ProxmoxColumn server={server} vms={vms} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Other devices */}
|
{/* Others */}
|
||||||
{otherDevices.length > 0 && (
|
{other.length > 0 && (
|
||||||
<div className="mt-4 w-full">
|
<div className="mt-3 w-full">
|
||||||
<p className="text-xs font-bold text-text-muted uppercase tracking-widest mb-2 text-center">
|
<p className="text-xs text-text-muted uppercase tracking-wider text-center mb-1.5">
|
||||||
Otros dispositivos
|
Otros dispositivos
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center gap-3 flex-wrap">
|
<div className="flex justify-center gap-2 flex-wrap">
|
||||||
{otherDevices.map((node) => (
|
{other.map((n) => <InfraCard key={n.ip} node={n} />)}
|
||||||
<InfraCard key={node.ip} node={node} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: clamp(14px, 1.15vw, 24px);
|
/* Scales with viewport: ~14px at 1920w, ~16px at 3840w, min 10px */
|
||||||
|
font-size: clamp(10px, 0.75vw, 16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -35,12 +36,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: var(--color-border);
|
background: var(--color-border);
|
||||||
border-radius: 3px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user