fix(wireless): CAPsMAN panel corrections from field testing (v0.22.1-beta)
All five points raised on #94 after the reporter tested v0.22.0 against a live CAPsMAN deployment. Two were regressions in that release. Operating channel instead of a capability list (mine) current_channel was stored from /interface/wifi/radio's current-channels, which is every channel the radio supports rather than the one it is using. Measured on a wAP ax: 535 characters on the 2.4 GHz radio and 3037 on the 5 GHz one — the "two kilobytes of text" reported. /interface/wifi/monitor gives the operating channel (5500/ax/Ceeeeeee/D), and carries state, peer counts and TX power too, all now shown on the radio chip. Client counts that were zero everywhere (mine) The panel read registered_clients from the CAP's own wireless_interfaces rows, which are empty when traffic is processed centrally by the controller. Monitor's registered-peers is not the fix either, and testing caught that before it shipped: clients register on the virtual AP carrying the SSID, not on the physical radio, so monitor on a physical radio reports zero on a busy access point. On the test AP, wifi1 and wifi2 both read 0 while wifi3-wifi6 held all ten clients. Counts now come from the registration table, with each entry attributed to its radio by following master-interface up to the interface owning a radio-mac. That field requires /interface/wifi/print detail — it is absent from the plain listing. Verified against real hardware: ten registrations, attributed 8 and 2 across the two radios, none dropped. "no network" on remote interfaces The Network column resolved from the AP's own bridge ports. A CAPsMAN-provisioned interface is not a local bridge port, so nothing matched and the UI asserted the interface had no network at all. It now falls back to the datapath — inline fields or a named/.id reference — and labels the source. Where the datapath names no bridge it says "via controller" rather than claiming no network. Status labels "Serving Clients" claimed something the underlying field does not say: running means the radio is up, not that clients are connected. Now "Running" and "Enabled, not running". Table title "SSIDs / Wireless Interfaces" conflated two different things; the table lists interfaces, virtual and physical. Now "Wireless Interfaces". Guest WiFi on CAPsMAN-provisioned radios Raised by the reporter as an aside, and it was a real hazard already shipping. setupGuestNetwork created virtual APs locally on whatever physical radios it found, with no awareness that a controller might own them — the SSID would be ignored or overwritten on the next provision while the UI reported success. Provisioned radios are now skipped with an explanation, and if every radio is provisioned the setup is refused, pointing at the controller instead. 37 unit tests on the CAPsMAN module. No regression on a fleet without a controller: local bridge-port resolution unchanged, roles still correct. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
14bab24c7e
commit
e15556b601
@@ -45,6 +45,7 @@ function Controller({ c }: { c: CapsmanController }) {
|
||||
<div className="text-[11px] text-gray-500 dark:text-slate-400 mt-0.5">
|
||||
{c.access_points.length} access point{c.access_points.length !== 1 ? 's' : ''} ·{' '}
|
||||
{radioCount} radio{radioCount !== 1 ? 's' : ''} ·{' '}
|
||||
{c.client_count} client{c.client_count !== 1 ? 's' : ''} ·{' '}
|
||||
{c.configurations.length} configuration{c.configurations.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,14 +84,21 @@ function Controller({ c }: { c: CapsmanController }) {
|
||||
{ap.radios[0]?.local && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 dark:bg-slate-700 text-gray-600 dark:text-slate-300">on controller</span>
|
||||
)}
|
||||
<span className="ml-auto text-[11px] text-gray-500 dark:text-slate-400">
|
||||
{ap.client_count} client{ap.client_count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{ap.radios.map((r) => (
|
||||
<div key={r.radio_mac} className="flex items-center gap-1.5 text-[11px] rounded px-2 py-1 bg-gray-50 dark:bg-slate-800">
|
||||
<Radio className="w-3 h-3 text-gray-400" />
|
||||
<Radio className={clsx('w-3 h-3', r.state === 'running' ? 'text-green-500' : 'text-gray-400')} />
|
||||
<span className="font-medium text-gray-700 dark:text-slate-200">{r.interface_name || r.radio_mac}</span>
|
||||
{r.hw_type && <span className="text-gray-400">{r.hw_type}</span>}
|
||||
{r.current_channel && <span className="mono text-gray-500 dark:text-slate-400">{r.current_channel}</span>}
|
||||
{r.registered_peers != null && (
|
||||
<span className="text-gray-500 dark:text-slate-400">{r.registered_peers} client{r.registered_peers !== 1 ? 's' : ''}</span>
|
||||
)}
|
||||
{r.tx_power != null && <span className="text-gray-400">{r.tx_power} dBm</span>}
|
||||
{r.hw_type && <span className="text-gray-400 hidden sm:inline">{r.hw_type}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -103,13 +111,13 @@ function Controller({ c }: { c: CapsmanController }) {
|
||||
{c.ssids.length > 0 && (
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-slate-400 mb-2">
|
||||
Provisioned SSIDs
|
||||
Provisioned interfaces
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 dark:border-slate-700">
|
||||
{['Access point', 'Interface', 'SSID', 'Band', 'Clients', ''].map((h, i) => (
|
||||
{['Access point', 'Interface', 'SSID', 'Band', ''].map((h, i) => (
|
||||
<th key={i} className="table-header px-3 py-2 text-left">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
@@ -123,7 +131,6 @@ function Controller({ c }: { c: CapsmanController }) {
|
||||
<td className="px-3 py-2 mono text-xs text-gray-500 dark:text-slate-400">
|
||||
{s.band || '—'}{s.frequency ? ` · ${s.frequency}` : ''}
|
||||
</td>
|
||||
<td className="px-3 py-2 mono num-tab text-xs text-gray-600 dark:text-slate-300">{s.registered_clients ?? 0}</td>
|
||||
<td className="px-3 py-2">
|
||||
<span className={clsx(
|
||||
'text-[10px] px-1.5 py-0.5 rounded font-medium',
|
||||
|
||||
@@ -916,7 +916,7 @@ export default function WirelessSettingsPage() {
|
||||
<div className="card overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-gray-200 dark:border-slate-700 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
SSIDs / Wireless Interfaces
|
||||
Wireless Interfaces
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@@ -975,8 +975,21 @@ export default function WirelessSettingsPage() {
|
||||
</td>
|
||||
<td className="px-4 py-3 text-xs text-gray-600 dark:text-slate-400">
|
||||
{iface.bridge
|
||||
? <span className="font-mono">{iface.bridge}{iface['bridge-pvid'] ? <span className="text-gray-400"> VLAN {iface['bridge-pvid']}</span> : ''}</span>
|
||||
: <span className="text-amber-500 font-medium">no network</span>}
|
||||
? (
|
||||
<span
|
||||
className="font-mono"
|
||||
title={iface['network_source'] === 'capsman-datapath'
|
||||
? 'From the CAPsMAN datapath, not a local bridge port'
|
||||
: undefined}
|
||||
>
|
||||
{iface.bridge}
|
||||
{iface['bridge-pvid'] ? <span className="text-gray-400"> VLAN {iface['bridge-pvid']}</span> : ''}
|
||||
{iface['network_source'] === 'capsman-datapath' && <span className="text-gray-400"> (datapath)</span>}
|
||||
</span>
|
||||
)
|
||||
: iface['managed_by_capsman']
|
||||
? <span className="text-gray-400" title="This interface is provisioned by CAPsMAN and its datapath does not name a bridge">via controller</span>
|
||||
: <span className="text-amber-500 font-medium">no network</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-xs text-gray-600 dark:text-slate-400">
|
||||
{iface.band || '—'}
|
||||
@@ -997,11 +1010,11 @@ export default function WirelessSettingsPage() {
|
||||
</span>
|
||||
) : iface.running === 'true' ? (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400">
|
||||
<CheckCircle className="w-3 h-3" />Serving Clients
|
||||
<CheckCircle className="w-3 h-3" />Running
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400">
|
||||
<Power className="w-3 h-3" />Enabled
|
||||
<Power className="w-3 h-3" />Enabled, not running
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -900,7 +900,12 @@ export interface CapsmanRadio {
|
||||
interface_name: string | null;
|
||||
local: boolean;
|
||||
hw_type: string | null;
|
||||
/** Operating channel from /interface/wifi/monitor, not the supported-channel list. */
|
||||
current_channel: string | null;
|
||||
state: string | null;
|
||||
registered_peers: number | null;
|
||||
authorized_peers: number | null;
|
||||
tx_power: number | null;
|
||||
remote_cap_name: string | null;
|
||||
matched_device_id: number | null;
|
||||
matched_device_name: string | null;
|
||||
@@ -911,6 +916,8 @@ export interface CapsmanAccessPoint {
|
||||
device_id: number | null;
|
||||
device_name: string;
|
||||
radios: CapsmanRadio[];
|
||||
/** Clients as the controller sees them; a CAP's own table is empty under central forwarding. */
|
||||
client_count: number;
|
||||
}
|
||||
|
||||
export interface CapsmanConfiguration {
|
||||
@@ -951,6 +958,7 @@ export interface CapsmanController {
|
||||
model: string | null;
|
||||
status: string;
|
||||
wifi_role: string | null;
|
||||
client_count: number;
|
||||
access_points: CapsmanAccessPoint[];
|
||||
configurations: CapsmanConfiguration[];
|
||||
provisioning: CapsmanProvisioning[];
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const APP_VERSION = 'v0.22.0 Beta';
|
||||
export const APP_VERSION = 'v0.22.1 Beta';
|
||||
|
||||
Reference in New Issue
Block a user