From 24b3883917adf47582553b75d063c33fd3c80860 Mon Sep 17 00:00:00 2001 From: gnacho Date: Tue, 20 Oct 2026 10:18:30 +0200 Subject: [PATCH] feat(ports): mark the management port in the chassis (#543) The chassis view treated every port the same, but the port the panel itself is reached through is the risky one to touch with mirror, rate limit or STP changes. The mirror probe already deduced it (gateway -> ARP -> bridge FDB) but fetching the whole mirror probe for a badge is too heavy (a tc filter show per port). Add a lightweight GET /api/management-port that runs only the heuristic (two /proc reads plus one brctl showmacs) and load it lazily from the chassis after the first paint: the matching tile gets a lock badge in the corner opposite the PoE one, the tooltip names it and the legend gains an entry. Undetected or missing probe: no badge, the chassis renders exactly as before. Demo mode stubs an empty port. Verified end-to-end on a 52-port Linksys LGS352C: the endpoint returns lan27 (the real uplink), the badge lands on the LAN 27 tile and the legend shows, with zero page errors. Closes #543 --- app/src/api.ts | 2 ++ app/src/components/ports/EthPortsCard.tsx | 29 +++++++++++++++++++++-- app/src/demo/index.ts | 2 ++ app/src/locales/en.ts | 1 + app/src/locales/es.ts | 1 + app/src/types.ts | 6 +++++ internal/modules/mirror.go | 16 +++++++++++++ internal/server/server.go | 8 +++++++ 8 files changed, 63 insertions(+), 2 deletions(-) diff --git a/app/src/api.ts b/app/src/api.ts index 049c1d3..caf2990 100644 --- a/app/src/api.ts +++ b/app/src/api.ts @@ -844,6 +844,8 @@ const realApi = { }), mirror: () => request("/api/mirror"), + managementPort: () => + request("/api/management-port"), setMirror: (monitor: string, sources: string[]) => request<{ status: string }>("/api/mirror", { method: "POST", diff --git a/app/src/components/ports/EthPortsCard.tsx b/app/src/components/ports/EthPortsCard.tsx index 6b26e4c..adf4486 100644 --- a/app/src/components/ports/EthPortsCard.tsx +++ b/app/src/components/ports/EthPortsCard.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Cable, Pencil, Zap } from "lucide-react"; +import { Cable, Lock, Pencil, Zap } from "lucide-react"; import { api } from "../../api"; import type { EthPort, PhysPortsProbe, SwitchPort, SwitchProbe } from "../../types"; import { @@ -45,6 +45,19 @@ export function EthPortsCard({ ports, index = 0, className = "md:col-span-2" }: const [phys, setPhys] = useState(); const physRequested = useRef(false); + // Boca de gestion (#543): badge con candado en la baldosa. Se pide de + // forma perezosa tras montar: la deteccion (gateway -> ARP -> FDB) tarda + // unos cientos de ms en switches densos y no debe retrasar el primer + // pintado. Sin probe o sin deducir: no hay badge, el chasis queda igual. + const [mgmtPort, setMgmtPort] = useState(); + useEffect(() => { + let alive = true; + api.managementPort() + .then((r) => { if (alive && r.port) setMgmtPort(r.port); }) + .catch(() => { /* sin dato, sin badge */ }); + return () => { alive = false; }; + }, []); + const load = useCallback(async () => { try { setProbe(await api.switchPorts()); @@ -170,13 +183,14 @@ export function EthPortsCard({ ports, index = 0, className = "md:col-span-2" }: : p.devices.length > 1 ? t("ports.unmanagedN", { count: p.devices.length }) : t("ports.busy"); const tier = speedTier(p); + const isMgmt = mgmtPort === p.name; const led = adminDown ? "bg-danger" : !p.up ? "bg-border-strong" : tier.tone === "ok" ? "bg-ok" : tier.tone === "warn" ? "bg-warn" : "bg-danger"; return (