From 1d359afba5fedb9b54d17b9a3d49c128da9a0d2e Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Tue, 13 Oct 2026 16:29:03 +0100 Subject: [PATCH 01/13] Adding vlanlist support into http_sim so it's easier to work on Also improving vlan.json to mirror the real output --- tools/httpd_sim.c | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/tools/httpd_sim.c b/tools/httpd_sim.c index 68f87232..a9452c90 100644 --- a/tools/httpd_sim.c +++ b/tools/httpd_sim.c @@ -19,6 +19,7 @@ #define NSFP 1 #define N_COUNTERS 52 +#define VLAN_MAX 1 #if PORTS == 9 const uint8_t physToLogPort[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8}; @@ -135,16 +136,49 @@ void send_vlan(int s, int vlan) v = json_object_new_object(); - sprintf(num_buff, "0x%08x", 0x00060011); + sprintf(num_buff, "0x%08x", 0x207ffff); json_object_object_add(v, "members", json_object_new_string(num_buff)); - write(s, header, strlen(header)); + json_object_object_add(v, "name", json_object_new_string("")); + json_object_object_add(v, "pvid", json_object_new_string("0x01ff")); + + write(s, header, strlen(header)); jstring = json_object_to_json_string_ext(v, JSON_C_TO_STRING_PLAIN); - write(s, jstring, strlen(jstring)); + write(s, jstring, strlen(jstring)); json_object_put(v); } +void send_vlanlist(int s) +{ + struct json_object *v, *vlans, *vlan; + const char *jstring; + char *header = "HTTP/1.1 200 OK\r\n" + "Content-Type: application/json; charset=UTF-8\r\n\r\n"; + + v = json_object_new_object(); + + json_object_object_add(v, "mgmt", json_object_new_int(1)); + + vlans = json_object_new_array_ext(VLAN_MAX); + + //foreach VLAN + for (int x=1;x<=VLAN_MAX;++x) { + vlan = json_object_new_object(); + json_object_object_add(vlan, "id", json_object_new_int(x)); + json_object_object_add(vlan, "name", json_object_new_string("")); + + json_object_array_add(vlans, vlan); + } + + + json_object_object_add(v, "vlan", vlans); + + write(s, header, strlen(header)); + jstring = json_object_to_json_string_ext(v, JSON_C_TO_STRING_PLAIN); + write(s, jstring, strlen(jstring)); + json_object_put(v); +} void send_status(int s) { @@ -735,6 +769,13 @@ void launch(struct Server *server) else send_vlan(new_socket, vlan); goto done; + } else if (!strncmp(&buffer[4], "/vlanlist", 9)) { + printf("request for vlan list"); + if (!authenticated) + send_unauthorized(new_socket); + else + send_vlanlist(new_socket); + goto done; } else if (!strncmp(&buffer[4], "/lldp.json", 10)) { printf("LLDP request"); if (!authenticated) From 48087d3520cea395ae7a3f7dc9f7d65cdb04d82f Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Tue, 13 Oct 2026 17:16:19 +0100 Subject: [PATCH 02/13] Decoupling the load from the spinner so we can get rid of it Also making the first VLAN in the table load when we load the UI Removing the tickbox for the PVID, I'll swap this out to ticks or something later on, but the PVID will be automatically set based on the untagged membership, otherwise it will be 1 Send the vlan with name command when attempting to add a new VLAN Because of the structure of the vlan command, we need to send the name as well if its set Set the PVID based on the untagged membership and reload the table when applied. We need to block setting the PVID to the tagged by proxy Blue button Need to provide array of pvid commands not concat Storing state of each vlan so we can refer later Blue button --- html/app.js | 361 ++++++++++++++++++++++++++++++++++------------ html/index.html | 12 +- tools/httpd_sim.c | 105 ++++++++++++-- 3 files changed, 369 insertions(+), 109 deletions(-) diff --git a/html/app.js b/html/app.js index 97960236..613ff769 100644 --- a/html/app.js +++ b/html/app.js @@ -1,6 +1,7 @@ "use strict"; var LANG={ en:{ +ROBTODO:"TODOTODOTODO", nav_dash:"Dashboard",nav_ports:"Ports",nav_stp:"Spanning tree",nav_stats:"Statistics", nav_vlan:"VLANs",nav_l2:"MAC table",nav_mirror:"Mirroring",nav_isolate:"Isolation",nav_lag:"LAG",nav_eee:"EEE", nav_bw:"Bandwidth",nav_system:"System",nav_fw:"Firmware",nav_lldp:"LLDP", @@ -47,8 +48,8 @@ st_autoref:"auto-refresh",st_counter:"Counter",st_value:"Value",st_fail:"failed v_title:"VLANs",v_vid:"VID",v_members:"Members",v_tagged:"Tagged",v_untagged:"Untagged",v_pvid_on:"PVID on", v_empty:"No VLANs configured.",v_mgmt:"Management VLAN",v_mgmt_none:"untagged",v_editor:"VLAN editor", v_optional:"optional",v_setmgmt:"Set as mgmt VLAN",v_setmgmt_t:"Make this the management VLAN", -v_member:"Member",v_pvid:"PVID", -v_legend:"U = untagged member, T = tagged member, - = not a member. PVID assigns this VLAN to untagged ingress traffic.", +v_member:"Member",v_pvid:"PVID",v_add:"Add a new VLAN", +v_legend:"U = untagged member, T = tagged member, - = not a member. PVID is automatically assigned based on the untagged membership.", v_ingress:"Ingress filtering",v_ingress_h:"write-only: the state is not readable from the switch", v_accept:"Accept",v_ing_all:"All",v_ing_apply:"Apply ingress modes",v_del_t:"Delete VLAN", v_del_q:"Delete VLAN {n}?",v_del_d:"Ports keep their PVID until reassigned.", @@ -1464,60 +1465,103 @@ function rangeStr(list){ } return parts.join(", "); } -function vlanRefresh(){ - var tb=$("vtable").tBodies[0]; - return getJSON("/vlanlist").then(function(d){ - var vl=d.vlan||[]; - $("vmgmtcur").textContent=d.mgmt?String(d.mgmt):t("v_mgmt_none"); - $("vempty").style.display=vl.length?"none":""; - tb.innerHTML=""; - var p=Promise.resolve(); - vl.forEach(function(v){ - p=p.then(function(){return getJSON("/vlan.json?vid="+v.id).catch(function(){return null})}).then(function(d){ - var tr=tb.insertRow(); - tr.insertCell().appendChild(h("a",{href:"#vlan",text:String(v.id),onclick:function(e){ - e.preventDefault();$("vvid").value=v.id;vlanLoad(); - }})); - tr.insertCell().textContent=v.name||""; - if(!d){for(var c=0;c<4;c++)tr.insertCell().textContent="?";} - else{ - var m=parseInt(d.members,16),mem=m&0x3ff,unt=((m>>10)&0x3ff)&mem; - var pv=parseInt(d.pvid,16)&0x3ff; - tr.insertCell().textContent=rangeStr(maskToPorts(mem)); - tr.insertCell().textContent=rangeStr(maskToPorts(mem&~unt)); - tr.insertCell().textContent=rangeStr(maskToPorts(unt)); - tr.insertCell().textContent=rangeStr(maskToPorts(pv)); +//ROBSTART +function vlanRefresh() { + var tb = $("vtable").tBodies[0]; + return getJSON("/vlanlist").then(function(d) { + var vl = d.vlan || []; + $("vmgmtcur").textContent = d.mgmt ? String(d.mgmt) : t("v_mgmt_none"); + $("vempty").style.display = vl.length ? "none" : ""; + tb.innerHTML = ""; + var p = Promise.resolve(); + vl.forEach(function(v) { + p = p.then(function() { + return getJSON("/vlan.json?vid=" + v.id).catch(function() { + return null + }) + }).then(function(d) { + var tr = tb.insertRow(); + var l = h("a", { + href: "#vlan", + text: String(v.id), + "data-id": v.id, + "data-name": v.name, + onclick: function(e) { + e.preventDefault(); + vlanLoad(v.id); + } + }); + tr.insertCell().appendChild(l); + tr.insertCell().textContent = v.name || ""; + if (!d) { + for (var c = 0; c < 4; c++) tr.insertCell().textContent = "?"; + } else { + var m = parseInt(d.members, 16), + mem = m & 0x3ff, + unt = ((m >> 10) & 0x3ff) & mem, + tag = mem & ~unt; + var pv = parseInt(d.pvid, 16) & 0x3ff; + tr.insertCell().textContent = rangeStr(maskToPorts(mem)); + tr.insertCell().textContent = rangeStr(maskToPorts(tag)); + tr.insertCell().textContent = rangeStr(maskToPorts(unt)); + tr.insertCell().textContent = rangeStr(maskToPorts(pv)); + l.dataset.t = maskToPorts(tag); + l.dataset.u = maskToPorts(unt); } - var del=tr.insertCell(); - if(v.id!==1)del.appendChild(h("button",{class:"ctl",text:"\u2715",title:t("v_del_t"),onclick:function(){ - confirmModal(t("v_del_q",{n:v.id}),t("v_del_d"),function(){ - postCmd("vlan "+v.id+" d").then(vlanRefresh).catch(function(){}); - }); - }})); - }).catch(function(){}); + var del = tr.insertCell(); + if (v.id !== 1) del.appendChild(h("button", { + class: "ctl", + text: "\u2715", + title: t("v_del_t"), + onclick: function() { + confirmModal(t("v_del_q", { + n: v.id + }), t("v_del_d"), function() { + postCmd("vlan " + v.id + " d").then(vlanRefresh).catch(function() {}); + }); + } + })); + }).catch(function() {}); }); return p; }); } -function buildVlanEdit(){ - var tb=$("vedit").tBodies[0]; - if(tb.rows.length||!S.n)return; - var hd=tb.insertRow();hd.insertCell().className="mut"; - var rM=tb.insertRow();rM.insertCell().textContent=t("v_member"); - var rP=tb.insertRow();rP.insertCell().textContent=t("v_pvid"); - for(var p=1;p<=S.n;p++)(function(p){ - hd.insertCell().innerHTML=''+p+''; - var seg=h("span",{class:"seg",id:"vm"+p}); - ["-","U","T"].forEach(function(s,ix){ - seg.appendChild(h("button",{text:s,"data-v":ix,onclick:function(){ - seg.querySelectorAll("button").forEach(function(b){b.classList.remove("on")}); - this.classList.add("on"); - }})); + +function buildVlanEdit() { + var tb = $("vedit").tBodies[0]; + if (tb.rows.length || !S.n) return; + var hd = tb.insertRow(); + hd.insertCell().className = "mut"; + var rM = tb.insertRow(); + rM.insertCell().textContent = t("v_member"); + var rP = tb.insertRow(); + rP.insertCell().textContent = t("v_pvid"); + for (var p = 1; p <= S.n; p++)(function(p) { + hd.insertCell().innerHTML = '' + p + ''; + var seg = h("span", { + class: "seg", + id: "vm" + p + }); + ["-", "U", "T"].forEach(function(s, ix) { + seg.appendChild(h("button", { + text: s, + "data-v": ix, + onclick: function() { + seg.querySelectorAll("button").forEach(function(b) { + b.classList.remove("on") + }); + this.classList.add("on"); + } + })); }); seg.children[0].classList.add("on"); rM.insertCell().appendChild(seg); - rP.insertCell().appendChild(h("input",{type:"checkbox",id:"vp"+p})); + rP.insertCell().appendChild(h("span", { + id: "vp" + p + })); })(p); + //ROBTEST TODO + /* var it=$("ingress").tBodies[0]; var ih=it.insertRow();ih.insertCell().className="mut"; var ir=it.insertRow();ir.insertCell().textContent=t("v_accept"); @@ -1528,67 +1572,200 @@ function buildVlanEdit(){ sel.appendChild(h("option",{value:o[0],text:o[1]})); }); ir.insertCell().appendChild(sel); - })(q); + })(q);*/ } -function segVal(p){ - return Number($("vm"+p).querySelector("button.on").getAttribute("data-v")); + +function segVal(p) { + return Number($("vm" + p).querySelector("button.on").getAttribute("data-v")); } -function segSet(p,v){ - $("vm"+p).querySelectorAll("button").forEach(function(b,i){b.classList.toggle("on",i===v)}); + +function segSet(p, v) { + $("vm" + p).querySelectorAll("button").forEach(function(b, i) { + b.classList.toggle("on", i === v) + }); } -function vlanLoad(){ - var vid=parseInt($("vvid").value,10); - if(!vid||vid<1||vid>4094){toast(t("v_vid_err"),"err");return;} - getJSON("/vlan.json?vid="+vid).then(function(d){ - $("vname").value=d.name||""; - var m=parseInt(d.members,16),mem=m&0x3ff,unt=((m>>10)&0x3ff)&mem; - var pv=parseInt(d.pvid,16)&0x3ff; - for(var p=1;p<=S.n;p++){ - var bit=S.physToLog[p-1]; - segSet(p,(mem>>bit)&1?(((unt>>bit)&1)?1:2):0); - $("vp"+p).checked=!!((pv>>bit)&1); + +function vlanLoad(vid) { + if (!vid || vid < 1 || vid > 4094) { + toast(t("v_vid_err"), "err"); + return; + } + getJSON("/vlan.json?vid=" + vid).then(function(d) { + $("vtitle").innerText = "VLAN " + vid + (d.name ? " (" + (d.name) + ")" : ""); + $("vtitle").dataset.id = vid; + $("vtitle").dataset.name = d.name; + var m = parseInt(d.members, 16), + mem = m & 0x3ff, + unt = ((m >> 10) & 0x3ff) & mem; + var pv = parseInt(d.pvid, 16) & 0x3ff; + for (var p = 1; p <= S.n; p++) { + var bit = S.physToLog[p - 1]; + segSet(p, (mem >> bit) & 1 ? (((unt >> bit) & 1) ? 1 : 2) : 0); + $("vp" + p).innerText = !!((pv >> bit) & 1); } - toast(t("v_loaded",{n:vid}),"ok"); - }).catch(function(){toast(t("v_notfound",{n:vid}),"err")}); + toast(t("v_loaded", { + n: vid + }), "ok"); + }).catch(function() { + toast(t("v_notfound", { + n: vid + }), "err") + }); } -function vlanApply(){ - var vid=parseInt($("vvid").value,10); - if(!vid||vid<1||vid>4094){toast(t("v_vid_err"),"err");return;} - var name=$("vname").value.trim(); - if(name&&!/^[a-zA-Z]\w*$/.test(name)){toast(t("v_name_err"),"err");return;} - var cmd="vlan "+vid+(name?" "+name:""),members=0; - for(var p=1;p<=S.n;p++){ - var v=segVal(p); - if(v===1){cmd+=" "+p;members++;} - else if(v===2){cmd+=" "+p+"t";members++;} + +function vlan2str(i, n, t, u) { + return ("vlan " + i + (n ? " " + n : "") + " " + t.map((x)=>x+"t").concat(u).join(" ")).trim(); +} +function pvidcmd(i, p){ + return "pvid " + p + " " + i; +} +function vlanApply() { + var vid = $("vtitle").dataset.id; + var vname = $("vtitle").dataset.name; + if (!vid || vid < 1 || vid > 4094) { + toast(t("v_vid_err"), "err"); + return; } - if(!members){toast(t("v_nomember"),"err");return;} - var cmds=[cmd]; - for(var q=1;q<=S.n;q++)if($("vp"+q).checked)cmds.push("pvid "+q+" "+vid); - postCmds(cmds).then(vlanRefresh).catch(function(){}); -} -function ingressApply(){ - var cmd="ingress",any=false; - for(var p=1;p<=S.n;p++){ - var v=$("ing"+p).value; - if(v){cmd+=" "+p+v;any=true;} + var top_cmds = [], + tail_cmds = [], + thisVlanInStructure={id:vid, tagged:[], untagged:[]}; + + for (var p = 1; p <= S.n; p++) { + var v = segVal(p); + if (v === 1) { + thisVlanInStructure.untagged.push(p); + } else if (v === 2) { + thisVlanInStructure.tagged.push(p); + } } - if(!any){toast(t("v_ing_none"),"err");return;} - postCmd(cmd).catch(function(){}); + if (thisVlanInStructure.tagged.length + thisVlanInStructure.untagged.length == 0) { + toast(t("v_nomember"), "err"); + return; + } + function mySplit(a){ + if(a=="") return []; + return a.split(","); + } + for(const vlanRow of $("vtable").tBodies[0].rows){ + var rowData = vlanRow.firstChild.firstChild.dataset, + rowUntagged = mySplit(rowData.u), + rowTagged = mySplit(rowData.t); + if(rowData.id == vid) continue; + for(const newVlanPort of thisVlanInStructure.untagged) { + if(rowUntagged.includes(""+newVlanPort)){ + rowUntagged.splice(rowUntagged.indexOf(""+newVlanPort),1); + } + } + top_cmds.push(vlan2str(rowData.id,rowData.name,rowTagged,rowUntagged)); + } + tail_cmds.push(vlan2str(vid, vname, thisVlanInStructure.tagged, thisVlanInStructure.untagged)); + //TODO: if the PVID is equal to this vid, we need to set it something else (probably 1) + + for(const up of thisVlanInStructure.untagged){ + tail_cmds.push(pvidcmd(vid,up)); + } + + var cmds = top_cmds.concat(tail_cmds); + console.log(cmds); + + postCmds(cmds).then(vlanRefresh).catch(function() {}).then(function() { + vlanLoad(vid) + }); } -$("vload").addEventListener("click",vlanLoad); -$("vapply").addEventListener("click",vlanApply); -$("ingapply").addEventListener("click",ingressApply); + +function ingressApply() { + var cmd = "ingress", + any = false; + for (var p = 1; p <= S.n; p++) { + var v = $("ing" + p).value; + if (v) { + cmd += " " + p + v; + any = true; + } + } + if (!any) { + toast(t("v_ing_none"), "err"); + return; + } + postCmd(cmd).catch(function() {}); +} +$("vapply").addEventListener("click", vlanApply); +$("vlan_add").addEventListener("click", function() { + var body = h("div"); + body.appendChild(document.createTextNode("ROBTODO: styling,layout etc")); + body.appendChild(h("p", null, [ + h("label", null, [document.createTextNode(t("v_vid"))]), + h("input", { + id: "nvlid", + class: "in sm", + type: "number", + min: "1", + max: "4094" + }) + ])); + body.appendChild(h("p", null, [ + h("label", null, [document.createTextNode(t("c_name"))]), + h("input", { + id: "nvlname", + class: "in", + size: "12", + maxlength: "15", + "data-i18n-p": "v_opt ional" + }) + ])); + modal("Add VLAN", body, [h("button", { + class: "ctl pri", + text: t("ROBTODO"), + onclick: function() { + addVlan() + } + })]); +}); + +function addVlan() { + var vid = parseInt($("nvlid").value, 10); + for (const r of $("vtable").tBodies[0].rows) { + if (r.firstChild.firstChild.dataset.id == vid) { + toast(t("ROBTODO"), "err"); + return; + } + } + var name = $("nvlname").value; + if (name && !/^[a-zA-Z]\w*$/.test(name)) { + toast(t("v_name_err"), "err"); + return; + } + var cmd = "vlan " + vid + (name ? " " + name : ""); + postCmd(cmd).then(function() { + closeModal(); + vlanRefresh() + }).catch(function() {}); +} +//ROBTODO +/* +//$("ingapply").addEventListener("click",ingressApply); $("vmgmt").addEventListener("click",function(){ var vid=parseInt($("vvid").value,10); if(!vid){toast(t("v_vid_first"),"err");return;} confirmModal(t("v_mgmt_q",{n:vid}),t("v_mgmt_d"), function(){postCmd("vlan "+vid+" mgmt").then(vlanRefresh).catch(function(){})}); }); -tabHooks.vlan={ - enter:function(){needPorts(function(){buildVlanEdit();vlanRefresh().catch(function(){})})}, +*/ +tabHooks.vlan = { + enter: function() { + needPorts(function() { + buildVlanEdit(); + vlanRefresh().catch(function() {}).then(function() { + VlanInit(); + }) + }) + }, }; +function VlanInit() { + $("vtable").tBodies[0].firstChild.firstChild.firstChild.click(); +}; +//ROBSTOP var l2Rows=[],l2SortCol="pport",l2SortDir=1,l2Gen=0; function l2Load(){ var seen={},all=[],idx=0,guard=0,gen=++l2Gen; diff --git a/html/index.html b/html/index.html index f042c0a8..74a2426e 100644 --- a/html/index.html +++ b/html/index.html @@ -291,22 +291,26 @@ button.danger{background:var(--bad);color:#fff;border-color:transparent} VIDNameMembersTaggedUntaggedPVID on + -

VLAN editor

-
+

VLAN editor

+
+

U = untagged member, T = tagged member, - = not a member. PVID assigns this VLAN to untagged ingress traffic.

+
diff --git a/tools/httpd_sim.c b/tools/httpd_sim.c index a9452c90..64654aac 100644 --- a/tools/httpd_sim.c +++ b/tools/httpd_sim.c @@ -19,7 +19,9 @@ #define NSFP 1 #define N_COUNTERS 52 -#define VLAN_MAX 1 +#define VLAN_MAX_ID 4094 + +int currentVLANCount = 1; #if PORTS == 9 const uint8_t physToLogPort[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8}; @@ -27,6 +29,14 @@ const uint8_t physToLogPort[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8}; const uint8_t physToLogPort[] = { 4, 5, 6, 7, 3, 8}; #endif +struct mem_vlan { + bool active; + char name[20]; + bool untagged[PORTS]; + bool tagged[PORTS]; +}; +struct mem_vlan mem_vlans[VLAN_MAX_ID+1]; +//mem_vlans[1] = {.active=true, .name="", .untagged={}, .tagged={}}; struct l2_entry { char mac[18]; char vlan[4]; @@ -135,13 +145,20 @@ void send_vlan(int s, int vlan) "Content-Type: application/json; charset=UTF-8\r\n\r\n"; v = json_object_new_object(); + if(vlan == 1) { + sprintf(num_buff, "0x%08x", 0x207ffff); + json_object_object_add(v, "members", json_object_new_string(num_buff)); + + json_object_object_add(v, "name", json_object_new_string("native")); + json_object_object_add(v, "pvid", json_object_new_string("0x01ff")); + } else { + if(mem_vlans[vlan].active) { + json_object_object_add(v, "name", json_object_new_string(mem_vlans[vlan].name)); + json_object_object_add(v, "members", json_object_new_string("0x0000")); + json_object_object_add(v, "pvid", json_object_new_string("0x0000")); + } + } - sprintf(num_buff, "0x%08x", 0x207ffff); - json_object_object_add(v, "members", json_object_new_string(num_buff)); - - json_object_object_add(v, "name", json_object_new_string("")); - json_object_object_add(v, "pvid", json_object_new_string("0x01ff")); - write(s, header, strlen(header)); jstring = json_object_to_json_string_ext(v, JSON_C_TO_STRING_PLAIN); @@ -160,17 +177,18 @@ void send_vlanlist(int s) json_object_object_add(v, "mgmt", json_object_new_int(1)); - vlans = json_object_new_array_ext(VLAN_MAX); + vlans = json_object_new_array_ext(currentVLANCount); //foreach VLAN - for (int x=1;x<=VLAN_MAX;++x) { + for (int x=1;x<=VLAN_MAX_ID;++x) { vlan = json_object_new_object(); - json_object_object_add(vlan, "id", json_object_new_int(x)); - json_object_object_add(vlan, "name", json_object_new_string("")); + if(x ==1 || mem_vlans[x].active) { + json_object_object_add(vlan, "id", json_object_new_int(x)); + json_object_object_add(vlan, "name", json_object_new_string(mem_vlans[x].name)); - json_object_array_add(vlans, vlan); + json_object_array_add(vlans, vlan); + } } - json_object_object_add(v, "vlan", vlans); @@ -680,6 +698,65 @@ void print_cmd_history(void) printf("%d: %s\n", i, cmd_history[i]); } +void delete_vlan(int vid) { + mem_vlans[vid] = (struct mem_vlan){}; + currentVLANCount--; +} + +void parse_cmd(char* cmd) +{ + char *token = strtok(cmd, " "); + char *name; + printf("\t OK FIRST TOKEN: '%s'\n",token); + if (!strncmp(token, "vlan", 4)) { + printf("\t OK IT IS VLAN\n"); + token = strtok(NULL, " "); + int vid = atoi(token); + if (vid<1 || vid > VLAN_MAX_ID) { + printf("\t REJECT BECAUSE OF VID:%d\n",vid); + return; + } + + token = strtok(NULL, " "); + if(!strncmp(token,"mgmt",4)) { + printf("\t MGMT LEAVE:\n"); + return; //TODO + } else if (!strncmp(token,"d",1)) { + printf("\t DELETE VID %d:\n",vid); + delete_vlan(vid); + } else { + if (token[0] >= 'A' && token[0] <= 'z' ){ + name = token; + token = strtok(NULL, " "); + + printf("\tIS NAME %s:\n", name); + } + + struct mem_vlan newvlan = {.active=true}; + strcpy(newvlan.name, name); + + // a string of [0-9][t]? + printf("\t TOKEN: %s:\n", token); + while(token != NULL) { + int portNum = atoi(token); + bool isTagged = token[strlen(token)] == 't'; + printf("\tVLAN: %d Name: %s Port: %d tagged:%d\n", vid, name, portNum, isTagged); + token = strtok(NULL, " "); + if(isTagged) { + newvlan.tagged[portNum] = true; + } else { + newvlan.untagged[portNum] = true; + } + } + + mem_vlans[vid] = newvlan; + currentVLANCount++; + } + } else { + + } +} + void launch(struct Server *server) { char buffer[BUFFER_SIZE]; @@ -872,6 +949,8 @@ void launch(struct Server *server) printf("POST cmd\n"); printf("CMD: %s\n", p + 4); strcpy(cmd_history[cmd_ptr], p + 4); + printf("\t GOING TO PARSE THIS: %s\n", p+4); + parse_cmd(p+4); cmd_ptr++; print_cmd_history(); char *response = "HTTP/1.1 200 OK\r\n" From 834403b97bf73634c5a2e0ecbca093ec6838a344 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 17 Oct 2026 14:51:06 +0100 Subject: [PATCH 03/13] Parking ports with only tagged membership into VLAN 4094 for now I think this should also resolve the work-flow where you can get a port's PVID and tagged membership to the same vlan --- html/app.js | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/html/app.js b/html/app.js index 613ff769..e636ddbf 100644 --- a/html/app.js +++ b/html/app.js @@ -1619,6 +1619,11 @@ function vlan2str(i, n, t, u) { function pvidcmd(i, p){ return "pvid " + p + " " + i; } +const parkingVLAN=4094; + function mySplit(a){ + if(a.trim()=="") return []; + return a.trim().split(","); + } function vlanApply() { var vid = $("vtitle").dataset.id; var vname = $("vtitle").dataset.name; @@ -1628,24 +1633,24 @@ function vlanApply() { } var top_cmds = [], tail_cmds = [], - thisVlanInStructure={id:vid, tagged:[], untagged:[]}; + thisVlanInStructure={id:vid, tagged:[], untagged:[]}, + vlansPerPort=[]; for (var p = 1; p <= S.n; p++) { var v = segVal(p); + vlansPerPort[p]={t:[],u:[]}; if (v === 1) { thisVlanInStructure.untagged.push(p); + vlansPerPort[p].u.push(vid); } else if (v === 2) { thisVlanInStructure.tagged.push(p); + vlansPerPort[p].t.push(vid); } } if (thisVlanInStructure.tagged.length + thisVlanInStructure.untagged.length == 0) { toast(t("v_nomember"), "err"); return; } - function mySplit(a){ - if(a=="") return []; - return a.split(","); - } for(const vlanRow of $("vtable").tBodies[0].rows){ var rowData = vlanRow.firstChild.firstChild.dataset, rowUntagged = mySplit(rowData.u), @@ -1656,11 +1661,23 @@ function vlanApply() { rowUntagged.splice(rowUntagged.indexOf(""+newVlanPort),1); } } + for(const rut of rowUntagged) { + vlansPerPort[rut].u.push(rowData.id); + } + for(const rt of rowTagged) { + vlansPerPort[rt].t.push(rowData.id) + } top_cmds.push(vlan2str(rowData.id,rowData.name,rowTagged,rowUntagged)); } tail_cmds.push(vlan2str(vid, vname, thisVlanInStructure.tagged, thisVlanInStructure.untagged)); - //TODO: if the PVID is equal to this vid, we need to set it something else (probably 1) - + + //park all ports with only tagged membership + for (p in vlansPerPort){ + if (vlansPerPort[p].u.length == 0 && vlansPerPort[p].t.length > 0) { + tail_cmds.push(pvidcmd(parkingVLAN, p)); + } + } + for(const up of thisVlanInStructure.untagged){ tail_cmds.push(pvidcmd(vid,up)); } @@ -1672,7 +1689,7 @@ function vlanApply() { vlanLoad(vid) }); } - +/* function ingressApply() { var cmd = "ingress", any = false; @@ -1689,6 +1706,7 @@ function ingressApply() { } postCmd(cmd).catch(function() {}); } +*/ $("vapply").addEventListener("click", vlanApply); $("vlan_add").addEventListener("click", function() { var body = h("div"); From c46261e0529de736e0ded96245c477196451b0f0 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 17 Oct 2026 20:09:33 +0100 Subject: [PATCH 04/13] Using PVID=0 for the situation where the port is only in tagged membership. This was tested on HI_K0801WS and it appears to not have any negative side-effects --- cmd_parser.c | 4 ++-- html/app.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd_parser.c b/cmd_parser.c index ab3a5894..c5aa42ad 100644 --- a/cmd_parser.c +++ b/cmd_parser.c @@ -2011,10 +2011,10 @@ void cmd_parser(void) __banked cmd_error("lacp (show | off); a group starts LACP with: lag <1-4> lacp ...\n"); } else if (cmd_compare(0, "pvid")) { if (cmd_words_len == 3 && cmd_parse_port_separator(cmd_words_b[1]) != 0 - && atoi_short(cmd_words_b[2]) && atoi_results_short && atoi_results_short <= 4094) + && atoi_short(cmd_words_b[2]) && atoi_results_short <= 4094) port_pvid_set(atoi_results_u8, atoi_results_short); else { - cmd_error("pvid <1-4094>\n"); + cmd_error("pvid <0-4094>\n"); } } else if (cmd_compare(0, "vlan")) { parse_vlan(); diff --git a/html/app.js b/html/app.js index e636ddbf..f781cfe1 100644 --- a/html/app.js +++ b/html/app.js @@ -1619,7 +1619,6 @@ function vlan2str(i, n, t, u) { function pvidcmd(i, p){ return "pvid " + p + " " + i; } -const parkingVLAN=4094; function mySplit(a){ if(a.trim()=="") return []; return a.trim().split(","); @@ -1674,7 +1673,7 @@ function vlanApply() { //park all ports with only tagged membership for (p in vlansPerPort){ if (vlansPerPort[p].u.length == 0 && vlansPerPort[p].t.length > 0) { - tail_cmds.push(pvidcmd(parkingVLAN, p)); + tail_cmds.push(pvidcmd(0, p)); } } From c3f9acea1092a081f125fb60840148c888874882 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 17 Oct 2026 20:29:44 +0100 Subject: [PATCH 05/13] Tidying up the new VLAN page and adding error messages When adding a new VLAN, we should load it after addition Prevent deleting the mgmt vlan Adding a button to promote a vlan to be the management VLAN We can't create a vlan called d or mgmt as that clashes with the commands to delete and promote a vlan Making the Management VLAN more obvious and adding a "confused" disabled button to where delete and promote are Switching to the i image and making it so you can click the entire row apart from the cell where the buttons are (to prevent accidents) Cleaning up the last bits of ingress filtering I can see --- html/app.js | 177 ++++++++++++++++++------------------------------ html/index.html | 15 +--- 2 files changed, 68 insertions(+), 124 deletions(-) diff --git a/html/app.js b/html/app.js index f781cfe1..29429e54 100644 --- a/html/app.js +++ b/html/app.js @@ -1,7 +1,6 @@ "use strict"; var LANG={ en:{ -ROBTODO:"TODOTODOTODO", nav_dash:"Dashboard",nav_ports:"Ports",nav_stp:"Spanning tree",nav_stats:"Statistics", nav_vlan:"VLANs",nav_l2:"MAC table",nav_mirror:"Mirroring",nav_isolate:"Isolation",nav_lag:"LAG",nav_eee:"EEE", nav_bw:"Bandwidth",nav_system:"System",nav_fw:"Firmware",nav_lldp:"LLDP", @@ -45,20 +44,19 @@ lldp_enabled: "LLDP Enabled",lldp_title:"Service Status",lldp_portname:"Name",ll st_title:"Port statistics",st_h:"totals since boot",st_txg:"TX good",st_txb:"TX bad",st_rxg:"RX good", st_rxb:"RX bad",st_details:"Details",st_counters:"MIB counters",st_nonzero:"non-zero only", st_autoref:"auto-refresh",st_counter:"Counter",st_value:"Value",st_fail:"failed to load counters", -v_title:"VLANs",v_vid:"VID",v_members:"Members",v_tagged:"Tagged",v_untagged:"Untagged",v_pvid_on:"PVID on", +v_title:"VLANs",v_vid:"VLAN ID",v_members:"Members",v_tagged:"Tagged",v_untagged:"Untagged",v_pvid_on:"PVID on", v_empty:"No VLANs configured.",v_mgmt:"Management VLAN",v_mgmt_none:"untagged",v_editor:"VLAN editor", -v_optional:"optional",v_setmgmt:"Set as mgmt VLAN",v_setmgmt_t:"Make this the management VLAN", -v_member:"Member",v_pvid:"PVID",v_add:"Add a new VLAN", +v_optional:"optional",v_setmgmt_q:"Set {n} as the management ID?",v_setmgmt_t:"Make this the management VLAN", +v_member:"Member",v_pvid:"PVID",v_add:"Add a new VLAN", v_mgmt_c:"WARNING: You must be able to access this VLAN to continue to manage the switch.", v_legend:"U = untagged member, T = tagged member, - = not a member. PVID is automatically assigned based on the untagged membership.", -v_ingress:"Ingress filtering",v_ingress_h:"write-only: the state is not readable from the switch", -v_accept:"Accept",v_ing_all:"All",v_ing_apply:"Apply ingress modes",v_del_t:"Delete VLAN", +v_accept:"Accept",v_ing_all:"All",v_del_t:"Delete VLAN", v_del_q:"Delete VLAN {n}?",v_del_d:"Ports keep their PVID until reassigned.", -v_vid_err:"Enter a VLAN ID (1-4094)",v_name_err:"Name must start with a letter (letters, digits, _)", +v_vid_err:"Enter a VLAN ID (1-4094)",v_name_err:"Name must start with a letter (letters, digits, _) and cannot be just 'd' or 'mgmt'", v_loaded:"Loaded VLAN {n}",v_notfound:"VLAN {n} not found (a new one can still be created)", -v_nomember:"Select at least one member port (or delete the VLAN instead)", -v_ing_none:"Pick an ingress mode for at least one port",v_vid_first:"Enter a VLAN ID first", -v_mgmt_q:"Set VLAN {n} as management VLAN?", +v_nomember:"Select at least one member port (or delete the VLAN instead)",v_vid_first:"Enter a VLAN ID first", +v_mgmt_q:"Set VLAN {n} as management VLAN?",v_vid_err:"VLAN ID already exists", v_mgmt_d:"The switch will start tagging its own traffic with that VLAN. If the port you are connected through does not carry it, this page becomes unreachable and the setting can only be undone over the console.", +v_mgmt_tt:"The management VLAN cannot be deleted, you need to assign management to another VLAN first.", l2_title:"MAC address table",l2_filter:"filter...",l2_flush:"Flush learned entries",l2_static:"static", l2_learned:"learned",l2_loading:"loading...",l2_failed:"load failed",l2_entries:"entries", l2_del_t:"Delete entry",l2_flush_q:"Flush all learned MAC entries?", @@ -180,14 +178,12 @@ v_empty:"VLAN は設定されていません。",v_mgmt:"管理 VLAN",v_mgmt_non v_optional:"任意",v_setmgmt:"管理 VLAN に設定",v_setmgmt_t:"この VLAN を管理 VLAN にする", v_member:"メンバー",v_pvid:"PVID", v_legend:"U = タグ無しメンバー、T = タグ付きメンバー、- = 非メンバー。PVID はタグ無し受信トラフィックをこの VLAN に割り当てます。", -v_ingress:"受信フィルタ",v_ingress_h:"書き込み専用: 現在の状態はスイッチから読み取れません", v_accept:"受け入れ",v_ing_all:"すべて",v_ing_apply:"受信モードを適用",v_del_t:"VLAN を削除", v_del_q:"VLAN {n} を削除しますか?",v_del_d:"ポートの PVID は再割り当てされるまで維持されます。", v_vid_err:"VLAN ID (1〜4094) を入力してください",v_name_err:"名前は英字で始めてください (英字、数字、_)", v_loaded:"VLAN {n} を読み込みました",v_notfound:"VLAN {n} は存在しません (新規作成は可能です)", v_nomember:"メンバーポートを 1 つ以上選択してください (または VLAN を削除してください)", -v_ing_none:"1 つ以上のポートの受信モードを選択してください",v_vid_first:"先に VLAN ID を入力してください", -v_mgmt_q:"VLAN {n} を管理 VLAN に設定しますか?", +v_vid_first:"先に VLAN ID を入力してください",v_mgmt_q:"VLAN {n} を管理 VLAN に設定しますか?", v_mgmt_d:"スイッチは自身のトラフィックにこの VLAN のタグを付けるようになります。接続中のポートがこの VLAN を通さない場合、このページにアクセスできなくなり、コンソールからしか元に戻せません。", l2_title:"MAC アドレステーブル",l2_filter:"フィルタ...",l2_flush:"学習エントリを消去",l2_static:"静的", l2_learned:"学習",l2_loading:"読み込み中...",l2_failed:"読み込み失敗",l2_entries:"エントリ", @@ -293,14 +289,12 @@ v_empty:"尚未配置 VLAN。",v_mgmt:"管理 VLAN",v_mgmt_none:"untagged",v_edi v_optional:"可选",v_setmgmt:"设为管理 VLAN",v_setmgmt_t:"将此 VLAN 设为管理 VLAN", v_member:"成员",v_pvid:"PVID", v_legend:"U = untagged 成员,T = tagged 成员,- = 非成员。PVID 将入方向的未标记流量归入此 VLAN。", -v_ingress:"入方向过滤",v_ingress_h:"只写: 当前状态无法从交换机读取", v_accept:"接受",v_ing_all:"全部",v_ing_apply:"应用入方向模式",v_del_t:"删除 VLAN", v_del_q:"删除 VLAN {n}?",v_del_d:"端口的 PVID 在重新分配前保持不变。", v_vid_err:"请输入 VLAN ID (1-4094)",v_name_err:"名称必须以字母开头 (字母、数字、_)", v_loaded:"已读取 VLAN {n}",v_notfound:"VLAN {n} 不存在 (仍可新建)", v_nomember:"请至少选择一个成员端口 (或改为删除该 VLAN)", -v_ing_none:"请至少为一个端口选择入方向模式",v_vid_first:"请先输入 VLAN ID", -v_mgmt_q:"将 VLAN {n} 设为管理 VLAN?", +v_vid_first:"请先输入 VLAN ID",v_mgmt_q:"将 VLAN {n} 设为管理 VLAN?", v_mgmt_d:"交换机将开始为自身流量打上该 VLAN 标签。如果您所连接的端口不属于该 VLAN,本页面将无法访问,且只能通过控制台撤销此设置。", l2_title:"MAC 地址表",l2_filter:"筛选...",l2_flush:"清除学习条目",l2_static:"静态", l2_learned:"动态学习",l2_loading:"加载中...",l2_failed:"加载失败",l2_entries:"条", @@ -406,14 +400,12 @@ v_empty:"No hay VLANs configuradas.",v_mgmt:"VLAN de gestión",v_mgmt_none:"sin v_optional:"opcional",v_setmgmt:"Establecer como VLAN de gestión",v_setmgmt_t:"Hacer esta la VLAN de gestión", v_member:"Miembro",v_pvid:"PVID", v_legend:"U = miembro sin etiquetar, T = miembro etiquetado, - = no miembro. PVID asigna esta VLAN al tráfico entrante sin etiquetar.", -v_ingress:"Filtrado de entrada",v_ingress_h:"solo escritura: el estado no se puede leer desde el switch", v_accept:"Aceptar",v_ing_all:"Todos",v_ing_apply:"Aplicar modos de entrada",v_del_t:"Eliminar VLAN", v_del_q:"¿Eliminar VLAN {n}?",v_del_d:"Los puertos conservan su PVID hasta que se reasignen.", v_vid_err:"Introduce un ID de VLAN (1-4094)",v_name_err:"El nombre debe empezar por una letra (letras, dígitos, _)", v_loaded:"VLAN {n} cargada",v_notfound:"VLAN {n} no encontrada (aún se puede crear una nueva)", v_nomember:"Selecciona al menos un puerto miembro (o elimina la VLAN)", -v_ing_none:"Elige un modo de entrada para al menos un puerto",v_vid_first:"Introduce primero un ID de VLAN", -v_mgmt_q:"¿Establecer VLAN {n} como VLAN de gestión?", +v_vid_first:"Introduce primero un ID de VLAN",v_mgmt_q:"¿Establecer VLAN {n} como VLAN de gestión?", v_mgmt_d:"El switch empezará a etiquetar su propio tráfico con esa VLAN. Si el puerto por el que te conectas no la transporta, esta página dejará de ser accesible y el ajuste solo podrá deshacerse por la consola.", l2_title:"Tabla de direcciones MAC",l2_filter:"filtrar...",l2_flush:"Vaciar entradas aprendidas",l2_static:"estática", l2_learned:"aprendida",l2_loading:"cargando...",l2_failed:"error de carga",l2_entries:"entradas", @@ -518,14 +510,12 @@ v_empty:"Aucun VLANs configuré.",v_mgmt:"VLAN de management",v_mgmt_none:"déta v_optional:"optionel",v_setmgmt:"Mette en VLAN de management",v_setmgmt_t:"Mettre celui-ci en VLAN de management", v_member:"Membre",v_pvid:"PVID", v_legend:"U = Membre détagué (UnTagged), T = membre tagué (Tagged), - = non membre. PVID (Port VLan ID) assigne ce VLAN au trafic entrant non tagué.", -v_ingress:"Filtrage entrant",v_ingress_h:"en ériture seule: cet état n'est pas lisible depuis le communtateur réseau", v_accept:"Accepté",v_ing_all:"Tous",v_ing_apply:"Appliquer les filtes",v_del_t:"Supprimer le VLAN", v_del_q:"Supprimer le VLAN {n}?",v_del_d:"Les ports conservent leur PVID tant qu'ils ne sont pas réassignés.", v_vid_err:"Entrez un identifiant VLAN (1-4094)",v_name_err:"Le nom doit commencer par une lettre (lettre, chiffres, _)", v_loaded:"VLAN chargés {n}",v_notfound:"VLAN {n} non trouvé (un nouveau peut toutefois être créé)", v_nomember:"Selectionnez au moins un port membre (ou supprimez le VLAN)", -v_ing_none:"Sélectionnez le mode entrant pour au moins un port",v_vid_first:"Entrez un identifiant VLAN d'abord", -v_mgmt_q:"Mettre ce VLAN {n} en tant que VLAN de management?", +v_vid_first:"Entrez un identifiant VLAN d'abord",v_mgmt_q:"Mettre ce VLAN {n} en tant que VLAN de management?", v_mgmt_d:"Ce commutateur commencera par taguer son propre trafic avec ce VLAN. Si le port avec lequel vous êtes connecté ne le permet pas, cette page deviendra inaccessible et les paramètres ne pourront plus être changés que via la console.", l2_title:"Table des adresses MAC",l2_filter:"filtre...",l2_flush:"Effacer les entrées apprises",l2_static:"static", l2_learned:"appris",l2_loading:"chargement...",l2_failed:"chargement non réussi",l2_entries:"entrées", @@ -636,14 +626,12 @@ v_empty:"Keine VLANs konfiguriert.",v_mgmt:"Management-VLAN",v_mgmt_none:"untagg v_optional:"optional",v_setmgmt:"Als Management-VLAN",v_setmgmt_t:"Dieses VLAN zum Management-VLAN machen", v_member:"Mitglied",v_pvid:"PVID", v_legend:"U = Mitglied untagged, T = Mitglied tagged, - = kein Mitglied. PVID ordnet eingehenden Verkehr ohne Tag diesem VLAN zu.", -v_ingress:"Ingress-Filter",v_ingress_h:"nur schreibbar: der Zustand lässt sich vom Switch nicht auslesen", v_accept:"Annehmen",v_ing_all:"Alle",v_ing_apply:"Ingress-Modi übernehmen",v_del_t:"VLAN löschen", v_del_q:"VLAN {n} löschen?",v_del_d:"Die Ports behalten ihre PVID, bis sie neu zugewiesen wird.", v_vid_err:"VLAN-ID eingeben (1-4094)",v_name_err:"Der Name muss mit einem Buchstaben beginnen (Buchstaben, Ziffern, _)", v_loaded:"VLAN {n} geladen",v_notfound:"VLAN {n} nicht gefunden (es kann neu angelegt werden)", v_nomember:"Mindestens einen Mitglieds-Port wählen (oder das VLAN stattdessen löschen)", -v_ing_none:"Für mindestens einen Port einen Ingress-Modus wählen",v_vid_first:"Zuerst eine VLAN-ID eingeben", -v_mgmt_q:"VLAN {n} als Management-VLAN setzen?", +v_vid_first:"Zuerst eine VLAN-ID eingeben",v_mgmt_q:"VLAN {n} als Management-VLAN setzen?", v_mgmt_d:"Der Switch versieht dann seinen eigenen Verkehr mit diesem VLAN-Tag. Führt der Port, über den du verbunden bist, dieses VLAN nicht, ist diese Seite nicht mehr erreichbar und die Einstellung lässt sich nur noch über die serielle Konsole zurücknehmen.", l2_title:"MAC-Adresstabelle",l2_filter:"filtern...",l2_flush:"Gelernte Einträge löschen",l2_static:"statisch", l2_learned:"gelernt",l2_loading:"lädt...",l2_failed:"Laden fehlgeschlagen",l2_entries:"Einträge", @@ -1465,12 +1453,13 @@ function rangeStr(list){ } return parts.join(", "); } -//ROBSTART + function vlanRefresh() { var tb = $("vtable").tBodies[0]; + tb.style="cursor:pointer;" return getJSON("/vlanlist").then(function(d) { var vl = d.vlan || []; - $("vmgmtcur").textContent = d.mgmt ? String(d.mgmt) : t("v_mgmt_none"); + $("vmgmtcur").dataset.id = d.mgmt; $("vempty").style.display = vl.length ? "none" : ""; tb.innerHTML = ""; var p = Promise.resolve(); @@ -1481,9 +1470,13 @@ function vlanRefresh() { }) }).then(function(d) { var tr = tb.insertRow(); + tr.onclick = function(e) { + e.preventDefault(); + vlanLoad(v.id); + }; var l = h("a", { href: "#vlan", - text: String(v.id), + text: String(v.id) + ($("vmgmtcur").dataset.id == v.id ? " ("+t("v_mgmt")+")":""), "data-id": v.id, "data-name": v.name, onclick: function(e) { @@ -1508,19 +1501,38 @@ function vlanRefresh() { l.dataset.t = maskToPorts(tag); l.dataset.u = maskToPorts(unt); } - var del = tr.insertCell(); - if (v.id !== 1) del.appendChild(h("button", { - class: "ctl", - text: "\u2715", - title: t("v_del_t"), - onclick: function() { - confirmModal(t("v_del_q", { - n: v.id - }), t("v_del_d"), function() { - postCmd("vlan " + v.id + " d").then(vlanRefresh).catch(function() {}); - }); - } - })); + var act = tr.insertCell(); + act.onclick=function(e){e.stopPropagation()}; + if (v.id == $("vmgmtcur").dataset.id) { + //act.appendChild(h("button",{class:"ctl", title:t("v_mgmt_tt"), disabled:"disabled", text:"\u{1F6C8}"})); + act.appendChild(h("span",{class:"ctl", title:t("v_mgmt_tt"), class:"hint", style:"color: var(--fg2); font-size: 150%; padding-left: 4px;"},[document.createTextNode("\u{1F6C8}")])); + } else { + act.appendChild(h("button", { + class: "ctl", + text: "\u2715", + title: t("v_del_t"), + onclick: function() { + confirmModal(t("v_del_q", { + n: v.id + }), t("v_del_d"), function() { + postCmd("vlan " + v.id + " d").then(vlanRefresh).catch(function() {}); + }); + }, + })); + act.appendChild(h("button", { + class: "ctl", + text: "\u2605", + style: "margin-left: 1em;", + title: t("v_setmgmt_t"), + onclick: function() { + confirmModal(t("v_setmgmt_q", { + n: v.id + }), t("v_mgmt_c"), function() { + postCmd("vlan " + v.id + " mgmt").then(vlanRefresh); + }); + } + })); + } }).catch(function() {}); }); return p; @@ -1560,19 +1572,6 @@ function buildVlanEdit() { id: "vp" + p })); })(p); - //ROBTEST TODO - /* - var it=$("ingress").tBodies[0]; - var ih=it.insertRow();ih.insertCell().className="mut"; - var ir=it.insertRow();ir.insertCell().textContent=t("v_accept"); - for(var q=1;q<=S.n;q++)(function(q){ - ih.insertCell().innerHTML=''+q+''; - var sel=h("select",{class:"in",id:"ing"+q}); - [["","-"],["a",t("v_ing_all")],["u",t("v_untagged")],["t",t("v_tagged")]].forEach(function(o){ - sel.appendChild(h("option",{value:o[0],text:o[1]})); - }); - ir.insertCell().appendChild(sel); - })(q);*/ } function segVal(p) { @@ -1601,7 +1600,7 @@ function vlanLoad(vid) { for (var p = 1; p <= S.n; p++) { var bit = S.physToLog[p - 1]; segSet(p, (mem >> bit) & 1 ? (((unt >> bit) & 1) ? 1 : 2) : 0); - $("vp" + p).innerText = !!((pv >> bit) & 1); + $("vp" + p).innerText = (!!((pv >> bit) & 1))?"Y":"N"; } toast(t("v_loaded", { n: vid @@ -1688,54 +1687,24 @@ function vlanApply() { vlanLoad(vid) }); } -/* -function ingressApply() { - var cmd = "ingress", - any = false; - for (var p = 1; p <= S.n; p++) { - var v = $("ing" + p).value; - if (v) { - cmd += " " + p + v; - any = true; - } - } - if (!any) { - toast(t("v_ing_none"), "err"); - return; - } - postCmd(cmd).catch(function() {}); -} -*/ $("vapply").addEventListener("click", vlanApply); $("vlan_add").addEventListener("click", function() { var body = h("div"); - body.appendChild(document.createTextNode("ROBTODO: styling,layout etc")); body.appendChild(h("p", null, [ - h("label", null, [document.createTextNode(t("v_vid"))]), - h("input", { - id: "nvlid", - class: "in sm", - type: "number", - min: "1", - max: "4094" - }) + h("label", {class: "row"}, [ + h("span",{class:"lb", "data-i18n":"v_vid"},[document.createTextNode(t("v_vid"))]), + h("input", {id: "nvlid", class: "in sm", type: "number", min: "1", max: "4094"}) + ]) ])); body.appendChild(h("p", null, [ - h("label", null, [document.createTextNode(t("c_name"))]), - h("input", { - id: "nvlname", - class: "in", - size: "12", - maxlength: "15", - "data-i18n-p": "v_opt ional" - }) + h("label", {class: "row"}, [ + h("span",{class:"lb", "data-i18n":"c_name"},[document.createTextNode(t("c_name"))]), + h("input", {id: "nvlname", class: "in", size: "12", maxlength: "15", "data-i18n-p": "v_optional"}) + ]) ])); modal("Add VLAN", body, [h("button", { - class: "ctl pri", - text: t("ROBTODO"), - onclick: function() { - addVlan() - } + class: "ctl pri", text: t("v_add"), + onclick: addVlan })]); }); @@ -1743,31 +1712,21 @@ function addVlan() { var vid = parseInt($("nvlid").value, 10); for (const r of $("vtable").tBodies[0].rows) { if (r.firstChild.firstChild.dataset.id == vid) { - toast(t("ROBTODO"), "err"); + toast(t("v_vid_err"), "err"); return; } } var name = $("nvlname").value; - if (name && !/^[a-zA-Z]\w*$/.test(name)) { + if (name && (!/^[a-zA-Z]\w*$/.test(name) || name == "d" || name == "mgmt")) { toast(t("v_name_err"), "err"); return; } var cmd = "vlan " + vid + (name ? " " + name : ""); postCmd(cmd).then(function() { closeModal(); - vlanRefresh() + vlanRefresh().then(function(){vlanLoad(vid)}); }).catch(function() {}); } -//ROBTODO -/* -//$("ingapply").addEventListener("click",ingressApply); -$("vmgmt").addEventListener("click",function(){ - var vid=parseInt($("vvid").value,10); - if(!vid){toast(t("v_vid_first"),"err");return;} - confirmModal(t("v_mgmt_q",{n:vid}),t("v_mgmt_d"), - function(){postCmd("vlan "+vid+" mgmt").then(vlanRefresh).catch(function(){})}); -}); -*/ tabHooks.vlan = { enter: function() { needPorts(function() { @@ -1778,11 +1737,9 @@ tabHooks.vlan = { }) }, }; - function VlanInit() { $("vtable").tBodies[0].firstChild.firstChild.firstChild.click(); }; -//ROBSTOP var l2Rows=[],l2SortCol="pport",l2SortDir=1,l2Gen=0; function l2Load(){ var seen={},all=[],idx=0,guard=0,gen=++l2Gen; diff --git a/html/index.html b/html/index.html index 74a2426e..010ffd95 100644 --- a/html/index.html +++ b/html/index.html @@ -286,7 +286,7 @@ button.danger{background:var(--bad);color:#fff;border-color:transparent}
-

VLANs Management VLAN: -

+

VLANs

VIDNameMembersTaggedUntaggedPVID on
@@ -294,23 +294,10 @@ button.danger{background:var(--bad);color:#fff;border-color:transparent}

VLAN editor

-

U = untagged member, T = tagged member, - = not a member. PVID assigns this VLAN to untagged ingress traffic.

-
From 8d6adedb5625041bba8b329d3b000f90c4ab7f29 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 19 Oct 2026 08:38:44 +0100 Subject: [PATCH 06/13] Removing my changes to the sim, in favour of #629 --- tools/httpd_sim.c | 130 ++-------------------------------------------- 1 file changed, 5 insertions(+), 125 deletions(-) diff --git a/tools/httpd_sim.c b/tools/httpd_sim.c index 64654aac..68f87232 100644 --- a/tools/httpd_sim.c +++ b/tools/httpd_sim.c @@ -19,9 +19,6 @@ #define NSFP 1 #define N_COUNTERS 52 -#define VLAN_MAX_ID 4094 - -int currentVLANCount = 1; #if PORTS == 9 const uint8_t physToLogPort[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8}; @@ -29,14 +26,6 @@ const uint8_t physToLogPort[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8}; const uint8_t physToLogPort[] = { 4, 5, 6, 7, 3, 8}; #endif -struct mem_vlan { - bool active; - char name[20]; - bool untagged[PORTS]; - bool tagged[PORTS]; -}; -struct mem_vlan mem_vlans[VLAN_MAX_ID+1]; -//mem_vlans[1] = {.active=true, .name="", .untagged={}, .tagged={}}; struct l2_entry { char mac[18]; char vlan[4]; @@ -145,58 +134,17 @@ void send_vlan(int s, int vlan) "Content-Type: application/json; charset=UTF-8\r\n\r\n"; v = json_object_new_object(); - if(vlan == 1) { - sprintf(num_buff, "0x%08x", 0x207ffff); - json_object_object_add(v, "members", json_object_new_string(num_buff)); - - json_object_object_add(v, "name", json_object_new_string("native")); - json_object_object_add(v, "pvid", json_object_new_string("0x01ff")); - } else { - if(mem_vlans[vlan].active) { - json_object_object_add(v, "name", json_object_new_string(mem_vlans[vlan].name)); - json_object_object_add(v, "members", json_object_new_string("0x0000")); - json_object_object_add(v, "pvid", json_object_new_string("0x0000")); - } - } - write(s, header, strlen(header)); + sprintf(num_buff, "0x%08x", 0x00060011); + json_object_object_add(v, "members", json_object_new_string(num_buff)); + + write(s, header, strlen(header)); jstring = json_object_to_json_string_ext(v, JSON_C_TO_STRING_PLAIN); - write(s, jstring, strlen(jstring)); + write(s, jstring, strlen(jstring)); json_object_put(v); } -void send_vlanlist(int s) -{ - struct json_object *v, *vlans, *vlan; - const char *jstring; - char *header = "HTTP/1.1 200 OK\r\n" - "Content-Type: application/json; charset=UTF-8\r\n\r\n"; - - v = json_object_new_object(); - - json_object_object_add(v, "mgmt", json_object_new_int(1)); - - vlans = json_object_new_array_ext(currentVLANCount); - - //foreach VLAN - for (int x=1;x<=VLAN_MAX_ID;++x) { - vlan = json_object_new_object(); - if(x ==1 || mem_vlans[x].active) { - json_object_object_add(vlan, "id", json_object_new_int(x)); - json_object_object_add(vlan, "name", json_object_new_string(mem_vlans[x].name)); - - json_object_array_add(vlans, vlan); - } - } - - json_object_object_add(v, "vlan", vlans); - - write(s, header, strlen(header)); - jstring = json_object_to_json_string_ext(v, JSON_C_TO_STRING_PLAIN); - write(s, jstring, strlen(jstring)); - json_object_put(v); -} void send_status(int s) { @@ -698,65 +646,6 @@ void print_cmd_history(void) printf("%d: %s\n", i, cmd_history[i]); } -void delete_vlan(int vid) { - mem_vlans[vid] = (struct mem_vlan){}; - currentVLANCount--; -} - -void parse_cmd(char* cmd) -{ - char *token = strtok(cmd, " "); - char *name; - printf("\t OK FIRST TOKEN: '%s'\n",token); - if (!strncmp(token, "vlan", 4)) { - printf("\t OK IT IS VLAN\n"); - token = strtok(NULL, " "); - int vid = atoi(token); - if (vid<1 || vid > VLAN_MAX_ID) { - printf("\t REJECT BECAUSE OF VID:%d\n",vid); - return; - } - - token = strtok(NULL, " "); - if(!strncmp(token,"mgmt",4)) { - printf("\t MGMT LEAVE:\n"); - return; //TODO - } else if (!strncmp(token,"d",1)) { - printf("\t DELETE VID %d:\n",vid); - delete_vlan(vid); - } else { - if (token[0] >= 'A' && token[0] <= 'z' ){ - name = token; - token = strtok(NULL, " "); - - printf("\tIS NAME %s:\n", name); - } - - struct mem_vlan newvlan = {.active=true}; - strcpy(newvlan.name, name); - - // a string of [0-9][t]? - printf("\t TOKEN: %s:\n", token); - while(token != NULL) { - int portNum = atoi(token); - bool isTagged = token[strlen(token)] == 't'; - printf("\tVLAN: %d Name: %s Port: %d tagged:%d\n", vid, name, portNum, isTagged); - token = strtok(NULL, " "); - if(isTagged) { - newvlan.tagged[portNum] = true; - } else { - newvlan.untagged[portNum] = true; - } - } - - mem_vlans[vid] = newvlan; - currentVLANCount++; - } - } else { - - } -} - void launch(struct Server *server) { char buffer[BUFFER_SIZE]; @@ -846,13 +735,6 @@ void launch(struct Server *server) else send_vlan(new_socket, vlan); goto done; - } else if (!strncmp(&buffer[4], "/vlanlist", 9)) { - printf("request for vlan list"); - if (!authenticated) - send_unauthorized(new_socket); - else - send_vlanlist(new_socket); - goto done; } else if (!strncmp(&buffer[4], "/lldp.json", 10)) { printf("LLDP request"); if (!authenticated) @@ -949,8 +831,6 @@ void launch(struct Server *server) printf("POST cmd\n"); printf("CMD: %s\n", p + 4); strcpy(cmd_history[cmd_ptr], p + 4); - printf("\t GOING TO PARSE THIS: %s\n", p+4); - parse_cmd(p+4); cmd_ptr++; print_cmd_history(); char *response = "HTTP/1.1 200 OK\r\n" From f94204e56fb69a1774c2584fca86830a228a0464 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Mon, 19 Oct 2026 18:22:27 +0100 Subject: [PATCH 07/13] Adding rename support for vlan --- html/app.js | 50 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/html/app.js b/html/app.js index 29429e54..52d1ddfa 100644 --- a/html/app.js +++ b/html/app.js @@ -49,7 +49,7 @@ v_empty:"No VLANs configured.",v_mgmt:"Management VLAN",v_mgmt_none:"untagged",v v_optional:"optional",v_setmgmt_q:"Set {n} as the management ID?",v_setmgmt_t:"Make this the management VLAN", v_member:"Member",v_pvid:"PVID",v_add:"Add a new VLAN", v_mgmt_c:"WARNING: You must be able to access this VLAN to continue to manage the switch.", v_legend:"U = untagged member, T = tagged member, - = not a member. PVID is automatically assigned based on the untagged membership.", -v_accept:"Accept",v_ing_all:"All",v_del_t:"Delete VLAN", +v_accept:"Accept",v_ing_all:"All",v_del_t:"Delete VLAN",v_rename:"Rename",v_rename_t:"Rename VLAN", v_del_q:"Delete VLAN {n}?",v_del_d:"Ports keep their PVID until reassigned.", v_vid_err:"Enter a VLAN ID (1-4094)",v_name_err:"Name must start with a letter (letters, digits, _) and cannot be just 'd' or 'mgmt'", v_loaded:"Loaded VLAN {n}",v_notfound:"VLAN {n} not found (a new one can still be created)", @@ -1503,14 +1503,35 @@ function vlanRefresh() { } var act = tr.insertCell(); act.onclick=function(e){e.stopPropagation()}; + act.appendChild(h("button", { + class: "ctl", + text: t("v_rename"), + title: t("v_rename_t"), + onclick: function() { + var body = h("div"); + body.appendChild(h("p", null, [ + h("label", {class: "row"}, [ + h("span",{class:"lb", "data-i18n":"c_name"},[document.createTextNode(t("c_name"))]), + h("input", {id: "nvlrename", class: "in", size: "12", maxlength: "15", "data-i18n-p": "v_optional", value:(v.name||"")}) + ]) + ])); + modal("Rename VLAN "+v.id, body, [h("button", { + class: "ctl pri", text: t("v_rename_t"), + onclick: function(){ + if(!validVlanName($("nvlrename").value)) return; + postCmd(vlan2str(v.id, $("nvlrename").value,mySplit(l.dataset.t),mySplit(l.dataset.u))).then(function(){closeModal();vlanRefresh()}); + } + })]); + }, + })); if (v.id == $("vmgmtcur").dataset.id) { - //act.appendChild(h("button",{class:"ctl", title:t("v_mgmt_tt"), disabled:"disabled", text:"\u{1F6C8}"})); - act.appendChild(h("span",{class:"ctl", title:t("v_mgmt_tt"), class:"hint", style:"color: var(--fg2); font-size: 150%; padding-left: 4px;"},[document.createTextNode("\u{1F6C8}")])); + act.appendChild(h("span",{class:"ctl", title:t("v_mgmt_tt"), class:"hint", style:"color: var(--fg2); font-size: 150%; padding-left: 10px;"},[document.createTextNode("\u{1F6C8}")])); } else { act.appendChild(h("button", { class: "ctl", text: "\u2715", title: t("v_del_t"), + style: "margin-left: 5px;", onclick: function() { confirmModal(t("v_del_q", { n: v.id @@ -1522,7 +1543,7 @@ function vlanRefresh() { act.appendChild(h("button", { class: "ctl", text: "\u2605", - style: "margin-left: 1em;", + style: "margin-left: 5px;", title: t("v_setmgmt_t"), onclick: function() { confirmModal(t("v_setmgmt_q", { @@ -1618,10 +1639,10 @@ function vlan2str(i, n, t, u) { function pvidcmd(i, p){ return "pvid " + p + " " + i; } - function mySplit(a){ - if(a.trim()=="") return []; - return a.trim().split(","); - } +function mySplit(a){ + if(a.trim()=="") return []; + return a.trim().split(","); +} function vlanApply() { var vid = $("vtitle").dataset.id; var vname = $("vtitle").dataset.name; @@ -1707,7 +1728,13 @@ $("vlan_add").addEventListener("click", function() { onclick: addVlan })]); }); - +function validVlanName(n){ + if (n && (!/^[a-zA-Z]\w*$/.test(n) || n == "d" || n == "mgmt")) { + toast(t("v_name_err"), "err"); + return false; + } + return true; +} function addVlan() { var vid = parseInt($("nvlid").value, 10); for (const r of $("vtable").tBodies[0].rows) { @@ -1717,10 +1744,7 @@ function addVlan() { } } var name = $("nvlname").value; - if (name && (!/^[a-zA-Z]\w*$/.test(name) || name == "d" || name == "mgmt")) { - toast(t("v_name_err"), "err"); - return; - } + if(!validVlanName(name)) return false; var cmd = "vlan " + vid + (name ? " " + name : ""); postCmd(cmd).then(function() { closeModal(); From a77d219baf49ee49bf9a9e26a1776631b47c0325 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Tue, 20 Oct 2026 12:09:57 +0100 Subject: [PATCH 08/13] Moving the mgmt switch into the bottom section so it applies when the user clicks apply on the specific vlan. In order for this to work when the user is simultaneously changing the vlan that their machine is connected through and the mgmt vlan id, we need to be able to send a batch of commands through to the switch atomically so the switch can process all of them and the momentary disconnection of the machine doesn't matter --- html/app.js | 78 +++++++++++++------------------------------------ html/index.html | 3 ++ 2 files changed, 23 insertions(+), 58 deletions(-) diff --git a/html/app.js b/html/app.js index 52d1ddfa..c40e2d09 100644 --- a/html/app.js +++ b/html/app.js @@ -46,7 +46,7 @@ st_rxb:"RX bad",st_details:"Details",st_counters:"MIB counters",st_nonzero:"non- st_autoref:"auto-refresh",st_counter:"Counter",st_value:"Value",st_fail:"failed to load counters", v_title:"VLANs",v_vid:"VLAN ID",v_members:"Members",v_tagged:"Tagged",v_untagged:"Untagged",v_pvid_on:"PVID on", v_empty:"No VLANs configured.",v_mgmt:"Management VLAN",v_mgmt_none:"untagged",v_editor:"VLAN editor", -v_optional:"optional",v_setmgmt_q:"Set {n} as the management ID?",v_setmgmt_t:"Make this the management VLAN", +v_optional:"optional",v_setmgmt_q:"Set {n} as the management ID?",v_setmgmt_t:"Management VLAN", v_member:"Member",v_pvid:"PVID",v_add:"Add a new VLAN", v_mgmt_c:"WARNING: You must be able to access this VLAN to continue to manage the switch.", v_legend:"U = untagged member, T = tagged member, - = not a member. PVID is automatically assigned based on the untagged membership.", v_accept:"Accept",v_ing_all:"All",v_del_t:"Delete VLAN",v_rename:"Rename",v_rename_t:"Rename VLAN", @@ -840,7 +840,6 @@ function postCmds(list){ list.forEach(function(c){p=p.then(function(){return postCmd(c,true)})}); return p.then(function(){toast(t("t_cmds",{n:list.length}),"ok")}); } - function toast(msg,cls){ var el=h("div",{class:"toast "+(cls||""),text:msg}); $("toasts").appendChild(el); @@ -1519,7 +1518,7 @@ function vlanRefresh() { class: "ctl pri", text: t("v_rename_t"), onclick: function(){ if(!validVlanName($("nvlrename").value)) return; - postCmd(vlan2str(v.id, $("nvlrename").value,mySplit(l.dataset.t),mySplit(l.dataset.u))).then(function(){closeModal();vlanRefresh()}); + postCmd(vlan2str(v.id,$("nvlrename").value,mySplit(l.dataset.t),mySplit(l.dataset.u))).then(function(){closeModal();vlanRefresh()}); } })]); }, @@ -1533,24 +1532,9 @@ function vlanRefresh() { title: t("v_del_t"), style: "margin-left: 5px;", onclick: function() { - confirmModal(t("v_del_q", { - n: v.id - }), t("v_del_d"), function() { + confirmModal(t("v_del_q", { n: v.id }), t("v_del_d"), function() { postCmd("vlan " + v.id + " d").then(vlanRefresh).catch(function() {}); }); - }, - })); - act.appendChild(h("button", { - class: "ctl", - text: "\u2605", - style: "margin-left: 5px;", - title: t("v_setmgmt_t"), - onclick: function() { - confirmModal(t("v_setmgmt_q", { - n: v.id - }), t("v_mgmt_c"), function() { - postCmd("vlan " + v.id + " mgmt").then(vlanRefresh); - }); } })); } @@ -1571,27 +1555,20 @@ function buildVlanEdit() { rP.insertCell().textContent = t("v_pvid"); for (var p = 1; p <= S.n; p++)(function(p) { hd.insertCell().innerHTML = '' + p + ''; - var seg = h("span", { - class: "seg", - id: "vm" + p - }); + var seg = h("span", { class: "seg", id: "vm" + p }); ["-", "U", "T"].forEach(function(s, ix) { seg.appendChild(h("button", { text: s, "data-v": ix, onclick: function() { - seg.querySelectorAll("button").forEach(function(b) { - b.classList.remove("on") - }); + seg.querySelectorAll("button").forEach(function(b) { b.classList.remove("on") }); this.classList.add("on"); } })); }); seg.children[0].classList.add("on"); rM.insertCell().appendChild(seg); - rP.insertCell().appendChild(h("span", { - id: "vp" + p - })); + rP.insertCell().appendChild(h("span", { id: "vp" + p })); })(p); } @@ -1600,9 +1577,7 @@ function segVal(p) { } function segSet(p, v) { - $("vm" + p).querySelectorAll("button").forEach(function(b, i) { - b.classList.toggle("on", i === v) - }); + $("vm" + p).querySelectorAll("button").forEach(function(b, i) { b.classList.toggle("on", i === v) }); } function vlanLoad(vid) { @@ -1623,17 +1598,12 @@ function vlanLoad(vid) { segSet(p, (mem >> bit) & 1 ? (((unt >> bit) & 1) ? 1 : 2) : 0); $("vp" + p).innerText = (!!((pv >> bit) & 1))?"Y":"N"; } - toast(t("v_loaded", { - n: vid - }), "ok"); - }).catch(function() { - toast(t("v_notfound", { - n: vid - }), "err") - }); + $("vmgmtpromote").checked = ($("vmgmtcur").dataset.id == vid); + toast(t("v_loaded", {n: vid}), "ok"); + }).catch(function() {toast(t("v_notfound", {n: vid}), "err")}); } -function vlan2str(i, n, t, u) { +function vlan2str(i, n, t, u){ return ("vlan " + i + (n ? " " + n : "") + " " + t.map((x)=>x+"t").concat(u).join(" ")).trim(); } function pvidcmd(i, p){ @@ -1650,11 +1620,7 @@ function vlanApply() { toast(t("v_vid_err"), "err"); return; } - var top_cmds = [], - tail_cmds = [], - thisVlanInStructure={id:vid, tagged:[], untagged:[]}, - vlansPerPort=[]; - + var top_cmds = [], tail_cmds = [], vlansPerPort=[], thisVlanInStructure={id:vid, tagged:[], untagged:[]}; for (var p = 1; p <= S.n; p++) { var v = segVal(p); vlansPerPort[p]={t:[],u:[]}; @@ -1674,11 +1640,9 @@ function vlanApply() { var rowData = vlanRow.firstChild.firstChild.dataset, rowUntagged = mySplit(rowData.u), rowTagged = mySplit(rowData.t); - if(rowData.id == vid) continue; + if(rowData.id == vid) continue; for(const newVlanPort of thisVlanInStructure.untagged) { - if(rowUntagged.includes(""+newVlanPort)){ - rowUntagged.splice(rowUntagged.indexOf(""+newVlanPort),1); - } + if(rowUntagged.includes(""+newVlanPort)) rowUntagged.splice(rowUntagged.indexOf(""+newVlanPort),1); } for(const rut of rowUntagged) { vlansPerPort[rut].u.push(rowData.id); @@ -1690,21 +1654,19 @@ function vlanApply() { } tail_cmds.push(vlan2str(vid, vname, thisVlanInStructure.tagged, thisVlanInStructure.untagged)); - //park all ports with only tagged membership - for (p in vlansPerPort){ + for (p in vlansPerPort){/*park all ports with only tagged membership*/ if (vlansPerPort[p].u.length == 0 && vlansPerPort[p].t.length > 0) { tail_cmds.push(pvidcmd(0, p)); } } - for(const up of thisVlanInStructure.untagged){ tail_cmds.push(pvidcmd(vid,up)); } - - var cmds = top_cmds.concat(tail_cmds); - console.log(cmds); - - postCmds(cmds).then(vlanRefresh).catch(function() {}).then(function() { + if($("vmgmtpromote").checked){/*must be the last command*/ + tail_cmds.push("vlan " + vid + " mgmt"); + } + var cmd = top_cmds.concat(tail_cmds); + postCmd(cmd.join("\n"),true).then(function(){toast(t("t_cmds",{n:cmd.length}),"ok")}).then(vlanRefresh).catch(function() {}).then(function() { vlanLoad(vid) }); } diff --git a/html/index.html b/html/index.html index 010ffd95..a64a0b0e 100644 --- a/html/index.html +++ b/html/index.html @@ -294,6 +294,9 @@ button.danger{background:var(--bad);color:#fff;border-color:transparent}

VLAN editor

+

U = untagged member, T = tagged member, - = not a member. PVID assigns this VLAN to untagged ingress traffic.

From 742aef546100fb47c8d4916a722e3faf58937377 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Fri, 23 Oct 2026 17:13:17 +0100 Subject: [PATCH 09/13] Adding highlighting to the currently selected VLAN in the top table so that it is more clear it is selected --- html/app.js | 3 +++ html/index.html | 1 + 2 files changed, 4 insertions(+) diff --git a/html/app.js b/html/app.js index c40e2d09..a7e98f45 100644 --- a/html/app.js +++ b/html/app.js @@ -1600,6 +1600,9 @@ function vlanLoad(vid) { } $("vmgmtpromote").checked = ($("vmgmtcur").dataset.id == vid); toast(t("v_loaded", {n: vid}), "ok"); + for(const vlanRow of $("vtable").tBodies[0].rows) { + vlanRow.className=vlanRow.firstChild.firstChild.dataset.id == vid ? "sel":""; + } }).catch(function() {toast(t("v_notfound", {n: vid}), "err")}); } diff --git a/html/index.html b/html/index.html index a64a0b0e..2660cfbc 100644 --- a/html/index.html +++ b/html/index.html @@ -93,6 +93,7 @@ table.t th.num{text-align:right} table.t td{padding:6px 8px;border-bottom:1px solid var(--bd);vertical-align:middle} table.t tr:last-child td{border-bottom:0} table.t tr:hover td{background:var(--sel)} +table.t tr.sel{background:var(--panel2); font-weight: bold;} .num{font-variant-numeric:tabular-nums;text-align:right} .scrollx{overflow-x:auto} input.in,select.in,textarea.in{ From c9f6362da59983911a1af7ad91419c812231110c Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 24 Oct 2026 09:46:22 +0100 Subject: [PATCH 10/13] Previously the `vlan 2 ` regex was assuming there were ports, now it'll accept just the definition. Also throughout the regex we have over-specified the number of digits sometimes ports are 4 digits long, sometimes they are 2. This seems unnecessary, 1 or more is sufficient --- html/app.js | 63 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/html/app.js b/html/app.js index a7e98f45..b47aca4f 100644 --- a/html/app.js +++ b/html/app.js @@ -789,34 +789,49 @@ function getJSON(p){return api(p).then(function(r){if(!r.ok)throw new Error(p+" function getText(p){return api(p).then(function(r){if(!r.ok)throw new Error(p+" "+r.status);return r.body})} var CONF_CMDS=[ - /^ip\s+(\d{1,3}\.){3}\d{1,3}$/,/^ip\s+dhcp$/, - /^gw\s+(\d{1,3}\.){3}\d{1,3}$/,/^netmask\s+(\d{1,3}\.){3}\d{1,3}$/, - /^syslog\s+(on|off)$/,/^syslog\s+ip\s+(\d{1,3}\.){3}\d{1,3}$/,/^syslog\s+port\s+\d{1,5}$/, + /^ip\s+(\d{1,3}\.){3}\d{1,3}$/, + /^ip\s+dhcp$/, + /^gw\s+(\d{1,3}\.){3}\d{1,3}$/, + /^netmask\s+(\d{1,3}\.){3}\d{1,3}$/, + /^syslog\s+(on|off)$/, + /^syslog\s+ip\s+(\d{1,3}\.){3}\d{1,3}$/, + /^syslog\s+port\s+\d{1,5}$/, /^session\s+\d{1,5}$/, /^dns\s+server\s+(\d{1,3}\.){3}\d{1,3}(\s+(\d{1,3}\.){3}\d{1,3})?$/, - /^ntp\s+(on|off)$/,/^ntp\s+server\s+[A-Za-z0-9.-]{1,47}$/,/^ntp\s+interval\s+\d{1,4}$/, - /^ntp\s+timezone\s+[+-]?\d{1,2}(:\d{2})?$/,/^ntp\s+dst\s+(off|eu|us)$/, + /^ntp\s+(on|off)$/, + /^ntp\s+server\s+[A-Za-z0-9.-]{1,47}$/, + /^ntp\s+interval\s+\d{1,4}$/, + /^ntp\s+timezone\s+[+-]?\d{1,2}(:\d{2})?$/, + /^ntp\s+dst\s+(off|eu|us)$/, /^passwd\s+\S+$/,/^hostname\s+\S{1,23}$/, - /^vlan\s+\d{1,4}\s+d$/,/^vlan\s+\d{1,4}\s+mgmt$/, - /^vlan\s+\d{1,4}(\s+[a-zA-Z]\w*)?(\s+\d{1,2}t?)+$/, - /^pvid\s+\d{1,2}\s+\d{1,4}$/, - /^ingress(\s+\d{1,2}[tua])+$/,/^ingress\s+[tua]$/, - /^port\s+\d{1,2}\s+(10m|100m|1g|2g5|5g|10g|auto|on|off)(\s+(half|full))?$/, - /^port\s+\d{1,2}\s+name\s+\S+$/, - /^port\s+\d{1,2}\s+lldp\s+(permit|block)+$/, - /^eee\s+(on|off)(\s+\d{1,2})?(\s+(100m|1g|2g5))?$/, - /^mirror(\s+\d{1,2})(\s+\d{1,2}[tr]?)+$/,/^mirror\s+off$/, - /^lag\s+[1-4](\s+\d{1,2})+$/,/^lag\s+[1-4]\s+d$/,/^lag\s+[1-4]\s+lacp(\s+\d{1,2})+$/,/^lag\s+[1-4]\s+lacp\s+off$/,/^laghash\s+[1-4](\s+\w+)+$/, - /^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}$/, + /^vlan\s+\d+\s+(d|mgmt)$/, + /^vlan\s+\d+(\s+[a-zA-Z]\w*)?(\s+\d+t?)*$/, + /^pvid\s+\d+\s+\d+$/, + /^ingress(\s+\d+[tua])+$/,/^ingress\s+[tua]$/, + /^port\s+\d+\s+(10m|100m|1g|2g5|5g|10g|auto|on|off)(\s+(half|full))?$/, + /^port\s+\d+\s+name\s+\S+$/, + /^port\s+\d+\s+lldp\s+(permit|block)+$/, + /^eee\s+(on|off)(\s+\d+)?(\s+(100m|1g|2g5))?$/, + /^mirror(\s+\d+)(\s+\d+[tr]?)+$/,/^mirror\s+off$/, + /^lag\s+[1-4](\s+\d+)+$/,/^lag\s+[1-4]\s+d$/, + /^lag\s+[1-4]\s+lacp(\s+\d+)+$/, + /^lag\s+[1-4]\s+lacp\s+off$/, + /^laghash\s+[1-4](\s+\w+)+$/, + /^isolate\s+\d+(\s+(off|\d+))+$/, + /^stp\s+(on|off)$/,/^stp\s+(prio|hello|maxage|fwd|txhold)\s+\d+$/, /^stp\s+version\s+(rstp|stp)$/, - /^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+$/,/^lldp\s+(on|off)$/, - /^bw\s+(in|out)\s+\d{1,2}\s+\S+$/, - /^storm\s+\d{1,2}\s+(bcast|mcast|ucast|umcast)\s+(off|\d{1,8}\s+(pps|kbps))$/, + /^stp\s+(port\s+\d+|lag\s+[1-4])\s+(on|off)$/, + /^stp\s+(port\s+\d+|lag\s+[1-4])\s+edge\s+(on|off|auto)$/, + /^stp\s+(port\s+\d+|lag\s+[1-4])\s+cost\s+\d+$/, + /^stp\s+(port\s+\d+|lag\s+[1-4])\s+prio\s+\d+$/, + /^stp\s+(port\s+\d+|lag\s+[1-4])\s+guard\s+(none|bpdu|root)$/, + /^stp\s+(port\s+\d+|lag\s+[1-4])\s+filter\s+(on|off)$/, + /^stp\s+(port\s+\d=|lag\s+[1-4])\s+p2p\s+(auto|on|off)$/, + /^igmp\s+(on|off)$/, + /^mtu\s+\d+\s+\d+$/, + /^lldp\s+(on|off)$/, + /^bw\s+(in|out)\s+\d+\s+\S+$/, + /^storm\s+\d+\s+(bcast|mcast|ucast|umcast)\s+(off|\d+\s+(pps|kbps))$/, ]; function isConfCmd(line){ for(var i=0;i Date: Sat, 24 Oct 2026 10:13:08 +0100 Subject: [PATCH 11/13] Fixing the unsaved changes not appearing. Previously if a set of changes were sent as a single cmd using \n separation then the dirty regex would skip it as $ wouldn't match \n. with /m modifier the regex will match --- html/app.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/html/app.js b/html/app.js index b47aca4f..226c60f6 100644 --- a/html/app.js +++ b/html/app.js @@ -804,9 +804,9 @@ var CONF_CMDS=[ /^ntp\s+timezone\s+[+-]?\d{1,2}(:\d{2})?$/, /^ntp\s+dst\s+(off|eu|us)$/, /^passwd\s+\S+$/,/^hostname\s+\S{1,23}$/, - /^vlan\s+\d+\s+(d|mgmt)$/, - /^vlan\s+\d+(\s+[a-zA-Z]\w*)?(\s+\d+t?)*$/, - /^pvid\s+\d+\s+\d+$/, + /^vlan\s+\d+\s+(d|mgmt)$/m, + /^vlan\s+\d+(\s+[a-zA-Z]\w*)?(\s+\d+t?)*$/m, + /^pvid\s+\d+\s+\d+$/m, /^ingress(\s+\d+[tua])+$/,/^ingress\s+[tua]$/, /^port\s+\d+\s+(10m|100m|1g|2g5|5g|10g|auto|on|off)(\s+(half|full))?$/, /^port\s+\d+\s+name\s+\S+$/, From 2d5424fedc91e0ed23309311b431380b602b040f Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sat, 24 Oct 2026 21:27:27 +0100 Subject: [PATCH 12/13] Fixing double firing of load vlan 1 when the tab is loaded --- html/app.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/html/app.js b/html/app.js index 226c60f6..ceb09924 100644 --- a/html/app.js +++ b/html/app.js @@ -1492,11 +1492,7 @@ function vlanRefresh() { href: "#vlan", text: String(v.id) + ($("vmgmtcur").dataset.id == v.id ? " ("+t("v_mgmt")+")":""), "data-id": v.id, - "data-name": v.name, - onclick: function(e) { - e.preventDefault(); - vlanLoad(v.id); - } + "data-name": v.name }); tr.insertCell().appendChild(l); tr.insertCell().textContent = v.name || ""; @@ -1742,7 +1738,7 @@ tabHooks.vlan = { }, }; function VlanInit() { - $("vtable").tBodies[0].firstChild.firstChild.firstChild.click(); + vlanLoad($("vtable").tBodies[0].firstChild.firstChild.firstChild.dataset.id) }; var l2Rows=[],l2SortCol="pport",l2SortDir=1,l2Gen=0; function l2Load(){ From 2e62f9eea74f71972b384470c3556d254f4cd9b6 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Sun, 25 Oct 2026 15:59:31 +0000 Subject: [PATCH 13/13] Adding the name of the port to the UI so it's easier to see what you are changing --- html/app.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/html/app.js b/html/app.js index ceb09924..5ddb3c5f 100644 --- a/html/app.js +++ b/html/app.js @@ -1560,6 +1560,8 @@ function buildVlanEdit() { if (tb.rows.length || !S.n) return; var hd = tb.insertRow(); hd.insertCell().className = "mut"; + var rN = tb.insertRow(); + rN.insertCell().textContent = t("c_name"); var rM = tb.insertRow(); rM.insertCell().textContent = t("v_member"); var rP = tb.insertRow(); @@ -1578,6 +1580,7 @@ function buildVlanEdit() { })); }); seg.children[0].classList.add("on"); + rN.insertCell().appendChild(document.createTextNode((S.ports.filter(x=>x.portNum==p)[0]).name)); rM.insertCell().appendChild(seg); rP.insertCell().appendChild(h("span", { id: "vp" + p })); })(p);