web: address a link aggregation group as one spanning tree entity

The firmware lists a LAG as its own entity in /stp.json (`lag` and `mbr`,
`p` 0) and drops the member ports from the list; its settings take
`stp lag <n>`. Give the group its own row in both spanning tree tables,
named with its members, key the cells by entity rather than port number,
and rebuild the tables whenever the entity set or a membership changes so
a group created on the LAG tab appears without a reload. The `stp lag`
commands are recognised for the unsaved-changes badge and the startup
config merge alongside `stp port`.
This commit is contained in:
bloqaudio committed 2026-09-14 08:14:53 -05:00
1 parent 206c72b5ef
commit 115ea09ebb
1 file changed
+20 -16
+20 -16
View File
@@ -440,10 +440,10 @@ var CONF_CMDS=[
/^isolate\s+\d{1,2}(\s+(off|\d{1,2}))+$/,
/^stp\s+(on|off)$/,/^stp\s+(prio|hello|maxage|fwd|txhold)\s+\d{1,2}$/,
/^stp\s+version\s+(rstp|stp)$/,
/^stp\s+port\s+\d{1,2}\s+(on|off)$/,/^stp\s+port\s+\d{1,2}\s+edge\s+(on|off|auto)$/,
/^stp\s+port\s+\d{1,2}\s+cost\s+\d{1,9}$/,/^stp\s+port\s+\d{1,2}\s+prio\s+\d{1,3}$/,
/^stp\s+port\s+\d{1,2}\s+guard\s+(none|bpdu|root)$/,/^stp\s+port\s+\d{1,2}\s+filter\s+(on|off)$/,
/^stp\s+port\s+\d{1,2}\s+p2p\s+(auto|on|off)$/,
/^stp\s+(port\s+\d{1,2}|lag\s+[1-4])\s+(on|off)$/,/^stp\s+(port\s+\d{1,2}|lag\s+[1-4])\s+edge\s+(on|off|auto)$/,
/^stp\s+(port\s+\d{1,2}|lag\s+[1-4])\s+cost\s+\d{1,9}$/,/^stp\s+(port\s+\d{1,2}|lag\s+[1-4])\s+prio\s+\d{1,3}$/,
/^stp\s+(port\s+\d{1,2}|lag\s+[1-4])\s+guard\s+(none|bpdu|root)$/,/^stp\s+(port\s+\d{1,2}|lag\s+[1-4])\s+filter\s+(on|off)$/,
/^stp\s+(port\s+\d{1,2}|lag\s+[1-4])\s+p2p\s+(auto|on|off)$/,
/^igmp\s+(on|off)$/,/^mtu\s+\d{1,2}\s+\d+$/,
/^bw\s+(in|out)\s+\d{1,2}\s+\S+$/,
];
@@ -815,7 +815,7 @@ tabHooks.ports={
};
var STP_PF={EN:1,ADMEDGE:2,AUTOEDGE:4,BPDUG:8,ROOTG:16,FILTER:32,OPEREDGE:64,TRIP:128};
var stpRows=0,stpCur=null;
var stpSig="",stpCur=null;
function stpSel(id,opts){
var s=h("select",{class:"in",id:id,onchange:stpTouch});
opts.forEach(function(o){s.appendChild(h("option",{value:o[0],text:o[1]}))});
@@ -826,12 +826,16 @@ function fmtBridge(hex){
if(!hex||hex.length<16)return"-";
return parseInt(hex.slice(0,4),16)+" / "+hex.slice(4).replace(/(..)(?=.)/g,"$1:");
}
function stpKey(pt){return pt.lag?"L"+pt.lag:String(pt.p)}
function stpName(pt){return pt.lag?"LAG"+pt.lag+" ("+maskToPorts(pt.mbr).join(",")+")":String(pt.p)}
function stpPre(k){return k.charAt(0)==="L"?"stp lag "+k.slice(1)+" ":"stp port "+k+" "}
function stpBuild(ports){
var tb=$("stpcfg").tBodies[0],st=$("stpstat").tBodies[0];
tb.innerHTML="";st.innerHTML="";
ports.forEach(function(pt){
var p=pt.p;
var p=stpKey(pt);
var tr=tb.insertRow();
tr.insertCell().textContent=p;
tr.insertCell().textContent=stpName(pt);
var sw=h("label",{class:"switch"},[h("input",{type:"checkbox",id:"sten"+p,onchange:stpTouch}),h("i")]);
tr.insertCell().appendChild(sw);
tr.insertCell().appendChild(stpSel("sted"+p,[["auto",t("c_auto")],["on",t("c_on")],["off",t("c_offc")]]));
@@ -844,10 +848,9 @@ function stpBuild(ports){
tr.insertCell().appendChild(stpSel("stpp"+p,[["auto",t("c_auto")],["on",t("c_on")],["off",t("c_offc")]]));
tr.insertCell().appendChild(h("button",{class:"ctl",text:t("c_apply"),onclick:function(){stpApplyPort(p)}}));
var sr=st.insertRow();
sr.insertCell().textContent=p;
sr.insertCell().textContent=stpName(pt);
["ro","st","db","dp","dc","oe","op"].forEach(function(k){sr.insertCell().id="st"+k+p});
});
stpRows=ports.length;
}
function stpPortVals(pt){
var f=pt.f;
@@ -862,8 +865,9 @@ function stpPortVals(pt){
}
function stpLoad(){
return getJSON("/stp.json").then(function(s){
s.ports.sort(function(a,b){return a.p-b.p});
if(!stpRows)stpBuild(s.ports);
s.ports.sort(function(a,b){return((a.lag||0)-(b.lag||0))||(a.p-b.p)});
var sig=s.ports.map(function(pt){return stpKey(pt)+":"+(pt.mbr||0)}).join();
if(sig!==stpSig){stpSig=sig;stpBuild(s.ports);}
stpCur=s;
var en=$("stpen");
if(document.activeElement!==en)en.checked=!!s.on;
@@ -885,7 +889,7 @@ function stpLoad(){
$("stpfwd").value=s.fwd;$("stptxhold").value=s.txhold;
}
s.ports.forEach(function(pt){
var p=pt.p;
var p=stpKey(pt);
var trip=(pt.f&STP_PF.TRIP)?" "+badge(t("stp_trip"),"bad"):"";
$("stro"+p).textContent=s.on&&pt.role?t("stp_r"+pt.role):"-";
$("stst"+p).innerHTML=s.on?badge(t("stp_s"+pt.st),pt.st===3?"ok":"")+trip:"-";
@@ -904,12 +908,12 @@ function stpLoad(){
}
function stpApplyPort(p){
var cur=null;
(stpCur?stpCur.ports:[]).forEach(function(pt){if(pt.p===p)cur=stpPortVals(pt)});
(stpCur?stpCur.ports:[]).forEach(function(pt){if(stpKey(pt)===p)cur=stpPortVals(pt)});
var cost=parseInt($("stco"+p).value,10);
if(isNaN(cost)||cost<0||cost>200000000){toast(t("stp_cost_err"),"err");return;}
var w={en:$("sten"+p).checked,edge:$("sted"+p).value,cost:cost,prio:$("stpr"+p).value,
guard:$("stgu"+p).value,filter:$("stfi"+p).value,p2p:$("stpp"+p).value};
var cmds=[],pre="stp port "+p+" ";
var cmds=[],pre=stpPre(p);
if(!cur||w.edge!==cur.edge)cmds.push(pre+"edge "+w.edge);
if(!cur||w.cost!==cur.cost)cmds.push(pre+"cost "+w.cost);
if(!cur||w.prio!==cur.prio)cmds.push(pre+"prio "+w.prio);
@@ -1615,10 +1619,10 @@ var CONF_OVERWRITE=[
/^eee\s+\d{1,2}\b/,/^eee\b/,/^mirror\b/,
/^lag\s+\d\b/,/^laghash\s+\d\b/,/^isolate\s+\d{1,2}\b/,
/^stp\s+(prio|hello|maxage|fwd|txhold|version)\b/,
/^stp\s+port\s+\d{1,2}\s+(edge|cost|prio|guard|filter|p2p)\b/,
/^stp\s+(port\s+\d{1,2}|lag\s+[1-4])\s+(edge|cost|prio|guard|filter|p2p)\b/,
/^igmp\b/,/^mtu\s+\d{1,2}\b/,
];
var CONF_TOGGLE=[/^(syslog)\s+(on|off)$/,/^(stp)\s+(on|off)$/,/^(stp\s+port\s+\d{1,2})\s+(on|off)$/];
var CONF_TOGGLE=[/^(syslog)\s+(on|off)$/,/^(stp)\s+(on|off)$/,/^(stp\s+(port\s+\d{1,2}|lag\s+[1-4]))\s+(on|off)$/];
function mergeConf(base,texts){
var conf=base.slice();
function drop(rx){conf=conf.filter(function(c){return!rx.test(c)})}