Files
DrDoof aff6ac0923 stp: full RSTP and a reworked Spanning Tree page
* stp: select the root port by cost and block redundant links
* stp: move the protocol to BANK4 and keep the command line in BANK2
* stp: implement the RSTP state machines of 802.1D-2004 clause 17
* stp: counters, more settings and a fuller Spanning Tree page
* stp: make a link aggregation group one port of the RSTP tree
* web: serve the Spanning Tree JSON from BANK4 as well

Kept in BANK1, the larger Spanning Tree page would leave under 1 kB
there for the rest of the web server and every page still to come, so
send_stp() and send_stp_counters() move to httpd/page_stp.c in BANK4
with the protocol, as banked calls, with their own copies of the few output
helpers and of the JSON header, so nothing in BANK4 calls into BANK1
directly or hands it a pointer into its code space. BANK4 takes the
free flash between BANK3 and the web files.

KP_9000_6XHML_X2: BANK1 44336, BANK2 36969, BANK3 11942, BANK4 20965
bytes of 49152.
2026-10-27 22:11:24 +01:00

524 lines
33 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Switch</title>
<style>
:root{
--bg:#f4f6f8;--panel:#ffffff;--panel2:#eaeef2;--bd:#d7dde4;--fg:#1a222c;--fg2:#5c6773;
--ac:#0969da;--acfg:#ffffff;--ok:#158744;--bad:#cf222e;--warn:#9a6700;--cy:#1b7c83;
--sel:rgba(9,105,218,.10);--shadow:0 2px 10px rgba(20,30,40,.07);
--s10:#b58900;--s100:#b58900;--s1000:#159744;--s2g5:#0d9488;--s5g:#0d9488;--s10g:#2563eb;
}
:root[data-theme="dark"]{
--bg:#0e1116;--panel:#161b22;--panel2:#1f262e;--bd:#2c333c;--fg:#e6edf3;--fg2:#8d96a0;
--ac:#4c8ef7;--acfg:#ffffff;--ok:#3fb950;--bad:#f85149;--warn:#d29922;--cy:#39c5cf;
--sel:rgba(76,142,247,.16);--shadow:0 2px 10px rgba(0,0,0,.35);
--s10:#d29922;--s100:#d29922;--s1000:#3fb950;--s2g5:#2dd4bf;--s5g:#2dd4bf;--s10g:#58a6ff;
}
:root[data-theme="sel-dark"]{
--bg:#103c48;--panel:#184956;--panel2:#2d5b69;--bd:#2d5b69;--fg:#adbcbc;--fg2:#72898f;
--ac:#4695f7;--acfg:#ffffff;--ok:#75b938;--bad:#fa5750;--warn:#dbb32d;--cy:#41c7b9;
--sel:rgba(70,149,247,.18);--shadow:0 2px 10px rgba(0,20,26,.35);
--s10:#dbb32d;--s100:#dbb32d;--s1000:#75b938;--s2g5:#41c7b9;--s5g:#41c7b9;--s10g:#4695f7;
}
:root[data-theme="sel-light"]{
--bg:#fbf3db;--panel:#ece3cc;--panel2:#d5cdb6;--bd:#d5cdb6;--fg:#53676d;--fg2:#909995;
--ac:#0072d4;--acfg:#ffffff;--ok:#489100;--bad:#d2212d;--warn:#ad8900;--cy:#009c8f;
--sel:rgba(0,114,212,.12);--shadow:0 2px 10px rgba(80,70,40,.12);
--s10:#ad8900;--s100:#ad8900;--s1000:#489100;--s2g5:#009c8f;--s5g:#009c8f;--s10g:#0072d4;
}
*{box-sizing:border-box;margin:0}
html,body{height:100%}
body{
font:14px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
background:var(--bg);color:var(--fg);display:flex;overflow:hidden;
}
button,input,select,textarea{font:inherit;color:var(--fg)}
a{color:var(--ac);text-decoration:none}
#nav{
width:200px;flex:none;background:var(--panel);border-right:1px solid var(--bd);
display:flex;flex-direction:column;position:sticky;top:0;height:100vh;
}
#brand{display:flex;align-items:center;gap:9px;padding:16px 16px 12px;font-weight:650;font-size:15px}
#brand svg{flex:none;stroke:var(--ac)}
#brand span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#navlist{list-style:none;padding:4px 8px;overflow-y:auto;flex:1}
#navlist li{
display:flex;align-items:center;gap:9px;padding:7px 10px;margin:1px 0;
border-radius:7px;cursor:pointer;color:var(--fg2);font-weight:500;
}
#navlist li svg{flex:none;stroke:currentColor}
#navlist li:hover{background:var(--panel2);color:var(--fg)}
#navlist li.act{background:var(--sel);color:var(--ac)}
#navfoot{padding:10px 16px;border-top:1px solid var(--bd);font-size:12px;color:var(--fg2)}
#wrap{flex:1;min-width:0;display:flex;flex-direction:column}
#top{
display:flex;align-items:center;flex-wrap:wrap;gap:10px;padding:8px 20px;
background:var(--panel);border-bottom:1px solid var(--bd);position:sticky;top:0;z-index:5;
}
#burger{display:none;background:none;border:0;cursor:pointer;padding:4px}
#ttitle{font-size:16px;font-weight:650;min-width:120px}
#dirty{
display:none;font-size:12px;font-weight:600;color:var(--warn);
border:1px solid var(--warn);border-radius:20px;padding:2px 10px;
}
#dirty.show{display:inline-block}
select.ctl,button.ctl{
background:var(--panel2);border:1px solid var(--bd);border-radius:7px;
padding:5px 9px;cursor:pointer;font-size:13px;white-space:nowrap;
}
button.pri{background:var(--ac);color:var(--acfg);border-color:transparent;font-weight:600}
button.pri:hover,button.ctl:hover{filter:brightness(1.07)}
button:disabled{opacity:.45;cursor:default;filter:none}
main{padding:18px 20px 40px;width:100%;overflow-y:auto;}
section.tab{display:none;max-width:1400px;}
section.tab.act{display:block}
.card{
background:var(--panel);border:1px solid var(--bd);border-radius:10px;
box-shadow:var(--shadow);padding:16px 18px;margin-bottom:16px;
}
.card h2{font-size:14px;font-weight:650;margin-bottom:12px;color:var(--fg)}
.card h2 .hint{font-weight:400;font-size:12px;color:var(--fg2);margin-left:8px}
.grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:16px;align-items:start}
.grid2 .card{margin-bottom:0}
table.t{width:100%;border-collapse:collapse;font-size:13px}
table.t th{
text-align:left;font-weight:600;color:var(--fg2);font-size:12px;
padding:6px 8px;border-bottom:1px solid var(--bd);white-space:nowrap;
}
table.t th[data-sort]{cursor:pointer;user-select:none}
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{
background:var(--bg);border:1px solid var(--bd);border-radius:7px;padding:5px 8px;outline:none;
}
input.in:focus,select.in:focus,textarea.in:focus{border-color:var(--ac)}
input.in.sm{width:7em}
label.row{display:flex;align-items:center;gap:10px;margin:8px 0}
label.row span.lb{width:110px;flex:none;color:var(--fg2);font-size:13px}
.switch{position:relative;display:inline-block;width:34px;height:19px;flex:none}
.switch input{opacity:0;width:0;height:0}
.switch i{
position:absolute;inset:0;background:var(--bd);border-radius:19px;transition:.15s;cursor:pointer;
}
.switch i:before{
content:"";position:absolute;width:15px;height:15px;border-radius:50%;
left:2px;top:2px;background:var(--panel);transition:.15s;
}
.switch input:checked+i{background:var(--ok)}
.switch input:checked+i:before{transform:translateX(15px)}
#strip{display:flex;gap:6px;flex:1;min-width:0;overflow-x:auto;scrollbar-width:thin}
.port:first-child{margin-left:auto}
.port:last-child{margin-right:auto}
.port{
width:40px;border:2px solid var(--bd);border-radius:7px;padding:3px 0 2px;
text-align:center;cursor:pointer;background:var(--panel2);position:relative;
}
.port.sfp{width:50px}
.port .pn{font-weight:650;font-size:12px}
.port .ps{font-size:9px;color:var(--fg2);display:block;line-height:1.2}
.port .jack{
display:block;width:21px;height:12px;margin:4px auto 2px;border-radius:2px;
border:2px solid var(--fg2);border-top-width:8px;opacity:.55;position:relative;
}
.port .jack:before{content:"";
position:absolute;bottom:100%;left:50%;transform:translateX(-50%);
width:6px;height:5px;margin-bottom:7px;border-radius: 1px 1px 0 0;
background-color:var(--fg2);
}
.port.sfp .jack{width:24px;height:16px;margin:0 auto 2px;border-top-width:5px}
.port.sfp .jack:before{width:10px;margin-bottom:2px;border-radius:0 0 1px 1px;background-color:var(--panel2)}
.port.up{border-color:var(--pc);background:color-mix(in srgb,var(--pc) 10%,var(--panel2))}
.port.up .jack{border-color:var(--pc);opacity:1}
.port.up .jack:before{background-color:var(--pc)}
.port.up.sfp .jack:before{background-color:color-mix(in srgb,var(--pc) 10%,var(--panel2))}
.port.up .ps{color:var(--pc);font-weight:600}
.port.dis{opacity:.45;border-style:dashed}
.badge{font-size:11px;font-weight:600;border-radius:10px;padding:1px 8px;background:var(--panel2);color:var(--fg2)}
.badge.ok{color:var(--ok)}.badge.bad{color:var(--bad)}
.seg{display:inline-flex;border:1px solid var(--bd);border-radius:7px;overflow:hidden}
.seg button{
background:var(--panel);border:0;padding:3px 8px;cursor:pointer;font-size:12px;color:var(--fg2);
border-right:1px solid var(--bd);
}
.seg button:last-child{border-right:0}
.seg button.on{background:var(--ac);color:var(--acfg)}
#toasts{position:fixed;right:16px;bottom:16px;z-index:50;display:flex;flex-direction:column;gap:8px}
.toast{
background:var(--panel);border:1px solid var(--bd);border-left:4px solid var(--ac);
border-radius:8px;padding:9px 14px;box-shadow:var(--shadow);font-size:13px;max-width:340px;
animation:tin .18s ease-out;
}
.toast.ok{border-left-color:var(--ok)}.toast.err{border-left-color:var(--bad)}
@keyframes tin{from{transform:translateY(8px);opacity:0}to{transform:none;opacity:1}}
#mback{
position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:40;display:none;
align-items:flex-start;justify-content:center;padding:6vh 16px;
}
#mback.show{display:flex}
#modal{
background:var(--panel);border:1px solid var(--bd);border-radius:12px;box-shadow:var(--shadow);
width:min(760px,96vw);max-height:86vh;display:flex;flex-direction:column;
}
#mhead{display:flex;align-items:center;padding:13px 18px;border-bottom:1px solid var(--bd)}
#mtitle{font-weight:650;font-size:15px;flex:1}
#mx{background:none;border:0;font-size:19px;cursor:pointer;color:var(--fg2);padding:0 2px}
#mbody{padding:14px 18px;overflow:auto}
#mfoot{padding:12px 18px;border-top:1px solid var(--bd);display:flex;gap:10px;justify-content:flex-end}
#mfoot:empty{display:none}
pre.cfg{
background:var(--bg);border:1px solid var(--bd);border-radius:8px;padding:10px 12px;
font:12px/1.5 ui-monospace,Consolas,monospace;white-space:pre-wrap;word-break:break-all;
}
textarea.cfg{
width:100%;min-height:200px;background:var(--bg);border:1px solid var(--bd);border-radius:8px;
padding:10px 12px;font:12px/1.5 ui-monospace,Consolas,monospace;resize:vertical;outline:none;
}
.mono{font-family:ui-monospace,Consolas,monospace;font-size:12px}
.mut{color:var(--fg2)}
.small{font-size:12px}
.dangerzone{border-color:color-mix(in srgb,var(--bad) 40%,var(--bd))}
button.danger{background:var(--bad);color:#fff;border-color:transparent}
@media (max-width:820px){
#nav{position:fixed;left:-210px;transition:left .18s;z-index:30;box-shadow:var(--shadow)}
#nav.open{left:0}
#burger{display:block}
main{padding:14px 12px 40px}
#top{padding:8px 10px;gap:7px}
#ttitle{font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#dirty{padding:2px 7px}
}
@media (max-width:720px){
#ttitle{flex:1}
#strip{order:3;flex-basis:100%}
}
</style>
<script>
(function(){
var pref;
try{pref=localStorage.getItem("theme")||"auto";}catch(e){pref="auto";}
var dark=window.matchMedia("(prefers-color-scheme: dark)").matches;
var t=pref;
if(pref==="auto")t=dark?"dark":"light";
if(pref==="auto-sel")t=dark?"sel-dark":"sel-light";
document.documentElement.dataset.theme=t;
})();
</script>
</head>
<body>
<nav id="nav">
<div id="brand">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="2">
<rect x="2" y="7" width="20" height="10" rx="2"/><path d="M6 11v2M10 11v2M14 11v2M18 11v2"/>
</svg>
<span id="brandname">Switch</span>
</div>
<ul id="navlist"></ul>
<div id="navfoot"><span id="fver"></span></div>
</nav>
<div id="wrap">
<header id="top">
<button id="burger" title="Menu">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--fg)" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
</button>
<span id="ttitle"></span>
<div id="strip"></div>
<span id="dirty" data-i18n="hdr_dirty" data-i18n-t="hdr_dirty_t">unsaved changes</span>
<button class="ctl" id="saveBtn" data-i18n="hdr_save" data-i18n-t="hdr_save_t">Save to flash</button>
</header>
<main>
<section class="tab act" id="tab-dash">
<div class="grid2">
<div class="card"><h2 data-i18n="d_system">System</h2><table class="t" id="sysinfo"></table></div>
<div class="card"><h2><span data-i18n="d_traffic">Traffic</span> <span class="hint" data-i18n="d_traffic_h">packets/s, live</span></h2>
<div class="scrollx"><table class="t" id="traffic">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="c_link">Link</th><th class="num" data-i18n="d_txpps">TX pps</th><th class="num" data-i18n="d_rxpps">RX pps</th><th class="num" data-i18n="d_txbad">TX bad</th><th class="num" data-i18n="d_rxbad">RX bad</th></tr></thead>
<tbody></tbody></table></div>
</div>
</div>
</section>
<section class="tab" id="tab-ports">
<div class="card"><h2 data-i18n="pt_title">Port configuration</h2>
<div class="scrollx"><table class="t" id="ptable">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="c_name">Name</th><th data-i18n="c_link">Link</th><th data-i18n="pt_conn">Devices learned</th><th data-i18n="pt_speed">Speed</th><th data-i18n="c_enabled">Enabled</th><th data-i18n="pt_mtu">MTU</th><th></th></tr></thead>
<tbody></tbody></table></div>
<p class="small mut" style="margin-top:8px" data-i18n="pt_sfp_note">SFP speed is set per slot and is not saved in the startup config.</p>
</div>
</section>
<section class="tab" id="tab-stp">
<div class="card"><h2><span data-i18n="stp_title">Spanning tree</span> <span class="hint" data-i18n="stp_h">RSTP, 802.1w</span></h2>
<label class="row"><span class="lb" data-i18n="c_enabled">Enabled</span>
<span class="switch"><input type="checkbox" id="stpen"><i></i></span></label>
<p class="small mono" id="stpids">-</p>
<div id="stpbridge" style="display:flex;gap:14px;flex-wrap:wrap;align-items:flex-end;margin-top:10px">
<label><span class="small mut" data-i18n="stp_version">Version</span><br><select class="in" id="stpver"><option value="rstp">RSTP</option><option value="stp" data-i18n="stp_v_stp">STP compatible</option></select></label>
<label><span class="small mut" data-i18n="stp_pcs">Path cost</span><br><select class="in" id="stppcs"><option value="long" data-i18n="stp_pcs_long">Long (32 bit)</option><option value="short" data-i18n="stp_pcs_short">Short (16 bit)</option></select></label>
<label><span class="small mut" data-i18n="stp_bh">BPDUs while disabled</span><br><select class="in" id="stpbh"><option value="flood" data-i18n="stp_bh_flood">Flood</option><option value="filter" data-i18n="stp_bh_filter">Filter</option></select></label>
<label><span class="small mut" data-i18n="stp_prio">Bridge priority</span><br><select class="in" id="stpprio"></select></label>
<label><span class="small mut" data-i18n="stp_hello">Hello [s]</span><br><input class="in sm" id="stphello" type="number" min="1" max="10"></label>
<label><span class="small mut" data-i18n="stp_maxage">Max age [s]</span><br><input class="in sm" id="stpmaxage" type="number" min="6" max="40"></label>
<label><span class="small mut" data-i18n="stp_fwd">Forward delay [s]</span><br><input class="in sm" id="stpfwd" type="number" min="4" max="30"></label>
<label><span class="small mut" data-i18n="stp_txhold">TX hold</span><br><input class="in sm" id="stptxhold" type="number" min="1" max="10"></label>
</div>
<div style="margin-top:10px"><button class="ctl pri" id="stpbapply" data-i18n="stp_bridge_apply">Apply bridge settings</button></div>
<p class="small mut" style="margin-top:8px" data-i18n="stp_prio_note">Lower priority wins the root election (default 8 = 32768). Settings apply immediately; use Save to flash to keep them.</p>
</div>
<div class="card"><h2 data-i18n="stp_portcfg">Port configuration</h2>
<div class="scrollx"><table class="t" id="stpcfg">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="c_enabled">Enabled</th><th data-i18n="stp_edge">Edge</th><th data-i18n="stp_pcost">Path cost</th><th data-i18n="stp_pprio">Priority</th><th data-i18n="stp_guard">Guard</th><th data-i18n="stp_filter">BPDU filter</th><th data-i18n="stp_p2p">Point-to-point</th><th></th></tr></thead>
<tbody></tbody></table></div>
<p class="small mut" style="margin-top:8px" data-i18n="stp_port_note">Edge ports forward immediately and cause no topology change; Auto treats a port as edge after 3 s without BPDUs, and any received BPDU revokes it. Path cost 0 = automatic.</p>
</div>
<div class="card"><h2 data-i18n="stp_status">Port status</h2>
<div class="scrollx"><table class="t" id="stpstat">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="stp_role">Role</th><th data-i18n="c_state">State</th><th data-i18n="stp_db">Designated bridge</th><th data-i18n="stp_dp">Designated port</th><th data-i18n="stp_dc">Designated cost</th><th data-i18n="stp_oedge">Oper. edge</th><th data-i18n="stp_op2p">Oper. P2P</th><th data-i18n="stp_proto">Protocol</th><th></th></tr></thead>
<tbody></tbody></table></div>
</div>
<div class="card"><details id="stpcntd"><summary><h2 style="display:inline" data-i18n="stp_cnt">Counters</h2></summary>
<div class="scrollx"><table class="t" id="stpcnt">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="stp_crx">BPDUs received</th><th data-i18n="stp_ctx">BPDUs sent</th><th data-i18n="stp_ctrx">TC received</th><th data-i18n="stp_cttx">TC sent</th><th data-i18n="stp_cage">Last BPDU [s]</th></tr></thead>
<tbody></tbody></table></div></details>
</div>
</section>
<section class="tab" id="tab-stats">
<div class="card"><h2><span data-i18n="st_title">Port statistics</span> <span class="hint" data-i18n="st_h">totals since boot</span></h2>
<div class="scrollx"><table class="t" id="stable">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="c_name">Name</th><th data-i18n="c_link">Link</th><th class="num" data-i18n="st_txg">TX good</th><th class="num" data-i18n="st_txb">TX bad</th><th class="num" data-i18n="st_rxg">RX good</th><th class="num" data-i18n="st_rxb">RX bad</th><th></th></tr></thead>
<tbody></tbody></table></div>
</div>
</section>
<section class="tab" id="tab-vlan">
<div class="card"><h2><span data-i18n="v_title">VLANs</span><span id="vmgmtcur"></span></h2>
<div class="scrollx"><table class="t" id="vtable">
<thead><tr><th data-i18n="v_vid">VID</th><th data-i18n="c_name">Name</th><th data-i18n="v_members">Members</th><th data-i18n="v_tagged">Tagged</th><th data-i18n="v_untagged">Untagged</th><th data-i18n="v_pvid_on">PVID on</th><th></th></tr></thead>
<tbody></tbody></table></div>
<p class="small mut" id="vempty" style="display:none;margin-top:6px" data-i18n="v_empty">No VLANs configured.</p>
<button class="ctl pri" id="vlan_add" data-i18n="v_add">Add a new VLAN</button>
</div>
<div class="card"><h2><span data-i18n="v_editor">VLAN editor</span> <span class="hint" id="vtitle"></span></h2>
<div><label class="row"><span class="lb" style="width:150px;" data-i18n="v_setmgmt_t"></span>
<span class="switch"><input type="checkbox" id="vmgmtpromote"><i></i></span>
</label></div>
<div class="scrollx"><table class="t" id="vedit"><tbody></tbody></table></div>
<div style="margin-top:10px"><button class="ctl pri" id="vapply" data-i18n="c_apply">Apply</button></div>
<p class="small mut" style="margin-top:6px" data-i18n="v_legend">U = untagged member, T = tagged member, - = not a member. PVID assigns this VLAN to untagged ingress traffic.</p>
</div>
</section>
<section class="tab" id="tab-l2">
<div class="card"><h2><span data-i18n="l2_title">MAC address table</span>
<span class="hint"><input class="in" id="l2filter" data-i18n-p="l2_filter" size="14"></span></h2>
<div style="display:flex;gap:10px;margin-bottom:10px">
<button class="ctl" id="l2refresh" data-i18n="c_refresh">Refresh</button>
<button class="ctl" id="l2flush" data-i18n="l2_flush">Flush learned entries</button>
<span class="small mut" id="l2count" style="align-self:center"></span>
</div>
<div class="scrollx"><table class="t" id="l2table">
<thead><tr><th data-sort="pport"><span data-i18n="c_port">Port</span><span class="arrow"></span></th><th data-sort="mac">MAC<span class="arrow"></span></th><th data-sort="vlan">VLAN<span class="arrow"></span></th><th data-sort="stat"><span data-i18n="c_type">Type</span><span class="arrow"></span></th><th></th></tr></thead>
<tbody></tbody></table></div>
</div>
</section>
<section class="tab" id="tab-mirror">
<div class="card"><h2 data-i18n="m_title">Port mirroring</h2>
<div style="display:flex;gap:12px;align-items:center;margin-bottom:10px;flex-wrap:wrap">
<span class="badge" id="mstate">off</span>
<label><span data-i18n="m_monitor">Monitor port</span> <select class="in" id="mport"></select></label>
<button class="ctl pri" id="mapply" data-i18n="c_apply">Apply</button>
<button class="ctl" id="moff" data-i18n="c_disable">Disable</button>
</div>
<div class="scrollx"><table class="t" id="mtable"><tbody></tbody></table></div>
<p class="small mut" style="margin-top:6px" data-i18n="m_note">Both = mirror RX and TX of the port to the monitor port.</p>
</div>
</section>
<section class="tab" id="tab-lldp">
<div class="card"><h2 data-i18n="lldp_title">LLDP</h2>
<label class="row"><span class="lb" data-i18n="lldp_enabled">Enabled</span>
<span class="switch"><input type="checkbox" id="lldp_en"><i></i></span>
</label>
<div class="scrollx"><table class="t" id="lldp_table">
<thead><tr><th data-i18n="c_port">Port Number</th><th data-i18n="lldp_portname">Name</th><th data-i18n="lldp_enable">Send Frames</th></tr></thead>
<tbody></tbody></table></div>
</div>
</section>
<section class="tab" id="tab-isolate">
<div class="card"><h2 data-i18n="iso_title">Port isolation</h2>
<p class="small mut" style="margin-bottom:10px" data-i18n="iso_intro">A frame entering a port may only leave through the ports ticked in its row.</p>
<div class="scrollx"><table class="t" id="isotbl" style="width:auto"><thead></thead><tbody></tbody></table></div>
<p class="small mut" style="margin-top:6px" data-i18n="iso_legend">The cell of the port itself is always allowed.</p>
<div style="display:flex;gap:12px;margin-top:10px">
<button class="ctl pri" id="isoapply" data-i18n="c_apply">Apply</button>
<button class="ctl" id="isoall" data-i18n="iso_all">Allow all</button>
</div>
</div>
</section>
<section class="tab" id="tab-lag">
<div id="lagwrap" class="grid2"></div>
<div class="card" style="margin-top:14px"><h2 data-i18n="lacp_heading">LACP (802.3ad)</h2>
<div class="scrollx"><table class="t" id="lacptbl">
<thead><tr><th data-i18n="c_port">Port</th><th>LAG</th><th data-i18n="lacp_actor">Actor</th><th data-i18n="lacp_partner">Partner</th><th data-i18n="lacp_rxstate">RX state</th><th data-i18n="lacp_rx">LACPDUs received</th><th data-i18n="lacp_psys">Partner system</th></tr></thead>
<tbody></tbody></table></div>
</div>
<p class="small mut" style="margin-top:10px" data-i18n="lag_note">A LAG needs at least one member to be saved in the startup config; applying an empty group clears it.</p>
</section>
<section class="tab" id="tab-eee">
<div class="card"><h2 data-i18n="e_title">Energy Efficient Ethernet</h2>
<div class="scrollx"><table class="t" id="etable">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="e_adv">Advertised</th><th data-i18n="e_lp">Link partner</th><th data-i18n="e_active">Active</th><th data-i18n="e_enable">Enable</th></tr></thead>
<tbody></tbody></table></div>
<p class="small mut" style="margin-top:8px" data-i18n="e_note">Advertised and link-partner flags per speed: 100M, 1G, 2.5G. SFP ports do not support EEE.</p>
</div>
</section>
<section class="tab" id="tab-bw">
<div class="card"><h2><span data-i18n="bw_title">Bandwidth limits</span> <span class="hint" data-i18n="bw_h">Mbit/s, 0.016-10000</span></h2>
<div class="scrollx"><table class="t" id="btable">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="bw_in">Ingress limit</th><th>Mbit/s</th><th data-i18n="bw_exceed">When exceeded</th><th data-i18n="bw_out">Egress limit</th><th>Mbit/s</th><th></th></tr></thead>
<tbody></tbody></table></div>
</div>
<div class="card"><h2><span data-i18n="sc_title">Storm control</span> <span class="hint" data-i18n="sc_h">per port and traffic type, empty = off</span></h2>
<div class="scrollx"><table class="t" id="sctable">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="sc_bcast">Broadcast</th><th data-i18n="sc_mcast">Multicast</th><th data-i18n="sc_ucast">Unknown unicast</th><th data-i18n="sc_umcast">Unknown multicast</th><th></th></tr></thead>
<tbody></tbody></table></div>
<p class="small mut" style="margin-top:8px" data-i18n="sc_note">Frames of a type above its limit are dropped where they enter the switch.</p>
</div>
</section>
<section class="tab" id="tab-system">
<div class="grid2">
<div class="card"><h2 data-i18n="sy_network">Network</h2>
<label class="row"><span class="lb" data-i18n="i_host">Hostname</span><input class="in" id="sy-host" maxlength="23"></label>
<label class="row"><span class="lb" data-i18n="i_ip">IP address</span><input class="in" id="sy-ip" size="15"></label>
<label class="row"><span class="lb" data-i18n="i_mask">Netmask</span><input class="in" id="sy-mask" size="15"></label>
<label class="row"><span class="lb" data-i18n="i_gw">Gateway</span><input class="in" id="sy-gw" size="15"></label>
<label class="row"><span class="lb" data-i18n="sy_dns1">DNS server</span><input class="in" id="sy-dns1" size="15"></label>
<label class="row"><span class="lb" data-i18n="sy_dns2">Second DNS</span><input class="in" id="sy-dns2" size="15"></label>
<p class="small mut" id="sy-dnsdhcp"></p>
<div style="display:flex;gap:10px;margin-top:12px">
<button class="ctl pri" id="sy-apply" data-i18n="c_apply">Apply</button>
<button class="ctl" id="sy-dhcp" data-i18n="sy_dhcp" data-i18n-t="sy_dhcp_t">Use DHCP</button>
</div>
</div>
<div class="card"><h2 data-i18n="sy_services">Services</h2>
<label class="row" style="justify-content:space-between"><span data-i18n="sy_igmp">IGMP snooping</span>
<span class="switch"><input type="checkbox" id="sy-igmp"><i></i></span></label>
<label class="row" style="justify-content:space-between"><span data-i18n="i_syslog">Syslog</span>
<span class="switch"><input type="checkbox" id="sy-syslog"><i></i></span></label>
<div class="row" style="display:flex;gap:8px;align-items:center;margin:-2px 0 10px">
<span class="small mut" data-i18n="sy_server">Server</span>
<input class="in" id="sy-sysip" style="flex:1;min-width:0;width:0" data-i18n-p="sy_sysip">
<span class="small mut" data-i18n="sy_port">Port</span>
<input class="in" id="sy-sysport" style="width:64px" type="number" min="1" max="65535"></div>
<label class="row" style="justify-content:space-between"><span data-i18n="sy_sesstmo">Session timeout (s)</span>
<input class="in" id="sy-sesstmo" style="width:80px" type="number" min="1" max="65535"></label>
<p class="small mut" data-i18n="sy_services_note">Service state reflects the startup config; runtime state is not readable.</p>
</div>
<div class="card" id="sy-ntpcard"><h2 data-i18n="sy_time">Time</h2>
<label class="row" style="justify-content:space-between"><span data-i18n="sy_ntp">NTP</span>
<span class="switch"><input type="checkbox" id="sy-ntp"><i></i></span></label>
<label class="row"><span class="lb" data-i18n="sy_ntpsrv">NTP server</span><input class="in" id="sy-ntpsrv" maxlength="47" data-i18n-p="sy_ntpsrv_p"></label>
<label class="row"><span class="lb" data-i18n="sy_ntpint">Poll interval [min]</span><input class="in sm" id="sy-ntpint" type="number" min="1" max="1440"></label>
<label class="row"><span class="lb" data-i18n="sy_tz">Time zone</span><select class="in" id="sy-tz"></select></label>
<label class="row"><span class="lb" data-i18n="sy_dst">Daylight saving</span><select class="in" id="sy-dst"><option value="off" data-i18n="c_offc">Off</option><option value="eu" data-i18n="sy_dst_eu">Europe</option><option value="us" data-i18n="sy_dst_us">United States</option></select></label>
<div style="display:flex;gap:10px;margin-top:8px">
<button class="ctl pri" id="sy-ntpapply" data-i18n="c_apply">Apply</button>
<button class="ctl" id="sy-ntprefresh" data-i18n="c_refresh">Refresh</button>
</div>
<label class="row" style="margin-top:10px"><span class="lb" data-i18n="sy_now">Local time</span><span class="mono" id="sy-now">-</span></label>
<label class="row"><span class="lb" data-i18n="sy_sync">Last synchronisation</span><span id="sy-sync">-</span></label>
<label class="row"><span class="lb" data-i18n="sy_ntpaddr">Server address</span><span class="mono" id="sy-ntpaddr">-</span></label>
<p class="small mut" style="margin-top:8px" data-i18n="sy_time_note">The switch has no battery backed clock.</p>
</div>
<div class="card"><h2 data-i18n="sy_password">Admin password</h2>
<label class="row"><span class="lb" data-i18n="sy_newpw">New password</span><input class="in" id="sy-pw1" type="password" autocomplete="new-password" maxlength="20"></label>
<label class="row"><span class="lb" data-i18n="sy_repeat">Repeat</span><input class="in" id="sy-pw2" type="password" autocomplete="new-password" maxlength="20"></label>
<button class="ctl pri" id="sy-pwapply" style="margin-top:8px" data-i18n="sy_pwapply">Change password</button>
<p class="small mut" style="margin-top:8px" data-i18n="sy_pw_note">Takes effect immediately; save to flash to keep it after reboot.</p>
</div>
<div class="card"><h2 data-i18n="sy_console">Console</h2>
<div style="display:flex;gap:8px">
<input class="in" id="sy-cmd" style="flex:1" data-i18n-p="sy_cmd" spellcheck="false">
<button class="ctl pri" id="sy-send" data-i18n="sy_send">Send</button>
</div>
<pre class="cfg" id="sy-cout" style="margin-top:10px;min-height:70px;max-height:180px;overflow:auto"></pre>
</div>
<div class="card"><h2 data-i18n="sy_display">Display</h2>
<label class="row"><span class="lb" data-i18n="sy_lang">Language</span>
<select class="in" id="langSel">
<option value="de">Deutsch</option>
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="ja">日本語</option>
<option value="zh">中文</option>
</select></label>
<label class="row"><span class="lb" data-i18n="sy_theme">Theme</span>
<select class="in" id="themeSel">
<option value="auto" data-i18n="th_auto">System</option>
<option value="auto-sel" data-i18n="th_auto_sel">System (Selenized)</option>
<option value="light" data-i18n="th_light">Light</option>
<option value="dark" data-i18n="th_dark">Dark</option>
<option value="sel-light" data-i18n="th_sel_light">Selenized Light</option>
<option value="sel-dark" data-i18n="th_sel_dark">Selenized Dark</option>
</select></label>
<p class="small mut" data-i18n="sy_display_note">Stored in this browser only.</p>
</div>
</div>
<div class="card" style="margin-top:16px"><h2><span data-i18n="sy_startup">Startup configuration</span>
<span class="hint"><span id="cfgbytes"></span>, <span data-i18n="sy_replayed">replayed on every boot</span></span></h2>
<textarea class="cfg" id="cfgedit" spellcheck="false"></textarea>
<div style="display:flex;gap:10px;margin-top:10px;flex-wrap:wrap">
<button class="ctl" id="cfgreload" data-i18n="sy_reload">Reload from flash</button>
<button class="ctl pri" id="cfgwrite" data-i18n="sy_write">Write to flash</button>
<span class="small mut" style="align-self:center" data-i18n="sy_unknown_note">Unknown lines are highlighted before writing.</span>
</div>
</div>
<div class="card dangerzone" style="margin-top:16px"><h2 data-i18n="sy_maint">Maintenance</h2>
<button class="ctl danger" id="sy-reboot" data-i18n="sy_reboot">Reboot switch</button>
</div>
</section>
<section class="tab" id="tab-fw">
<div class="card"><h2 data-i18n="fw_title">Firmware update</h2>
<p class="small mut" style="margin-bottom:12px" data-i18n="fw_intro">Upload an RTLPlayground image (512 KiB .bin). The image is staged to flash and CRC-checked; on success the switch reboots itself, re-verifies the image and applies it. The startup configuration is preserved.</p>
<input type="file" id="fwfile" accept=".bin">
<div id="fwinfo" class="small" style="margin:10px 0"></div>
<div style="display:flex;gap:10px;align-items:center">
<button class="ctl pri" id="fwup" disabled data-i18n="fw_upload">Upload</button>
<progress id="fwprog" max="100" value="0" style="display:none;width:220px"></progress>
<span class="small" id="fwstat"></span>
</div>
</div>
</section>
</main>
</div>
<div id="toasts"></div>
<div id="mback"><div id="modal">
<div id="mhead"><span id="mtitle"></span><button id="mx">&#x2715;</button></div>
<div id="mbody"></div><div id="mfoot"></div>
</div></div>
<script src="/app.js"></script>
</body>
</html>