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:
2026-02-16 02:00:35 +00:00
parent c4065f2cce
commit 27907a2e39
5 changed files with 145 additions and 237 deletions

View File

@@ -16,76 +16,50 @@ export function CalendarView({ events }: CalendarViewProps) {
(e) => !e.start.startsWith(today) && !e.start.startsWith(tomorrow)
);
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString("es-MX", {
weekday: "long",
month: "long",
day: "numeric",
});
};
const hasEvents = events.length > 0;
const fmt = (d: string) =>
new Date(d).toLocaleDateString("es-MX", { weekday: "long", month: "long", day: "numeric" });
return (
<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">
<span className="text-base text-text-secondary">
<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-text-secondary">
<span className="font-bold text-text-primary">{events.length}</span> eventos esta semana
</span>
</div>
<div className="flex-1 overflow-y-auto px-8 py-6">
{!hasEvents ? (
<div className="flex flex-col items-center justify-center h-full gap-4">
<span className="text-6xl opacity-30">📅</span>
<p className="text-2xl font-semibold text-text-secondary">
Sin eventos programados
</p>
<p className="text-lg text-text-muted">
Los próximos eventos del calendario de Odoo aparecerán aquí
</p>
<div className="flex-1 overflow-y-auto px-6 py-4">
{events.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full gap-3">
<span className="text-5xl opacity-30">📅</span>
<p className="text-xl font-semibold text-text-secondary">Sin eventos programados</p>
<p className="text-base text-text-muted">Los eventos de Odoo aparecerán aquí</p>
</div>
) : (
<div className="grid grid-cols-3 gap-8 h-full">
<div className="grid grid-cols-3 gap-6 h-full">
<section>
<h2 className="text-xl font-bold text-text-primary mb-1">Hoy</h2>
<p className="text-sm text-text-muted capitalize mb-4">{formatDate(today)}</p>
{todayEvents.length === 0 ? (
<p className="text-base text-text-secondary">Sin eventos</p>
) : (
<div className="space-y-3">
{todayEvents.map((e) => <EventCard key={e.id} event={e} />)}
</div>
)}
<h2 className="text-lg font-bold text-text-primary mb-1">Hoy</h2>
<p className="text-xs text-text-muted capitalize mb-3">{fmt(today)}</p>
{todayEvents.length === 0
? <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>}
</section>
<section>
<h2 className="text-xl font-bold text-text-primary mb-1">Mañana</h2>
<p className="text-sm text-text-muted capitalize mb-4">{formatDate(tomorrow)}</p>
{tomorrowEvents.length === 0 ? (
<p className="text-base text-text-secondary">Sin eventos</p>
) : (
<div className="space-y-3">
{tomorrowEvents.map((e) => <EventCard key={e.id} event={e} />)}
</div>
)}
<h2 className="text-lg font-bold text-text-primary mb-1">Mañana</h2>
<p className="text-xs text-text-muted capitalize mb-3">{fmt(tomorrow)}</p>
{tomorrowEvents.length === 0
? <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>}
</section>
<section>
<h2 className="text-xl font-bold text-text-primary mb-1">Esta semana</h2>
<p className="text-sm text-text-muted mb-4">Próximos días</p>
{laterEvents.length === 0 ? (
<p className="text-base text-text-secondary">Sin eventos</p>
) : (
<div className="space-y-2">
{laterEvents.map((e) => (
<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>
<h2 className="text-lg font-bold text-text-primary mb-1">Esta semana</h2>
<p className="text-xs text-text-muted mb-3">Próximos días</p>
{laterEvents.length === 0
? <p className="text-sm text-text-secondary">Sin eventos</p>
: <div className="space-y-1.5">{laterEvents.map((e) => (
<div key={e.id} className="flex gap-2 text-xs bg-bg-card rounded px-2 py-1.5 border border-border">
<span className="font-mono text-text-muted capitalize">{fmt(e.start)}</span>
<span className="text-text-primary truncate">{e.name}</span>
</div>
))}
</div>
)}
))}</div>}
</section>
</div>
)}

View File

@@ -27,25 +27,18 @@ export function Header({ viewName, connected }: HeaderProps) {
});
return (
<header className="flex items-center justify-between px-8 py-3 bg-bg-secondary border-b border-border shrink-0">
<div className="flex items-center gap-4">
<h1 className="text-2xl font-bold tracking-tight text-text-primary">
Consultoria AS
</h1>
<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-3">
<h1 className="text-xl font-bold tracking-tight text-text-primary">Consultoria AS</h1>
<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 className="flex items-center gap-6">
<span className="text-base text-text-secondary capitalize">{dateStr}</span>
<span className="text-2xl font-mono font-bold tabular-nums">{timeStr}</span>
<div className="flex items-center gap-2">
<span
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 className="flex items-center gap-4">
<span className="text-sm text-text-secondary capitalize">{dateStr}</span>
<span className="text-xl font-mono font-bold tabular-nums">{timeStr}</span>
<span
className={`w-2.5 h-2.5 rounded-full ${connected ? "bg-success animate-pulse" : "bg-danger"}`}
/>
</div>
</header>
);

View File

@@ -5,57 +5,37 @@ interface KanbanBoardProps {
}
const STAGE_COLORS = [
"bg-blue-500",
"bg-emerald-500",
"bg-amber-500",
"bg-purple-500",
"bg-rose-500",
"bg-cyan-500",
"bg-orange-500",
"bg-teal-500",
"bg-blue-500", "bg-emerald-500", "bg-amber-500", "bg-purple-500",
"bg-rose-500", "bg-cyan-500", "bg-orange-500", "bg-teal-500",
];
function ProjectRow({ project }: { project: Project }) {
const stages = Object.entries(project.stages);
const totalTasks = stages.reduce((sum, [, tasks]) => sum + tasks.length, 0);
if (totalTasks === 0) return null;
return (
<div className="bg-bg-card border border-border rounded-xl p-4 flex flex-col gap-2.5">
<div className="flex items-center justify-between gap-3">
<h3 className="text-lg font-bold text-text-primary leading-snug">
<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-2">
<h3 className="text-sm font-bold text-text-primary leading-tight truncate">
{project.name}
</h3>
<span className="text-sm font-mono text-text-secondary shrink-0">
{totalTasks}
</span>
<span className="text-xs font-mono text-text-muted shrink-0">{totalTasks}</span>
</div>
<div className="flex h-3 rounded-full overflow-hidden bg-bg-primary">
{stages.map(([stageName, tasks], i) => {
<div className="flex h-2 rounded-full overflow-hidden bg-bg-primary">
{stages.map(([name, tasks], i) => {
const pct = (tasks.length / totalTasks) * 100;
if (pct < 1) return null;
return (
<div
key={stageName}
className={`${STAGE_COLORS[i % STAGE_COLORS.length]}`}
style={{ width: `${pct}%` }}
/>
<div key={name} className={STAGE_COLORS[i % STAGE_COLORS.length]} style={{ width: `${pct}%` }} />
);
})}
</div>
<div className="flex flex-wrap gap-1.5">
{stages.map(([stageName, tasks], i) => (
<span
key={stageName}
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>
<div className="flex flex-wrap gap-1">
{stages.map(([name, 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 className={`w-1.5 h-1.5 rounded-full ${STAGE_COLORS[i % STAGE_COLORS.length]}`} />
<span className="truncate max-w-[10rem]">{name}</span>
<span className="font-mono font-bold text-text-primary">{tasks.length}</span>
</span>
))}
@@ -65,33 +45,27 @@ function ProjectRow({ project }: { project: Project }) {
}
export function KanbanBoard({ projects }: KanbanBoardProps) {
const activeProjects = projects.filter((p) => {
const total = Object.values(p.stages).reduce((s, t) => s + t.length, 0);
return total > 0;
});
const totalTasks = activeProjects.reduce(
(sum, p) => sum + Object.values(p.stages).reduce((s, t) => s + t.length, 0),
0
const active = projects.filter((p) =>
Object.values(p.stages).reduce((s, t) => s + t.length, 0) > 0
);
const total = active.reduce(
(sum, p) => sum + Object.values(p.stages).reduce((s, t) => s + t.length, 0), 0
);
return (
<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">
<span className="text-base text-text-secondary">
<span className="font-bold text-text-primary">{activeProjects.length}</span> proyectos
<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-text-secondary">
<span className="font-bold text-text-primary">{active.length}</span> proyectos
</span>
<span className="text-border-light">|</span>
<span className="text-base text-text-secondary">
<span className="font-bold text-text-primary">{totalTasks}</span> tareas
<span className="text-text-secondary">
<span className="font-bold text-text-primary">{total}</span> tareas
</span>
</div>
<div className="flex-1 overflow-y-auto px-8 py-4">
<div className="grid grid-cols-3 gap-4">
{activeProjects.map((project) => (
<ProjectRow key={project.id} project={project} />
))}
<div className="flex-1 overflow-y-auto px-6 py-3">
<div className="grid grid-cols-3 gap-3">
{active.map((p) => <ProjectRow key={p.id} project={p} />)}
</div>
</div>
</div>

View File

@@ -11,171 +11,137 @@ const ICON_MAP: Record<string, string> = {
phone: "📞", camera: "📷", device: "📱",
};
/* ── Compact VM pill ─────────────────────────────────────── */
function VmPill({ node }: { node: NetworkNode }) {
const isUp = node.status === "up";
const dotColor = isUp ? "bg-success" : "bg-danger";
const dot = node.status === "up" ? "bg-success" : "bg-danger";
return (
<div className="flex items-center gap-1.5 bg-bg-card border border-border rounded-md px-2 py-1">
<span className={`w-2 h-2 rounded-full ${dotColor} shrink-0`} />
<span className="text-sm font-medium text-text-primary truncate">{node.name}</span>
<div className="flex items-center gap-1 bg-bg-card/80 border border-border rounded px-1.5 py-0.5">
<span className={`w-1.5 h-1.5 rounded-full ${dot} shrink-0`} />
<span className="text-xs text-text-primary truncate">{node.name}</span>
</div>
);
}
/* ── Infrastructure card ─────────────────────────────────── */
function InfraCard({ node }: { node: NetworkNode }) {
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";
const border = isUp ? "border-success/30" : "border-danger/30";
const bg = isUp ? "bg-success-dim" : "bg-danger-dim";
const dot = isUp ? "bg-success" : "bg-danger";
return (
<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)}
>
<div className="flex items-center gap-2">
<span className="text-2xl">{ICON_MAP[node.icon] || "📦"}</span>
<div className="flex-1 min-w-0">
<p className="text-lg font-bold text-text-primary leading-tight">{node.name}</p>
<p className="text-sm font-mono text-text-secondary">{node.ip}</p>
<span className="text-lg">{ICON_MAP[node.icon] || "📦"}</span>
<div className="min-w-0 flex-1">
<p className="text-sm font-bold text-text-primary leading-none">{node.name}</p>
<p className="text-xs font-mono text-text-secondary leading-none mt-0.5">{node.ip}</p>
</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>
{showPass && node.username && (
<p className="text-xs text-text-secondary mt-1">
<p className="text-xs text-text-muted mt-1">
{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>
)}
{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>
);
}
/* ── Proxmox server column ───────────────────────────────── */
function ProxmoxColumn({ server, vms }: { server: NetworkNode; vms: NetworkNode[] }) {
const upCount = vms.filter((v) => v.status === "up").length;
function ProxmoxCol({ server, vms }: { server: NetworkNode; vms: NetworkNode[] }) {
const up = vms.filter((v) => v.status === "up").length;
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} />
<div className="w-px h-4 bg-border-light" />
<span className="text-xs text-text-muted mb-2">
{upCount}/{vms.length} activos
</span>
<div className="grid grid-cols-3 gap-1.5 w-full">
{vms.map((vm) => (
<VmPill key={vm.ip + vm.name} node={vm} />
))}
<div className="w-px h-2 bg-border-light" />
<span className="text-xs text-text-muted mb-1">{up}/{vms.length}</span>
<div className="grid grid-cols-3 gap-1 w-full">
{vms.map((vm) => <VmPill key={vm.ip + vm.name} node={vm} />)}
</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) {
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 find = (name: string) => nodes.find((n) => n.name === name);
const modem = find("Router Telmex");
const firewall = find("Firewall OPNsense");
const sw = find("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"
);
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 pve = nodes.filter((n) => n.type === "proxmox");
const vms = new Map<string, NetworkNode[]>();
for (const n of nodes) {
if ((n.type === "vm" || n.type === "ct") && n.parent) {
const list = vms.get(n.parent) || [];
list.push(n);
vms.set(n.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 total = nodes.length;
const online = nodes.filter((n) => n.status === "up").length;
const vmTotal = nodes.filter((n) => n.type === "vm" || n.type === "ct").length;
return (
<div className="flex flex-col h-full">
{/* Summary bar */}
<div className="flex items-center gap-6 px-8 py-2 bg-bg-secondary border-b border-border">
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-full bg-success" />
<span className="text-base text-text-secondary">
<span className="font-bold text-text-primary">{onlineCount}</span> online
</span>
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-full bg-danger" />
<span className="text-base text-text-secondary">
<span className="font-bold text-text-primary">{total - onlineCount}</span> offline
</span>
</div>
{/* Stats */}
<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="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-success" />
<span className="font-bold text-text-primary">{online}</span>
<span className="text-text-secondary">online</span>
</span>
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-danger" />
<span className="font-bold text-text-primary">{nodes.length - online}</span>
<span className="text-text-secondary">offline</span>
</span>
<span className="text-border-light">|</span>
<span className="text-base text-text-secondary">
<span className="font-bold text-text-primary">{proxmoxServers.length}</span> Proxmox ·{" "}
<span className="font-bold text-text-primary">
{nodes.filter((n) => n.type === "vm" || n.type === "ct").length}
</span>{" "}
VMs/CTs
<span className="text-text-secondary">
<span className="font-bold text-text-primary">{pve.length}</span> Proxmox ·{" "}
<span className="font-bold text-text-primary">{vmTotal}</span> VMs/CTs
</span>
</div>
{/* Diagram */}
<div className="flex-1 overflow-y-auto flex flex-col items-center px-6 py-4">
{/* Top chain: Modem → Firewall → Switch */}
<div className="flex items-center gap-3">
{/* Tree */}
<div className="flex-1 flex flex-col items-center justify-start px-4 py-3 overflow-hidden">
{/* Top chain */}
<div className="flex items-center gap-2">
{modem && <InfraCard node={modem} />}
<span className="text-border-light text-xl"></span>
<span className="text-border-light text-base"></span>
{firewall && <InfraCard node={firewall} />}
<span className="text-border-light text-xl"></span>
{switchNode && <InfraCard node={switchNode} />}
<span className="text-border-light text-base"></span>
{sw && <InfraCard node={sw} />}
</div>
<VLine />
{/* Horizontal branch line */}
<div className="w-3/4 h-px bg-border-light" />
{/* Branch down */}
<div className="w-px h-3 bg-border-light" />
<div className="h-px bg-border-light" style={{ width: "80%" }} />
{/* Proxmox columns */}
<div className="flex gap-6 w-full px-4 mt-0">
{proxmoxServers.map((server) => {
const vms = vmsByParent.get(server.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 className="flex gap-4 w-full">
{pve.map((s) => (
<ProxmoxCol key={s.ip} server={s} vms={vms.get(s.name) || []} />
))}
</div>
{/* Other devices */}
{otherDevices.length > 0 && (
<div className="mt-4 w-full">
<p className="text-xs font-bold text-text-muted uppercase tracking-widest mb-2 text-center">
{/* Others */}
{other.length > 0 && (
<div className="mt-3 w-full">
<p className="text-xs text-text-muted uppercase tracking-wider text-center mb-1.5">
Otros dispositivos
</p>
<div className="flex justify-center gap-3 flex-wrap">
{otherDevices.map((node) => (
<InfraCard key={node.ip} node={node} />
))}
<div className="flex justify-center gap-2 flex-wrap">
{other.map((n) => <InfraCard key={n.ip} node={n} />)}
</div>
</div>
)}

View File

@@ -21,7 +21,8 @@
}
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 {
@@ -35,12 +36,12 @@ body {
}
::-webkit-scrollbar {
width: 6px;
width: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 3px;
border-radius: 2px;
}