From 67ae4598d95b1ec3a7973676459d3ed644d944b4 Mon Sep 17 00:00:00 2001 From: eraiza0816 <10572974+eraiza0816@users.noreply.github.com> Date: Tue, 11 Aug 2026 06:37:20 +0900 Subject: [PATCH] feat(webui): turn the Web UI into a single-page application index.html now contains all pages as sections (overview, ports, stat, vlan, l2, mirror, lag, eee, bandwidth, system, update); the sidebar switches between them via the URL hash (#/ports, #/vlan, ...). login.html stays separate as the authentication gate. A section's initialisation runs when the section is shown, its polling intervals run only while it is visible, and re-entering a section refetches the data; the checkbox builders (lag/vlan/mirror) and the L2 scanner were made idempotent so re-initialising cannot duplicate rows. tools/minify.py: recognise regex literals - an escaped slash before the closing delimiter was previously stripped as a // comment. Measured (make MACHINE=KP_9000_6XHML_X2): 8 embedded files, 109,814 -> 97,730 -> 25,302 bytes gzip (23.0 %); the data block ends at 0x462d6 (~86 KB freed). On a real PCB-K0402WS-V3: cold load ~450 ms, section switches ~60-160 ms with no asset re-downloads, all sections render without JavaScript errors. --- html/bandwidth.html | 18 --- html/eee.html | 22 ---- html/index.html | 289 ++++++++++++++++++++++++++++++++++++++++---- html/l2.html | 27 ----- html/lag.html | 25 ---- html/main.js | 150 +++++++++++++++-------- html/mirror.html | 23 ---- html/ports.html | 22 ---- html/stat.html | 45 ------- html/system.html | 104 ---------------- html/update.html | 19 --- html/vlan.html | 55 --------- tools/minify.py | 36 +++++- 13 files changed, 397 insertions(+), 438 deletions(-) delete mode 100644 html/bandwidth.html delete mode 100644 html/eee.html delete mode 100644 html/l2.html delete mode 100644 html/lag.html delete mode 100644 html/mirror.html delete mode 100644 html/ports.html delete mode 100644 html/stat.html delete mode 100644 html/system.html delete mode 100644 html/update.html delete mode 100644 html/vlan.html diff --git a/html/bandwidth.html b/html/bandwidth.html deleted file mode 100644 index 57ce9459..00000000 --- a/html/bandwidth.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Ingress and Egress Bandwidth - - - -
-
-

Ingress and Egress Bandwidth

- - - -
Ingress Egress
Port Limit Bandwidth [kBit/s] Flow Control Limit Bandwidth [kBit/s] Apply
-
- - diff --git a/html/eee.html b/html/eee.html deleted file mode 100644 index a2c10eca..00000000 --- a/html/eee.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - EEE Configuration - - - -
-
-

EEE Status

- - - -
Advertising Link-Partner advertises
Port 2.5G 1G 100M 2.5G 1G 100M Active?
-
- - -
-
- - diff --git a/html/index.html b/html/index.html index e0de34ab..332fc527 100644 --- a/html/index.html +++ b/html/index.html @@ -1,29 +1,274 @@ - FreeSwitchOS Main Page - + + + + +
- - -
-
-

Switch Configuration

- - - - - - -
Settings
-
- +
+

Switch Configuration

+ + + + + + +
Settings
+
+ +
+

Port Configuration

+ + +
Port Name Current Link SpeedSet SpeedDisabledApply
+

Configure Maximum Frame Size (MTU) forwarded at Port

+ +
+
+ +
+ +

Port Statistics

+ + +
Port Name link TX Good TX Bad RX Good RX Bad All Counters
+
+ +
+

VLAN Configuration

+
+
+ + +
+
+
+ + + +
+

+ +

+
+

Tagged Ports

+
+

Untagged Ports

+
+

Use as default VLAN for incoming traffic (PVID)

+
+
+
+

Configured VLANs

+ + + + + + + + + + + + + + +
VLANNameMember PortsTagged PortsUntagged PortsPVID PortsDelete
+
+ +
+

L2 Configuration

+

Shown: -

+ + + + + + + +
Port
+
MAC
+
VLAN
+
Type
+
Remove Entry
+
+ +
+

Mirror Configuration

+
+ +

Mirrored Ports (TX)

+
+
+

Mirrored Ports (RX)

+
+
+ +
+ +
+

Link Aggregation Groups Configuration

+

LAG 1

+
+
+

LAG 2

+
+
+

LAG 3

+
+
+

LAG 4

+
+
+ +
+

EEE Status

+ + + +
Advertising Link-Partner advertises
Port 2.5G 1G 100M 2.5G 1G 100M Active?
+
+ + +
+
+ +
+

Ingress and Egress Bandwidth

+ + + +
Ingress Egress
Port Limit Bandwidth [kBit/s] Flow Control Limit Bandwidth [kBit/s] Apply
+
+ +
+
+ + + +
+
+

System Settings

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ When updating the above settings, remember to point your browser to the new IP afterwards:
+
+
+ Save all current settings to Flash:
+ +
+ +
+

Advanced Settings

+
+ +

+ Be careful when saving the directly edited startup configuration, you can lock yourself out:
+ +
+ +
+ +
+ +
+

Console Command

+ + +
+

+ Be careful when entering console commands, you can lock yourself out!
+
+
+ +
+

Firmware Update

+
+ + Choose a firmware update file to upload:

+
+ +
+
+ diff --git a/html/l2.html b/html/l2.html deleted file mode 100644 index 7ba2e1d1..00000000 --- a/html/l2.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - FreeSwitchOS L2 Configuration - - - -
-
-

L2 Configuration

-

Shown: -

- - - - - - - -
Port
-
MAC
-
VLAN
-
Type
-
Remove Entry
-
- - diff --git a/html/lag.html b/html/lag.html deleted file mode 100644 index 8e3e9cbc..00000000 --- a/html/lag.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - Link Aggregation Configuration - - - -
-
-

Link Aggregation Groups Configuration

-

LAG 1

-
-
-

LAG 2

-
-
-

LAG 3

-
-
-

LAG 4

-
-
- - diff --git a/html/main.js b/html/main.js index e4daa829..7d14c621 100644 --- a/html/main.js +++ b/html/main.js @@ -899,17 +899,17 @@ document.addEventListener('DOMContentLoaded', function() { var sidebarEl = document.getElementById('sidebar'); if (sidebarEl) sidebarEl.innerHTML = - ""; + ""; }); document.addEventListener('DOMContentLoaded', function() { @@ -920,6 +920,54 @@ document.addEventListener('DOMContentLoaded', function() { }); }); +/* Single-page navigation: the sidebar links set the URL hash + * ("#/ports" and friends) and the matching section in index.html is + * shown. Each section initialises on first display, and its polling + * intervals run only while the section is visible. */ +var sectionIntervals = []; +var sectionInits = {}; + +function setSectionInterval(fn, ms) { + sectionIntervals.push(setInterval(fn, ms)); +} + +function clearSectionIntervals() { + sectionIntervals.forEach(function(t) { clearInterval(t); }); + sectionIntervals = []; +} + +function showSection(name) { + document.querySelectorAll('.page').forEach(function(el) { el.style.display = 'none'; }); + var sec = document.getElementById('page-' + name); + if (sec) sec.style.display = 'block'; + clearSectionIntervals(); + if (sectionInits[name]) + sectionInits[name](); +} + +window.addEventListener('hashchange', function() { + showSection((location.hash || '#/overview').replace(/^#\//, '')); +}); + +document.addEventListener('DOMContentLoaded', function() { + showSection((location.hash || '#/overview').replace(/^#\//, '')); +}); + +sectionInits.overview = function() { + update( () => { + setSectionInterval(update, 2000); + }); +}; + +/* System page tabs (system.html section of index.html) */ +function openTab(evt, tabId) { + document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + document.getElementById(tabId).classList.add('active'); + evt.currentTarget.classList.add('active'); +} + + var configInterval = Number(); var configuration = []; const conf_cmds = [ @@ -1195,12 +1243,11 @@ function resetSwitch() { }, 3000); } -window.addEventListener("load", function() { - if (!document.getElementById('ip')) return; +sectionInits.system = function() { var langSel = document.getElementById('lang-select'); if (langSel) langSel.value = rtlLang; - systemInterval = setInterval(fetchIP, 1000); -}); + systemInterval = setSectionInterval(fetchIP, 1000); +}; var mgmtVlanCurrent = 0; @@ -1382,16 +1429,15 @@ function getMTUs() { xhttp.timeout = 1500; sendXHTTP(xhttp); } -window.addEventListener("load", function() { - if (!document.getElementById('speedtable')) return; +sectionInits.ports = function() { update( () => { createPortTable(); updatePortTable(); getMTUs() - const interval = setInterval(update, 2000); - const updatePortTableInterval = setInterval(updatePortTable, 1000); + setSectionInterval(update, 2000); + setSectionInterval(updatePortTable, 1000); }); -}); +}; const mib_counters = [ "Interface in Octets", 8, @@ -1595,15 +1641,14 @@ document.addEventListener('DOMContentLoaded', function() { }); }); -window.addEventListener("load", function() { - if (!document.getElementById('statstable')) return; +sectionInits.stat = function() { update( () => { update(); fillStats(); - const stat = setInterval(fillStats, 1000); - const interval = setInterval(update, 2000); + setSectionInterval(fillStats, 1000); + setSectionInterval(update, 2000); }); -}); +}; var l2GetInterval; var l2Entries = []; @@ -1765,20 +1810,23 @@ function getL2() { xhttp.timeout = 1500; sendXHTTP(xhttp); } -window.addEventListener("load", function() { - if (!document.getElementById('l2table')) return; +sectionInits.l2 = function() { + l2Entries = []; + l2CurrentEntry = 0; update( () => { getL2(); - const interval = setInterval(update, 2000); - l2GetInterval = setInterval(getL2, 1000); - });; -}); + setSectionInterval(update, 2000); + l2GetInterval = setSectionInterval(getL2, 1000); + }); +}; var vlanInterval = Number(); function vlanForm() { if (!numPorts) return; + document.querySelectorAll('#tPorts .cbgroup, #uPorts .cbgroup, #pPorts .cbgroup') + .forEach(function(el) { el.remove(); }); var t = document.getElementById('tPorts'); var u = document.getElementById('uPorts'); var p = document.getElementById('pPorts'); @@ -1974,8 +2022,7 @@ function loadVlanList() { sendXHTTP(xhttp); } -window.addEventListener("load", function() { - if (!document.getElementById('vlanSelect')) return; +sectionInits.vlan = function() { update( () => { vlanForm(); refreshVlanViews(); @@ -1983,9 +2030,9 @@ window.addEventListener("load", function() { document.getElementById('vid').value = this.value; fetchVLAN(); }; - const interval = setInterval(update, 2000); + setSectionInterval(update, 2000); }); -}); +}; async function vlanSub() { var commands = []; @@ -2028,6 +2075,8 @@ var lagInterval = Number(); function lagForm() { if (!numPorts) return; + for (let j = 0; j < 4; j++) + document.getElementById("mLAG" + j).innerHTML = ''; for (let j=0; j < 4; j++) { var lag = "mLAG" + j console.log("Adding LAG " + lag) @@ -2098,13 +2147,12 @@ async function lagSub(l) { } } -window.addEventListener("load", function() { - if (!document.getElementById('mLAG0')) return; +sectionInits.lag = function() { update( () => { lagForm(); - const interval = setInterval(update, 2000); + setSectionInterval(update, 2000); }); -}); +}; var mirrorInterval = Number(); const mirrors = ["mPortsTX", "mPortsRX"]; @@ -2112,6 +2160,7 @@ const mirrors = ["mPortsTX", "mPortsRX"]; function mirrorForm() { if (!numPorts) return; + mirrors.forEach(function(m) { document.getElementById(m).innerHTML = ''; }); for (let j=0; j < mirrors.length; j++) { console.log("Adding Mirror " + j) var m = document.getElementById(mirrors[j]); @@ -2164,13 +2213,12 @@ function fetchMirror() { sendXHTTP(xhttp); } -window.addEventListener("load", function() { - if (!document.getElementById('mPortsTX')) return; +sectionInits.mirror = function() { update( () => { mirrorForm(); - const interval = setInterval(update, 2000); + setSectionInterval(update, 2000); }); -}); +}; async function mirrorSub() { var cmd = "mirror "; @@ -2260,15 +2308,14 @@ function getEEE() { xhttp.timeout = 1500; sendXHTTP(xhttp); } -window.addEventListener("load", function() { - if (!document.getElementById('eeetable')) return; +sectionInits.eee = function() { update( () => { createEEE(); getEEE(); - const interval = setInterval(update, 2000); - const iCount = setInterval(getEEE, 2000); + setSectionInterval(update, 2000); + setSectionInterval(getEEE, 2000); }); -}); +}; async function eeeSub(port, enable) { var cmd = "eee "; @@ -2419,14 +2466,13 @@ function getBW() { xhttp.timeout = 1500; sendXHTTP(xhttp); } -window.addEventListener("load", function() { - if (!document.getElementById('bwtable')) return; +sectionInits.bandwidth = function() { update( () => { createBW(); getBW(); - const interval = setInterval(update, 2000); + setSectionInterval(update, 2000); }); -}); +}; document.addEventListener("DOMContentLoaded", function () { if (!document.getElementById('infoTable')) return; diff --git a/html/mirror.html b/html/mirror.html deleted file mode 100644 index 105f3218..00000000 --- a/html/mirror.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - Mirror Configuration - - - -
-
-

Mirror Configuration

-
- -

Mirrored Ports (TX)

-
-
-

Mirrored Ports (RX)

-
-
- -
- - diff --git a/html/ports.html b/html/ports.html deleted file mode 100644 index 233be139..00000000 --- a/html/ports.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - FreeSwitchOS Port Configuration - - - -
-
-

Port Configuration

-
- - -
Port Name Current Link SpeedSet SpeedDisabledApply
-

Configure Maximum Frame Size (MTU) forwarded at Port

- -
-
-
- - diff --git a/html/stat.html b/html/stat.html deleted file mode 100644 index ba114fcd..00000000 --- a/html/stat.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - FreeSwitchOS Port Statistics - - - - -
-
- -

Port Statistics

- - -
Port Name link TX Good TX Bad RX Good RX Bad All Counters
-
- - diff --git a/html/system.html b/html/system.html deleted file mode 100644 index af9c4c61..00000000 --- a/html/system.html +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - System Settings - - - -
- - - -
- -
-
- -
-

System Settings

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- When updating the above settings, remember to point your browser to the new IP afterwards:
-
-
- Save all current settings to Flash:
- -
- -
-

Advanced Settings

-
- -

- Be careful when saving the directly edited startup configuration, you can lock yourself out:
- -
- -
- -
- -
-

Console Command

- - -
-

- Be careful when entering console commands, you can lock yourself out!
- -
- -
- - - diff --git a/html/update.html b/html/update.html deleted file mode 100644 index fce51f58..00000000 --- a/html/update.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - Firmware update - - - -
-

Firmware Update

-
- - Choose a firmware update file to upload:

-
- -
- - diff --git a/html/vlan.html b/html/vlan.html deleted file mode 100644 index b682cc05..00000000 --- a/html/vlan.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - FreeSwitchOS VLAN Configuration - - - -
-
-

VLAN Configuration

-
-
- - -
-
-
- - - -
-

- -

-
-

Tagged Ports

-
-

Untagged Ports

-
-

Use as default VLAN for incoming traffic (PVID)

-
-
-
-

Configured VLANs

- - - - - - - - - - - - - - -
VLANNameMember PortsTagged PortsUntagged PortsPVID PortsDelete
-
- - diff --git a/tools/minify.py b/tools/minify.py index 8c033044..6f7a6864 100644 --- a/tools/minify.py +++ b/tools/minify.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 """Safe minifier for the WebUI sources (HTML/JS). -Removes comments and line-leading whitespace without touching string -literals, so the output is functionally identical to the input. Used -by the firmware Makefile before the files are embedded in the flash; -the raw sources in html/ stay untouched for development. +Removes comments and line-leading whitespace without touching string or +regex literals, so the output is functionally identical to the input. +Used by the firmware Makefile before the files are embedded in the +flash; the raw sources in html/ stay untouched for development. Binary files (.ico) are copied through unchanged. @@ -19,6 +19,10 @@ def js_min(s): out = [] i = 0 n = len(s) + prev = '\n' + # Characters after which a '/' starts a regex literal rather than a + # division: operator / delimiter / statement-start contexts. + regex_start = set('([{=,:;!&|?+-*%^~<>') while i < n: c = s[i] if c == '"' or c == "'": @@ -29,6 +33,28 @@ def js_min(s): j += 1 out.append(s[i:j + 1]) i = j + 1 + prev = c + elif c == '/' and (i + 1 >= n or s[i + 1] not in '/=*') and \ + (prev in regex_start or prev == '\n'): + # Regex literal (e.g. /^#\// or /https?:\/\/x/g): copy through + # the closing '/', honouring escapes and character classes. + j = i + 1 + in_class = False + while j < n: + ch = s[j] + if ch == '\\': + j += 2 + continue + if ch == '[': + in_class = True + elif ch == ']': + in_class = False + elif ch == '/' and not in_class: + break + j += 1 + out.append(s[i:j + 1]) + i = j + 1 + prev = '/' elif c == '/' and i + 1 < n and s[i + 1] == '/': j = s.find('\n', i) i = j if j >= 0 else n @@ -38,6 +64,8 @@ def js_min(s): else: out.append(c) i += 1 + if c not in ' \t': + prev = c lines = [l.strip() for l in ''.join(out).split('\n')] return '\n'.join(l for l in lines if l) + '\n'