diff --git a/app/public/locales/en/translation.json b/app/public/locales/en/translation.json index 7786dafd..c2b4094f 100644 --- a/app/public/locales/en/translation.json +++ b/app/public/locales/en/translation.json @@ -385,6 +385,9 @@ }, "copyIp": "Copy IP", "detail": { + "minSignalAny": "Any signal", + "minSignal": "Min signal", + "linkedMacs": "Linked MACs", "dhcpLease": "DHCP lease", "firstSeen": "First seen", "lastSeen": "Last seen", diff --git a/app/public/locales/es/translation.json b/app/public/locales/es/translation.json index 880c6733..5e338e46 100644 --- a/app/public/locales/es/translation.json +++ b/app/public/locales/es/translation.json @@ -385,6 +385,9 @@ }, "copyIp": "Copiar IP", "detail": { + "minSignalAny": "Cualquier señal", + "minSignal": "Señal mínima", + "linkedMacs": "MACs enlazadas", "dhcpLease": "Concesión DHCP", "firstSeen": "Primera vez vista", "lastSeen": "Última vez vista", diff --git a/app/src/components/channels/CurrentChannels.tsx b/app/src/components/channels/CurrentChannels.tsx index 7695def3..cac524fa 100644 --- a/app/src/components/channels/CurrentChannels.tsx +++ b/app/src/components/channels/CurrentChannels.tsx @@ -145,14 +145,32 @@ function ChannelAnalysisChart({ bandName, channels, scans }: { bandName: string; ) } -export function CurrentChannels({ routerId, routerName }: { routerId: string; routerName?: string }) { +export function CurrentChannels({ + routerId, + routerName, + band: bandProp, + onBandChange, +}: { + routerId: string + routerName?: string + /** #1283: banda controlada por el selector superior de Canales. Cuando + * llega, el selector interno se oculta (el de arriba manda). */ + band?: '2.4 GHz' | '5 GHz' + onBandChange?: (b: '2.4 GHz' | '5 GHz') => void +}) { const { t } = useTranslation() const reduce = useReducedMotion() const [overview, setOverview] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(false) const [noApi, setNoApi] = useState(false) - const [band, setBand] = useState('all') + const controlled = bandProp !== undefined + const [internalBand, setInternalBand] = useState('all') + const band: SurveyBand = controlled ? (bandProp as SurveyBand) : internalBand + const setBand = (b: SurveyBand) => { + if (controlled) onBandChange?.(b as '2.4 GHz' | '5 GHz') + else setInternalBand(b) + } // Vecinos recientes del channel-plan de la unidad (capa opcional sobre el // survey, #538). const [scans, setScans] = useState([]) @@ -252,28 +270,30 @@ export function CurrentChannels({ routerId, routerName }: { routerId: string; ro > {/* Header + filtro banda */}
-
-
- -
-

{t('roaming.survey.title')}

+ {!controlled && ( +
+
+ +
+

{t('roaming.survey.title')}

+
+
+
+ {bandOptions.map((b) => ( + + ))}
-
- {bandOptions.map((b) => ( - - ))} -
-
+ )}
{/* La tarjeta de la unidad seleccionada */} diff --git a/app/src/pages/ChannelPlan.tsx b/app/src/pages/ChannelPlan.tsx index 8e3c32d1..70f540b2 100644 --- a/app/src/pages/ChannelPlan.tsx +++ b/app/src/pages/ChannelPlan.tsx @@ -161,6 +161,10 @@ export default function ChannelPlan() { // vivo de la unidad, ex-tab de Itinerancia). const [lens, setLens] = useState<'24h' | 'now'>('24h') const [activeRadio, setActiveRadio] = useState('') + // #1283: banda preferida del usuario ('2.4 GHz'...). Al cambiar de unidad + // NO se resetea: si el nuevo AP tiene esa banda, se selecciona - poder + // comparar la misma banda entre APs sin que salte a la primera radio. + const [bandPref, setBandPref] = useState('') const [hidden, setHidden] = useState>(new Set()) const [selected, setSelected] = useState(null) const [hover, setHover] = useState<{ net: SpectrumNet; x: number; y: number } | null>(null) @@ -276,7 +280,10 @@ export default function ChannelPlan() { const radioKey = (r: RadioRec) => `${r.section || r.iface}-${r.name}-${r.channel}` const radios: RadioRec[] = data?.radios ?? [] - const active: RadioRec | undefined = radios.find((r) => radioKey(r) === activeRadio) ?? radios[0] + const active: RadioRec | undefined = + radios.find((r) => radioKey(r) === activeRadio) ?? + (bandPref ? radios.find((r) => r.name === bandPref) : undefined) ?? + radios[0] const routerName = sortedRouters.find((r) => r.id === routerId)?.name ?? routerId // Vecinos de la radio activa: por banda; si el iface del radio casa con @@ -506,6 +513,7 @@ export default function ChannelPlan() { aria-selected={isActive} onClick={() => { setActiveRadio(key) + setBandPref(r.name) setHidden(new Set()) setSelected(null) setFocus(null) @@ -603,7 +611,22 @@ export default function ChannelPlan() { )} {lens === 'now' && routerId && ( - r.id === routerId)?.name} /> + r.id === routerId)?.name} + band={active?.name === '5 GHz' ? '5 GHz' : '2.4 GHz'} + onBandChange={(b) => { + // #1283: el selector superior manda - elegir la radio de esa banda. + const r = radios.find((rr) => rr.name === b) + if (r) { + setActiveRadio(radioKey(r)) + setBandPref(b) + setHidden(new Set()) + setSelected(null) + setFocus(null) + } + }} + /> )} {lens === '24h' && !loading && data && active && ( diff --git a/app/src/pages/Devices.tsx b/app/src/pages/Devices.tsx index da3f5c88..6d12b16f 100644 --- a/app/src/pages/Devices.tsx +++ b/app/src/pages/Devices.tsx @@ -578,6 +578,9 @@ interface FilterBarProps { onlyWeak: boolean setOnlyWeak: (v: boolean) => void weakCount: number + /** #1283: señal mínima para la tabla (null = sin filtro). */ + minSignal: number | null + setMinSignal: (v: number | null) => void /** #1003: umbral configurado, se muestra en la etiqueta del chip */ weakDbm: number view: 'list' | 'grid' @@ -660,6 +663,18 @@ function FilterBar(p: FilterBarProps) { ))} + {/* Señal mínima (#1283): oculta los vecinos débiles de la tabla */} + {/* Estado online: All / Online / Offline (#923) */} options={[ @@ -836,6 +851,12 @@ function DeviceDetail({ {device.hostname} + {device.aliasMacs && device.aliasMacs.length > 0 && ( + // #1298: las MACs enlazadas (#1151) visibles en el detalle. + + {device.aliasMacs.join(', ')} + + )} {manufacturerLabel(device.manufacturer)} {fmtSeenAgo(device.lastSeenMs)} {hasAdGuard && ( @@ -1458,6 +1479,9 @@ export default function Devices() { setOnlineState(v) }, []) const [onlyWeak, setOnlyWeak] = useState(false) + // #1283: señal mínima de la tabla (oculta vecinos débiles); null = sin filtro. + const [minSignal, setMinSignal] = useState(null) + // #1003: umbral de señal débil desde el ajuste global (como Roaming #906). const weakDbm = useWeakSignalDbm() // Filtro "sin proteger por AdGuard" (#959): se activa/desactiva desde la @@ -1570,12 +1594,15 @@ export default function Devices() { if (online === 'online' && !d.online) return false if (online === 'offline' && d.online) return false if (onlyWeak && !(d.online && d.signalDbm !== null && d.signalDbm < weakDbm)) return false + if (minSignal !== null && d.signalDbm !== null && d.signalDbm < minSignal) return false if (onlyUnprotected && d.adguard) return false if (routerIds.length > 0 && !routerIds.includes(d.routerId)) return false if (bandsEffective.length > 0 && !bandsEffective.includes(d.band as BandValue)) return false if (groups.length > 0 && !groups.includes(d.group)) return false if (q) { - const hay = `${d.name} ${d.ip} ${d.mac} ${d.manufacturer} ${d.hostname}`.toLowerCase() + // #1298: las MACs enlazadas (#1151) también buscables - la MAC de + // otra banda/red del mismo cliente encuentra al canónico. + const hay = `${d.name} ${d.ip} ${d.mac} ${d.manufacturer} ${d.hostname} ${(d.aliasMacs ?? []).join(' ')}`.toLowerCase() if (!hay.includes(q)) return false } return true @@ -1618,6 +1645,13 @@ export default function Devices() { break } if (c !== 0) return dir * c + // #1283: al ordenar por banda o SSID, desempate por señal DESC + // (primero lo que más impacta: vecinos cercanos/fuertes agrupados). + if (sort.key === 'band' || sort.key === 'name') { + const sa = a.signalDbm ?? -999 + const sb = b.signalDbm ?? -999 + if (sa !== sb) return sb - sa + } // Desempate: online primero, luego nombre if (a.online !== b.online) return a.online ? -1 : 1 return a.name.localeCompare(b.name, numLocale()) @@ -1910,6 +1944,8 @@ export default function Devices() { {/* ③ Filter bar */}