Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 56 additions & 35 deletions modules/luci-base/htdocs/luci-static/resources/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,15 @@ function getProtocolHandlers() {
});
}

function getWifiStateBySid(sid) {
function getWifiStateBySid(sid, radioname) {
const s = uci.get('wireless', sid);

if (s != null && s['.type'] == 'wifi-iface') {
for (let radioname in _state.radios) {
for (let netstate of _state.radios[radioname].interfaces) {
for (let rname in _state.radios) {
if (radioname != null && rname != radioname)
continue;

for (let netstate of _state.radios[rname].interfaces) {

if (typeof(netstate.section) != 'string')
continue;
Expand All @@ -145,7 +148,7 @@ function getWifiStateBySid(sid) {
if (s2['.anonymous'] == false && netstate.section.charAt(0) == '@')
return null;

return [ radioname, _state.radios[radioname], netstate ];
return [ rname, _state.radios[rname], netstate ];
}
}
}
Expand Down Expand Up @@ -183,7 +186,7 @@ function getWifiSidByNetid(netid) {
const sections = uci.sections('wireless', 'wifi-iface');
let n = 0;
for (let s of sections) {
if (s.device != m[1])
if (L.toArray(s.device).indexOf(m[1]) == -1)
continue;

if (++n == +m[2])
Expand All @@ -208,23 +211,27 @@ function getWifiSidByIfname(ifname) {
return null;
}

function getWifiNetidBySid(sid) {
function getWifiNetidBySid(sid, radioname) {
const s = uci.get('wireless', sid);
if (s != null && s['.type'] == 'wifi-iface') {
const radioname = s.device;
if (typeof(radioname) == 'string') {
const radionames = L.toArray(s.device);

if (radioname == null)
radioname = radionames[0];

if (typeof(radioname) == 'string' && radionames.indexOf(radioname) != -1) {
const sections = uci.sections('wireless', 'wifi-iface');
let n = 0;
for (let sec of sections) {
if (sec.device != radioname)
if (L.toArray(sec.device).indexOf(radioname) == -1)
continue;

n++;

if (sec['.name'] != s['.name'])
continue;

return [ '%s.network%d'.format(s.device, n), s.device ];
return [ '%s.network%d'.format(radioname, n), radioname ];
}

}
Expand Down Expand Up @@ -1292,14 +1299,13 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
const networkCount = {};

for (let wf_if of uciWifiIfaces) {
if (typeof(wf_if.device) != 'string')
continue;

networkCount[wf_if.device] = (networkCount[wf_if.device] || 0) + 1;
for (let radioname of L.toArray(wf_if.device)) {
networkCount[radioname] = (networkCount[radioname] || 0) + 1;

const netid = '%s.network%d'.format(wf_if.device, networkCount[wf_if.device]);
const netid = '%s.network%d'.format(radioname, networkCount[radioname]);

devices[netid] = this.instantiateDevice(netid);
devices[netid] = this.instantiateDevice(netid);
}
}

/* find uci declared devices */
Expand Down Expand Up @@ -1430,8 +1436,17 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
const wifiIfaces = uci.sections('wireless', 'wifi-iface');
const rv = [];

for (let wf_if of wifiIfaces)
rv.push(this.lookupWifiNetwork(wf_if['.name']));
for (let wf_if of wifiIfaces) {
const radionames = L.toArray(wf_if.device);

if (radionames.length < 2) {
rv.push(this.lookupWifiNetwork(wf_if['.name']));
continue;
}

for (let radioname of radionames)
rv.push(this.lookupWifiNetwork(wf_if['.name'], radioname));
}

rv.sort(function(a, b) {
return L.naturalCompare(a.getID(), b.getID());
Expand Down Expand Up @@ -1687,15 +1702,21 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
},

/* private */
lookupWifiNetwork(netname) {
let sid, res, netid, radioname, radiostate, netstate;
lookupWifiNetwork(netname, radioname) {
let sid, res, netid, radiostate, netstate;

if (typeof(radioname) != 'string')
radioname = null;

sid = getWifiSidByNetid(netname);

if (sid != null) {
res = getWifiStateBySid(sid);
const m = /^(\w+)\.network\d+$/.exec(netname);
const multiradio = (L.toArray(uci.get('wireless', sid, 'device')).length > 1);

res = getWifiStateBySid(sid, (multiradio && m) ? m[1] : null);
netid = netname;
radioname = res ? res[0] : null;
radioname = res ? res[0] : (m ? m[1] : null);
radiostate = res ? res[1] : null;
netstate = res ? res[2] : null;
}
Expand All @@ -1707,20 +1728,20 @@ Network = baseclass.extend(/** @lends LuCI.network.prototype */ {
radiostate = res[1];
netstate = res[2];
sid = netstate.section;
netid = L.toArray(getWifiNetidBySid(sid))[0];
netid = L.toArray(getWifiNetidBySid(sid, radioname))[0];
}
else {
res = getWifiStateBySid(netname);
res = getWifiStateBySid(netname, radioname);

if (res != null) {
radioname = res[0];
radiostate = res[1];
netstate = res[2];
sid = netname;
netid = L.toArray(getWifiNetidBySid(sid))[0];
netid = L.toArray(getWifiNetidBySid(sid, radioname))[0];
}
else {
res = getWifiNetidBySid(netname);
res = getWifiNetidBySid(netname, radioname);

if (res != null) {
netid = res[0];
Expand Down Expand Up @@ -2779,19 +2800,19 @@ Protocol = baseclass.extend(/** @lends LuCI.network.Protocol.prototype */ {
const uciWifiIfaces = uci.sections('wireless', 'wifi-iface');

for (let wf_if of uciWifiIfaces) {
if (typeof(wf_if.device) != 'string')
continue;

const radionames = L.toArray(wf_if.device);
const networks = L.toArray(wf_if.network);

for (let n of networks) {
if (n != this.sid)
continue;

const netid = getWifiNetidBySid(wf_if['.name']);
for (let radioname of radionames) {
const netid = getWifiNetidBySid(wf_if['.name'], radioname);

if (netid != null)
rv.push(Network.prototype.instantiateDevice(netid[0], this));
if (netid != null)
rv.push(Network.prototype.instantiateDevice(netid[0], this));
}
}
}

Expand Down Expand Up @@ -3602,7 +3623,7 @@ WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ {
return Network.prototype.getWifiNetwork(network).then(L.bind(function(networkInstance) {
const uciWifiIface = (networkInstance.sid ? uci.get('wireless', networkInstance.sid) : null);

if (uciWifiIface == null || uciWifiIface['.type'] != 'wifi-iface' || uciWifiIface.device != this.sid)
if (uciWifiIface == null || uciWifiIface['.type'] != 'wifi-iface' || L.toArray(uciWifiIface.device).indexOf(this.sid) == -1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One device comparison in this same class was missed: WifiDevice.deleteWifiNetwork() at network.js:3703 still does uci.get('wireless', sid, 'device') != this.sid.

For a section spanning several radios that value is an array, so the comparison is always true and the method returns Promise.resolve(false) without ever removing the section — i.e. deleting a multi-radio network through the public API silently fails. It needs the same L.toArray(...).indexOf(this.sid) == -1 treatment applied here and in getWifiNetwork().


Generated by Claude Code

return Promise.reject();

return networkInstance;
Expand Down Expand Up @@ -3679,7 +3700,7 @@ WifiDevice = baseclass.extend(/** @lends LuCI.network.WifiDevice.prototype */ {
sid = getWifiSidByIfname(network);
}

if (sid == null || uci.get('wireless', sid, 'device') != this.sid)
if (sid == null || L.toArray(uci.get('wireless', sid, 'device')).indexOf(this.sid) == -1)
return Promise.resolve(false);

uci.remove('wireless', sid);
Expand Down Expand Up @@ -3895,7 +3916,7 @@ WifiNetwork = baseclass.extend(/** @lends LuCI.network.WifiNetwork.prototype */
* or `null` if it cannot be determined.
*/
getWifiDeviceName() {
return this.ubus('radio') || this.get('device');
return this.ubus('radio') || L.toArray(this.get('device'))[0];
},

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ function render_modal_status(node, radioNet) {
const is_assoc = (bssid && bssid != '00:00:00:00:00:00' && channel && mode != 'Unknown' && !disabled);

if (node == null)
node = E('span', { 'class': 'ifacebadge large', 'data-network': radioNet.getName() }, [ E('small'), E('span') ]);
node = E('span', { 'class': 'ifacebadge large', 'data-network': radioNet.getID() ?? radioNet.getName() }, [ E('small'), E('span') ]);

dom.content(node.firstElementChild, render_signal_badge(
disabled ? -1 : radioNet.getSignalPercent(),
Expand Down Expand Up @@ -261,7 +261,7 @@ function network_updown(id, map, ev) {
const wifi_ifaces = uci.sections('wireless', 'wifi-iface');

wifi_ifaces.forEach(wifi_iface => {
if (wifi_iface.device == radio && wifi_iface.disabled != '1')
if (L.toArray(wifi_iface.device).indexOf(radio) != -1 && wifi_iface.disabled != '1')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

radio here is uci.get('wireless', id, 'device') from line 249,`` which for exactly the sections this PR adds support for is an array, not a radio name. So:

  • indexOf(radio) compares an array object against strings and never matches — all_networks_disabled stays true unconditionally.
  • Lines 251, 255 and 269 pass that array as a section id to uci.get/uci.set/uci.unset. resolveSID() returns non-strings unchanged, so the lookup key becomes the stringified array ("radio0,radio1,radio2"); the read yields null and the write is dropped by the if (!v[conf]?.[sid]) return; guard.

The user-visible consequence is a state mismatch with the row button: renderRowActions() computes isDisabled from uci.get('wireless', inst.getWifiDeviceName(), 'disabled') — a real radio name — so when the radio is disabled at the wifi-device level the row reads "Enable", but network_updown() evaluates disabled as false (the radio term is null) and disables the section instead of enabling it. Clearing the radio's disabled flag never happens either.

Since the row is already per-radio, the cleanest fix is to hand the radio name in from renderRowActions() (it already has inst.getWifiDeviceName()) rather than re-deriving it from device inside this function.


Generated by Claude Code

all_networks_disabled = false;
});

Expand Down Expand Up @@ -699,7 +699,7 @@ return view.extend({
rows.forEach(row => {
const section_id = row.getAttribute('data-sid');
const radioDev = data[1].filter(function(d) { return d.getName() == section_id; })[0];
const radioNet = data[2].filter(function(n) { return n.getName() == section_id; })[0];
const radioNet = data[2].filter(function(n) { return n.getID() == section_id || n.getName() == section_id; })[0];
const badge = row.querySelector('[data-name="_badge"] > div');
const stat = row.querySelector('[data-name="_stat"]');
const btns = row.querySelectorAll('.cbi-section-actions button');
Expand Down Expand Up @@ -816,7 +816,10 @@ return view.extend({
const status = document.querySelector('.cbi-modal [data-name="_wifistat_modal"] .ifacebadge.large');

if (status)
render_modal_status(status, data[2].filter(function(n) { return n.getName() == status.getAttribute('data-network'); })[0]);
render_modal_status(status, data[2].filter(function(n) {
const key = status.getAttribute('data-network');
return n.getID() == key || n.getName() == key;
})[0]);

return network.flushCache();
},
Expand Down Expand Up @@ -917,25 +920,45 @@ return view.extend({
rv.push(radio.getName());

this.wifis.forEach(wifi => {
if (wifi.getWifiDeviceName() == radio.getName())
rv.push(wifi.getName());
if (wifi.getWifiDeviceName() != radio.getName())
return;

/* Sections spanning several radios render one row per radio
* and need the netid to stay unique. Single-radio sections
* keep their UCI section name as before. */
const multiradio = (L.toArray(uci.get('wireless', wifi.getName(), 'device')).length > 1);

rv.push(multiradio ? (wifi.getID() || wifi.getName()) : wifi.getName());
});
});

return rv;
};

s.modaltitle = function(section_id) {
const radioNet = this.wifis.filter(function(w) { return w.getName() == section_id; })[0];
const radioNet = this.editWifiNetwork || this.wifis.filter(function(w) { return w.getID() == section_id || w.getName() == section_id; })[0];
return radioNet ? radioNet.getI18n() : _('Edit wireless network');
};

s.renderMoreOptionsModal = function(section_id, ev) {
const inst = this.lookupRadioOrNetwork(section_id);

this.editWifiNetwork = null;

if (inst && !inst.getWifiNetworks) {
this.editWifiNetwork = inst;
section_id = inst.getName();
}

return form.GridSection.prototype.renderMoreOptionsModal.apply(this, [section_id, ev]);
};

s.lookupRadioOrNetwork = function(section_id) {
const radioDev = this.radios.filter(function(r) { return r.getName() == section_id; })[0];
if (radioDev)
return radioDev;

const radioNet = this.wifis.filter(function(w) { return w.getName() == section_id; })[0];
const radioNet = this.wifis.filter(function(w) { return w.getID() == section_id || w.getName() == section_id; })[0];
if (radioNet)
return radioNet;

Expand Down Expand Up @@ -966,14 +989,15 @@ return view.extend({
];
}
else {
const sid = inst.getName();
const isDisabled = (inst.get('disabled') == '1' ||
uci.get('wireless', inst.getWifiDeviceName(), 'disabled') == '1');

btns = [
E('button', {
'class': 'cbi-button cbi-button-neutral enable-disable',
'title': isDisabled ? _('Enable this network') : _('Disable this network'),
'click': ui.createHandlerFn(this, network_updown, section_id, this.map)
'click': ui.createHandlerFn(this, network_updown, sid, this.map)
}, isDisabled ? _('Enable') : _('Disable')),
E('button', {
'class': 'cbi-button cbi-button-action important',
Expand All @@ -992,7 +1016,11 @@ return view.extend({
};

s.addModalOptions = function(s) {
const editWifiNetwork = this.editWifiNetwork;

return network.getWifiNetwork(s.section).then(function(radioNet) {
radioNet = editWifiNetwork || radioNet;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This correctly binds the modal to the clicked radio, but the modal's Status field does not stay bound to it.

_wifistat_modal renders from this radioNet on open, yet render_modal_status() stamps data-network with radioNet.getName() — the shared sid — at wireless.js:183, and [`poll_status()` resolves it back with `n.getName() == ...` at wireless.js:819](https://github.com/openwrt/luci/blob/69020328f255129392a3b3beabe59615f2e79711/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js#L819).`` For a multi-radio section every per-radio instance shares that sid, so [0] always picks the first radio. Opening the 5 GHz row therefore shows the correct BSSID/channel/signal initially and then flips to the first radio's values on the first poll tick.

Keying the badge on the netid and matching it with the same getID() == ... || getName() == ... pattern you already use for rows on line 703 would keep it consistent.


Generated by Claude Code


const hwtype = uci.get('wireless', radioNet.getWifiDeviceName(), 'type');
const have_mesh = L.hasSystemFeature('hostapd', 'mesh') || L.hasSystemFeature('wpasupplicant', 'mesh');
let o, ss;
Expand Down Expand Up @@ -2215,8 +2243,38 @@ return view.extend({
};

s.handleRemove = function(section_id, ev) {
document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_id)).style.opacity = 0.5;
return form.TypedSection.prototype.handleRemove.apply(this, [section_id, ev]);
const inst = this.lookupRadioOrNetwork(section_id);
let sid = section_id;

if (inst && !inst.getWifiNetworks) {
sid = inst.getName();

const radioname = inst.getWifiDeviceName();
const devices = L.toArray(uci.get('wireless', sid, 'device'));

if (devices.length > 1 && devices.indexOf(radioname) != -1) {
const row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_id));

if (row)
row.style.opacity = 0.5;

uci.set('wireless', sid, 'device', devices.filter(d => d != radioname));

return this.map.save(null, true);
}
}

this.wifis.forEach(wifi => {
if (wifi.getName() != sid)
return;

const row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(wifi.getID() || wifi.getName()));

if (row)
row.style.opacity = 0.5;
});

return form.TypedSection.prototype.handleRemove.apply(this, [sid, ev]);
};

s.handleScan = function(radioDev, ev) {
Expand Down Expand Up @@ -2380,13 +2438,13 @@ return view.extend({

if (replopt.formvalue('_new_') == '1') {
for (let ws of wifi_sections)
if (ws.device == radioDev.getName())
if (L.toArray(ws.device).indexOf(radioDev.getName()) != -1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: widening this to a membership test changes what "Replace wireless configuration" does to multi-radio sections. Before this PR they were skipped here (ws.device was not a string); now joining a network on one radio removes the whole shared wifi-iface, tearing down the BSSes it also provides on the other radios. The same applies to the disabled propagation on line 2445, which now marks the shared section disabled on every radio it spans.

Is that the intent, or should the join flow drop just radioDev from the section's device list — the way handleRemove() now does — and only delete the section when it is the last radio? The PR description covers per-radio Remove but not this path.


Generated by Claude Code

uci.remove('wireless', ws['.name']);
}

if (uci.get('wireless', radioDev.getName(), 'disabled') == '1') {
for (let ws of wifi_sections)
if (ws.device == radioDev.getName())
if (L.toArray(ws.device).indexOf(radioDev.getName()) != -1)
uci.set('wireless', ws['.name'], 'disabled', '1');

uci.unset('wireless', radioDev.getName(), 'disabled');
Expand Down