Merge pull request #233 from Erdnusschokolade/feature/vlan-overview

Add VLAN selector dropdown and overview table to vlan.html
This commit is contained in:
feelfree69
2026-05-25 12:30:30 +02:00
committed by GitHub
7 changed files with 204 additions and 0 deletions
+1
View File
@@ -163,3 +163,4 @@ margin: 30px 0;
} }
select { text-align-last: right; font-family: monospace} select { text-align-last: right; font-family: monospace}
option { direction: rtl; font-family: sans-serif} option { direction: rtl; font-family: sans-serif}
#vlanTable td { text-align: left; }
+23
View File
@@ -10,6 +10,13 @@
<div id="ports"></div> <div id="ports"></div>
<h1>VLAN Configuration</h1> <h1>VLAN Configuration</h1>
<form id="vform" action="/vlan.html"> <form id="vform" action="/vlan.html">
<div>
<label for="vlanSelect">VLAN auswählen:</label>
<select id="vlanSelect" style="margin: 0 0 0 8px">
<option value="" disabled selected>— VLAN wählen —</option>
</select>
</div>
<br/>
<div> <div>
<label for="vid">VLAN ID:</label> <label for="vid">VLAN ID:</label>
<input type="number" min="1" max="4094" id="vid" name="vid"> <input type="number" min="1" max="4094" id="vid" name="vid">
@@ -29,6 +36,22 @@
<br/> <input style="width:40%;" class="action" id="vlan_sub" onclick="vlanSub();" type="button" value="Update / Create"> <br/> <input style="width:40%;" class="action" id="vlan_sub" onclick="vlanSub();" type="button" value="Update / Create">
<script src="/vlan_sub.js"></script> <script src="/vlan_sub.js"></script>
</form> </form>
<h2>Configured VLANs</h2>
<table id="vlanTable" style="width:90%">
<thead>
<tr>
<th>VLAN</th>
<th>Name</th>
<th>Member Ports</th>
<th>Tagged Ports</th>
<th>Untagged Ports</th>
<th>PVID Ports</th>
<th>Delete</th>
</tr>
</thead>
<tbody id="vlanTableBody">
</tbody>
</table>
</div> </div>
<script src="/navigation.js"></script> <script src="/navigation.js"></script>
</body> </body>
+120
View File
@@ -83,9 +83,129 @@ function fetchVLAN() {
sendXHTTP(xhttp); 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() { window.addEventListener("load", function() {
update( () => { update( () => {
vlanForm(); vlanForm();
refreshVlanViews();
document.getElementById('vlanSelect').onchange = function() {
document.getElementById('vid').value = this.value;
fetchVLAN();
};
const interval = setInterval(update, 2000); const interval = setInterval(update, 2000);
}); });
}); });
+1
View File
@@ -28,6 +28,7 @@ async function vlanSub() {
}); });
console.log('Completed!', response); console.log('Completed!', response);
} }
refreshVlanViews();
} catch(err) { } catch(err) {
console.error(`Error: ${err}`); console.error(`Error: ${err}`);
} }
+2
View File
@@ -682,6 +682,8 @@ void httpd_appcall(void)
send_mtu(); send_mtu();
} else if (is_word(q, "/lag.json")) { } else if (is_word(q, "/lag.json")) {
send_lag(); send_lag();
} else if (is_word(q, "/vlanlist")) {
send_vlanlist();
} else if (is_word(q, "/config")) { } else if (is_word(q, "/config")) {
send_config(); send_config();
} else if (is_word(q, "/cmd_log")) { } else if (is_word(q, "/cmd_log")) {
+56
View File
@@ -97,6 +97,19 @@ void itoa_html(uint8_t v)
char_to_html('0' + (v % 10)); 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) void string_to_html(__code char *s)
{ {
while (*s) char_to_html(*s++); while (*s) char_to_html(*s++);
@@ -825,3 +838,46 @@ void send_cmd_log(void)
p = (p + 1) & CMD_HISTORY_MASK; 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(']');
}
+1
View File
@@ -14,6 +14,7 @@ void send_mtu(void);
void send_config(void); void send_config(void);
void send_cmd_log(void); void send_cmd_log(void);
void send_lag(void); void send_lag(void);
void send_vlanlist(void);
/* Convert only the lower nibble to ascii HEX char. /* Convert only the lower nibble to ascii HEX char.
For convenience the upper nibble is masked out. For convenience the upper nibble is masked out.