From 2974e4b66ac3448980d3747d148485cab4cb7d4b Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 5 Aug 2026 21:21:03 +0200 Subject: [PATCH 1/5] l2: stop labelling the SFP port as CPU The port column was mapped from logical to physical numbering first and the CPU label applied afterwards, testing for port 9. On an 8+1 board the SFP port maps to physical 9 as well, so every entry learned on the SFP was shown as CPU - on this switch that was 26 of 30 entries. Label the CPU while the number is still logical, before the mapping, so the two cannot collide. --- html/l2.js | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/html/l2.js b/html/l2.js index 25b1b1d..f68f837 100644 --- a/html/l2.js +++ b/html/l2.js @@ -64,6 +64,47 @@ function delL2(idx) { xhttp.timeout = 1500; xhttp.send(); } +var l2All = []; +var l2SortCol = 'port'; +var l2SortDir = 1; + +/* Sort keys: ports are numbers except the CPU, which must not compare as one. */ +function l2Key(e, col) { + if (col === 'port') return e.port === 'CPU' ? Number.MAX_SAFE_INTEGER : Number(e.port); + if (col === 'vlan') return Number(e.vlan); + return String(e[col]).toLowerCase(); +} + +function l2SortBy(col) { + l2SortDir = (col === l2SortCol) ? -l2SortDir : 1; + l2SortCol = col; + renderL2(); +} + +function l2FilterChanged() { renderL2(); } + +function renderL2() { + var tbl = document.getElementById('l2table'); + if (!tbl) return; + var f = {}; + ['port', 'mac', 'vlan', 'type'].forEach(function(c) { + var el = document.getElementById('l2f_' + c); + f[c] = el ? el.value.trim().toLowerCase() : ''; + }); + var rows = l2All.filter(function(e) { + return ['port', 'mac', 'vlan', 'type'].every(function(c) { + return !f[c] || String(e[c]).toLowerCase().indexOf(f[c]) !== -1; + }); + }); + rows.sort(function(a, b) { + var x = l2Key(a, l2SortCol), y = l2Key(b, l2SortCol); + return (x < y ? -1 : x > y ? 1 : 0) * l2SortDir; + }); + paintL2(tbl, rows); + var cnt = document.getElementById('l2count'); + if (cnt) cnt.textContent = rows.length + ' / ' + l2All.length; +} + function fillL2(s) { var tbl = document.getElementById('l2table'); @@ -71,7 +112,13 @@ function fillL2(s) return; s.sort(l2CMP); s = uniq(s); - var s = s.map(function(e) { e.port = e.port != 9 ? e.port : 'CPU'; return e; }); + l2All = s; + renderL2(); + l2Entries = []; +} + +function paintL2(tbl, s) +{ console.log("L2: ", JSON.stringify(s)); for (let i = 0; i < s.length; i++) { var e = s[i]; @@ -92,7 +139,6 @@ function fillL2(s) } for (let i = tbl.rows.length - 1; i > s.length; i--) tbl.deleteRow(i); - l2Entries = []; } function getL2() { @@ -104,7 +150,10 @@ function getL2() { e.vlan = parseInt(e.vlan, 16); e.idx = parseInt(e.idx, 16); e.type = e.type == "s" ? t('l2_static') : t('l2_learned'); - e.port = e.port == 9 ? 9 : logToPhysPort[e.port]; + /* Label the CPU port before mapping to physical numbering: the + * SFP port maps to physical 9 as well, and tagging afterwards + * relabelled every SFP entry as CPU. */ + e.port = e.port == 9 ? 'CPU' : logToPhysPort[e.port]; return e; }); l2Entries.push(...s); From 5e901fb5c2eab857d6a89171aff5e2dedffd8371 Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 5 Aug 2026 21:21:03 +0200 Subject: [PATCH 2/5] l2: sort and filter the forwarding table from its header The table lists every learned and static entry in one flat block, which is fine with a handful and unusable with a few hundred: finding out where one MAC sits, or what a port has learned, meant reading the whole thing. Make the column headings sort and give each one a filter box. Filters are substring matches combined with AND, so "port 8 + static" is two keystrokes. A counter above the table shows matched out of total, so a filter that hides everything is obvious rather than looking like an empty table. Sorting keeps the CPU entry from comparing as a number - it sorts last instead of landing between ports 8 and 9, where a string-vs-number compare would otherwise put it. The filter inputs live inside the existing header cells rather than in a second row: the paint loop addresses data rows as rows[i+1], and a second header row would have shifted every one of them. --- html/i18n.js | 3 +++ html/l2.html | 12 +++++++++++- html/style.css | 5 +++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/html/i18n.js b/html/i18n.js index bab49dc..fb4bce9 100644 --- a/html/i18n.js +++ b/html/i18n.js @@ -117,6 +117,7 @@ var LANG = { l2_col_port: 'Port', l2_col_type: 'Type', l2_col_remove: 'Remove Entry', + l2_shown: 'Shown:', l2_delete: 'Delete', l2_static: 'static', l2_learned: 'learned', @@ -296,6 +297,7 @@ var LANG = { l2_col_port: 'ポート', l2_col_type: 'タイプ', l2_col_remove: 'エントリ削除', + l2_shown: 'Shown:', l2_delete: '削除', l2_static: '静的', l2_learned: '学習', @@ -475,6 +477,7 @@ var LANG = { l2_col_port: '端口', l2_col_type: '类型', l2_col_remove: '删除条目', + l2_shown: 'Shown:', l2_delete: '删除', l2_static: '静态', l2_learned: '动态学习', diff --git a/html/l2.html b/html/l2.html index 029c4e9..b739d0f 100644 --- a/html/l2.html +++ b/html/l2.html @@ -10,8 +10,18 @@

L2 Configuration

+

Shown: -

- + + + + + +
Port MAC VLAN Type Remove Entry
Port
+
MAC
+
VLAN
+
Type
+
Remove Entry
diff --git a/html/style.css b/html/style.css index 84d3a2e..3533c6b 100644 --- a/html/style.css +++ b/html/style.css @@ -164,3 +164,8 @@ margin: 30px 0; select { text-align-last: right; font-family: monospace} option { direction: rtl; font-family: sans-serif} #vlanTable td { text-align: left; } + +/* L2 table: sortable headers and per-column filters */ +.l2sort{cursor:pointer;user-select:none} +.l2sort:hover{text-decoration:underline} +.l2filter{width:100%;box-sizing:border-box;font-weight:normal;font-size:0.9em} From 4389cfcd8b57d5bf972265bdee10fa2e26811bbc Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 5 Aug 2026 21:35:54 +0200 Subject: [PATCH 3/5] l2: show which column sorts and in which direction Clicking a heading sorted the table but nothing said so afterwards - the only feedback was the rows moving, which is no help when the sort key is a column you are not looking at. Give every sortable heading a permanent marker: a neutral double arrow when it is not the sort key, up or down when it is. The marker sits in its own span so the i18n pass, which replaces the heading text, does not wipe it. --- html/l2.html | 8 ++++---- html/l2.js | 4 ++++ html/style.css | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/html/l2.html b/html/l2.html index b739d0f..6134372 100644 --- a/html/l2.html +++ b/html/l2.html @@ -13,13 +13,13 @@

Shown: -

- - - - diff --git a/html/l2.js b/html/l2.js index f68f837..20125dc 100644 --- a/html/l2.js +++ b/html/l2.js @@ -100,6 +100,10 @@ function renderL2() { var x = l2Key(a, l2SortCol), y = l2Key(b, l2SortCol); return (x < y ? -1 : x > y ? 1 : 0) * l2SortDir; }); + ['port', 'mac', 'vlan', 'type'].forEach(function(c) { + var a = document.getElementById('l2a_' + c); + if (a) a.textContent = (c === l2SortCol) ? (l2SortDir > 0 ? ' \u25b2' : ' \u25bc') : ' \u21c5'; + }); paintL2(tbl, rows); var cnt = document.getElementById('l2count'); if (cnt) cnt.textContent = rows.length + ' / ' + l2All.length; diff --git a/html/style.css b/html/style.css index 3533c6b..758800b 100644 --- a/html/style.css +++ b/html/style.css @@ -168,4 +168,5 @@ option { direction: rtl; font-family: sans-serif} /* L2 table: sortable headers and per-column filters */ .l2sort{cursor:pointer;user-select:none} .l2sort:hover{text-decoration:underline} +.l2arrow{opacity:0.55;font-size:0.85em} .l2filter{width:100%;box-sizing:border-box;font-weight:normal;font-size:0.9em} From ccb90ed16f47e56d530bb3f89807d6326fec7a49 Mon Sep 17 00:00:00 2001 From: d00f Date: Wed, 5 Aug 2026 21:42:54 +0200 Subject: [PATCH 4/5] l2: refresh the type column when a row is reused The paint loop reuses existing rows and rewrites port, MAC, VLAN and the delete button, but never the type cell - only the insert path set it. With a fixed row order that stayed invisible, since a row usually landed back where it was. Sorting moves rows, so every other column followed the data while type kept the previous row's value, which made sorting by type look broken when the sort itself was correct. --- html/l2.js | 1 + 1 file changed, 1 insertion(+) diff --git a/html/l2.js b/html/l2.js index 20125dc..21bd91c 100644 --- a/html/l2.js +++ b/html/l2.js @@ -131,6 +131,7 @@ function paintL2(tbl, s) tbl.rows[i+1].cells[0].innerHTML = `${e.port}`; tbl.rows[i+1].cells[1].innerHTML = `${e.mac}`; tbl.rows[i+1].cells[2].innerHTML = `${e.vlan}`; + tbl.rows[i+1].cells[3].innerHTML = `${e.type}`; tbl.rows[i+1].cells[4].innerHTML = ''; } else { const tr = tbl.insertRow(); From 22a09bd06aaafc67615caa75db62087ec3c8a019 Mon Sep 17 00:00:00 2001 From: d00f Date: Sat, 8 Aug 2026 17:55:02 +0200 Subject: [PATCH 5/5] l2: drop the comments and define the column list once Review feedback on #307: these files are served from flash byte for byte, so comments ride along on every page load. The three added in this branch are gone, 357 bytes across l2.js and style.css. The column list existed in three copies inside renderL2() and is now a single const. --- html/l2.js | 11 ++++------- html/style.css | 1 - 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/html/l2.js b/html/l2.js index 21bd91c..d3a058b 100644 --- a/html/l2.js +++ b/html/l2.js @@ -65,10 +65,10 @@ function delL2(idx) { } var l2All = []; +const l2Cols = ['port', 'mac', 'vlan', 'type']; var l2SortCol = 'port'; var l2SortDir = 1; -/* Sort keys: ports are numbers except the CPU, which must not compare as one. */ function l2Key(e, col) { if (col === 'port') return e.port === 'CPU' ? Number.MAX_SAFE_INTEGER : Number(e.port); if (col === 'vlan') return Number(e.vlan); @@ -87,12 +87,12 @@ function renderL2() { var tbl = document.getElementById('l2table'); if (!tbl) return; var f = {}; - ['port', 'mac', 'vlan', 'type'].forEach(function(c) { + l2Cols.forEach(function(c) { var el = document.getElementById('l2f_' + c); f[c] = el ? el.value.trim().toLowerCase() : ''; }); var rows = l2All.filter(function(e) { - return ['port', 'mac', 'vlan', 'type'].every(function(c) { + return l2Cols.every(function(c) { return !f[c] || String(e[c]).toLowerCase().indexOf(f[c]) !== -1; }); }); @@ -100,7 +100,7 @@ function renderL2() { var x = l2Key(a, l2SortCol), y = l2Key(b, l2SortCol); return (x < y ? -1 : x > y ? 1 : 0) * l2SortDir; }); - ['port', 'mac', 'vlan', 'type'].forEach(function(c) { + l2Cols.forEach(function(c) { var a = document.getElementById('l2a_' + c); if (a) a.textContent = (c === l2SortCol) ? (l2SortDir > 0 ? ' \u25b2' : ' \u25bc') : ' \u21c5'; }); @@ -155,9 +155,6 @@ function getL2() { e.vlan = parseInt(e.vlan, 16); e.idx = parseInt(e.idx, 16); e.type = e.type == "s" ? t('l2_static') : t('l2_learned'); - /* Label the CPU port before mapping to physical numbering: the - * SFP port maps to physical 9 as well, and tagging afterwards - * relabelled every SFP entry as CPU. */ e.port = e.port == 9 ? 'CPU' : logToPhysPort[e.port]; return e; }); diff --git a/html/style.css b/html/style.css index 758800b..1830557 100644 --- a/html/style.css +++ b/html/style.css @@ -165,7 +165,6 @@ select { text-align-last: right; font-family: monospace} option { direction: rtl; font-family: sans-serif} #vlanTable td { text-align: left; } -/* L2 table: sortable headers and per-column filters */ .l2sort{cursor:pointer;user-select:none} .l2sort:hover{text-decoration:underline} .l2arrow{opacity:0.55;font-size:0.85em}
Port
+
Port
MAC
+
MAC
VLAN
+
VLAN
Type
+
Type
Remove Entry