diff --git a/html/style.css b/html/style.css index 581737a..84d3a2e 100644 --- a/html/style.css +++ b/html/style.css @@ -163,3 +163,4 @@ margin: 30px 0; } select { text-align-last: right; font-family: monospace} option { direction: rtl; font-family: sans-serif} +#vlanTable td { text-align: left; } diff --git a/html/vlan.html b/html/vlan.html index 7962776..b2ffa55 100644 --- a/html/vlan.html +++ b/html/vlan.html @@ -10,6 +10,13 @@

VLAN Configuration

+
+ + +
+
@@ -29,6 +36,22 @@
+

Configured VLANs

+ + + + + + + + + + + + + + +
VLANNameMember PortsTagged PortsUntagged PortsPVID PortsDelete
diff --git a/html/vlan.js b/html/vlan.js index 4a481e6..d0288ca 100644 --- a/html/vlan.js +++ b/html/vlan.js @@ -83,9 +83,129 @@ function fetchVLAN() { sendXHTTP(xhttp); } +function portsToRange(mask, nPorts) { + var parts = []; + var start = -1, prev = -1; + for (var p = 1; p <= nPorts; p++) { + var bit = physToLogPort[p - 1]; + if ((mask >> bit) & 1) { + if (start < 0) start = p; + prev = p; + } else { + if (start >= 0) { + parts.push(start === prev ? String(start) : start + '-' + prev); + start = -1; prev = -1; + } + } + } + if (start >= 0) + parts.push(start === prev ? String(start) : start + '-' + prev); + return parts.length ? parts.join(',') : '-'; +} + +async function loadVlanTable() { + var tbody = document.getElementById('vlanTableBody'); + if (!tbody) return; + tbody.innerHTML = ''; + var resp; + try { resp = await fetch('/vlanlist'); } catch(e) { return; } + if (!resp.ok) return; + var vlans = await resp.json(); + for (var i = 0; i < vlans.length; i++) { + var v = vlans[i]; + var vresp; + try { vresp = await fetch('/vlan.json?vid=' + v.id); } catch(e) { continue; } + if (!vresp.ok) continue; + var s = await vresp.json(); + var m = parseInt(s.members, 16); + var members = m & 0x3FF; + var untag = ((m >> 10) & 0x3FF) & members; + var tagged = members & ~untag; + var pvid = parseInt(s.pvid, 16) & 0x3FF; + var tr = document.createElement('tr'); + var td, a, btn; + td = document.createElement('td'); + a = document.createElement('a'); + a.href = '#'; + a.textContent = v.id; + (function(vid) { + a.onclick = function(e) { + e.preventDefault(); + document.getElementById('vid').value = vid; + fetchVLAN(); + }; + })(v.id); + td.appendChild(a); tr.appendChild(td); + td = document.createElement('td'); + td.textContent = v.name || ''; tr.appendChild(td); + td = document.createElement('td'); + td.textContent = portsToRange(members, numPorts); tr.appendChild(td); + td = document.createElement('td'); + td.textContent = portsToRange(tagged, numPorts); tr.appendChild(td); + td = document.createElement('td'); + td.textContent = portsToRange(untag, numPorts); tr.appendChild(td); + td = document.createElement('td'); + td.textContent = portsToRange(pvid, numPorts); tr.appendChild(td); + td = document.createElement('td'); + if (v.id !== 1) { + btn = document.createElement('button'); + btn.textContent = '✕'; + (function(vid) { + btn.onclick = function() { deleteVlan(vid); }; + })(v.id); + td.appendChild(btn); + } + tr.appendChild(td); + tbody.appendChild(tr); + } +} + +function deleteVlan(id) { + if (!confirm('Delete VLAN ' + id + '?')) return; + fetch('/cmd', { method: 'POST', body: 'vlan ' + id + ' d' }) + .then(function() { refreshVlanViews(); }) + .catch(function(err) { console.error('Delete failed:', err); }); +} + +function refreshVlanViews() { + loadVlanList(); + loadVlanTable(); +} + +function loadVlanList() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState !== 4) return; + var sel = document.getElementById('vlanSelect'); + if (this.status !== 200) { + sel.style.display = 'none'; + return; + } + var vlans = JSON.parse(this.responseText); + if (!vlans.length) { + sel.style.display = 'none'; + return; + } + sel.options.length = 1; + for (var i = 0; i < vlans.length; i++) { + var opt = document.createElement('option'); + opt.value = vlans[i].id; + opt.text = vlans[i].name ? vlans[i].id + ' — ' + vlans[i].name : String(vlans[i].id); + sel.appendChild(opt); + } + }; + xhttp.open('GET', '/vlanlist', true); + sendXHTTP(xhttp); +} + window.addEventListener("load", function() { update( () => { vlanForm(); + refreshVlanViews(); + document.getElementById('vlanSelect').onchange = function() { + document.getElementById('vid').value = this.value; + fetchVLAN(); + }; const interval = setInterval(update, 2000); }); }); diff --git a/html/vlan_sub.js b/html/vlan_sub.js index ea14c95..2816951 100644 --- a/html/vlan_sub.js +++ b/html/vlan_sub.js @@ -28,6 +28,7 @@ async function vlanSub() { }); console.log('Completed!', response); } + refreshVlanViews(); } catch(err) { console.error(`Error: ${err}`); } diff --git a/httpd/httpd.c b/httpd/httpd.c index 67d56d1..c05a7e1 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -682,6 +682,8 @@ void httpd_appcall(void) send_mtu(); } else if (is_word(q, "/lag.json")) { send_lag(); + } else if (is_word(q, "/vlanlist")) { + send_vlanlist(); } else if (is_word(q, "/config")) { send_config(); } else if (is_word(q, "/cmd_log")) { diff --git a/httpd/page_impl.c b/httpd/page_impl.c index 0010063..66b234e 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -97,6 +97,19 @@ void itoa_html(uint8_t v) char_to_html('0' + (v % 10)); } +void itoa16_html(uint16_t v) /* sufficient for VLAN IDs (max 4094) */ +{ + uint8_t print_zeros = 0; + uint8_t d; + d = v / 1000; + if (d) { char_to_html('0' + d); print_zeros = 1; } + d = (v / 100) % 10; + if (d || print_zeros) { char_to_html('0' + d); print_zeros = 1; } + d = (v / 10) % 10; + if (d || print_zeros) char_to_html('0' + d); + char_to_html('0' + (v % 10)); +} + void string_to_html(__code char *s) { while (*s) char_to_html(*s++); @@ -825,3 +838,46 @@ void send_cmd_log(void) p = (p + 1) & CMD_HISTORY_MASK; } } + + +void send_vlanlist(void) +{ + /* Worst case per entry: {"id":4094,"name":"<117-char name>"} = 138 bytes + * (name bound: CMD_BUF_SIZE=128 minus command prefix); +1 for closing ']'. + * At worst case ~18 VLANs fit; typical configs with short names fit many more. */ + __xdata uint16_t i; + __xdata uint16_t n; + uint8_t first = 1; + + slen = strtox(outbuf, HTTP_RESPONCE_JSON); + char_to_html('['); + + for (i = 1; i < 4095; i++) { + if (vlan_get(i) < 0) + continue; + if (!(sfr_data[0] & 0x02)) /* bit 1: VLAN table entry valid */ + continue; + + if (slen + 139 > TCP_OUTBUF_SIZE) /* 138 bytes worst-case entry + 1 byte for closing ']' */ + break; + + if (!first) + char_to_html(','); + first = 0; + + slen += strtox(outbuf + slen, "{\"id\":"); + itoa16_html(i); + slen += strtox(outbuf + slen, ",\"name\":\""); + + n = vlan_name(i); + if (n != 0xffff) { + while (vlan_names[n] && vlan_names[n] != ' ') + char_to_html(vlan_names[n++]); + } + + slen += strtox(outbuf + slen, "\"}"); + + } + + char_to_html(']'); +} diff --git a/httpd/page_impl.h b/httpd/page_impl.h index 5591e14..7907285 100644 --- a/httpd/page_impl.h +++ b/httpd/page_impl.h @@ -14,6 +14,7 @@ void send_mtu(void); void send_config(void); void send_cmd_log(void); void send_lag(void); +void send_vlanlist(void); /* Convert only the lower nibble to ascii HEX char. For convenience the upper nibble is masked out.