From 57e3fce95e580b5b823c7b9d585324d9cc8a8e47 Mon Sep 17 00:00:00 2001 From: eraiza0816 <10572974+eraiza0816@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:04:19 +0900 Subject: [PATCH 01/12] feat(webui): minify and gzip-compress the embedded web assets Two build-time steps shrink the embedded Web UI (html/) from 113,015 to 34,784 bytes (30.8 %): - tools/minify.py (new): string-literal-safe minifier that strips comments and indentation; comments are pure overhead on the wire, so the transfer size drops already before gzip (113,015 -> 101,498). Binary files (.ico) are copied through unchanged. The output goes to output/html_min/, the new fileadder input. - fileadder -z: gzip-compresses every added file (zlib gzip format, Z_BEST_COMPRESSION); the generated f_data entries carry a gzip flag. tools/Makefile links the tool against -lz. - httpd: adds "Content-Encoding: gzip" for flagged entries; the 8051 only copies compressed bytes, decompression happens in the browser. The JSON API stays uncompressed. Fixes required to make the sizes deterministic: - fileadder terminated files at data_read + 1, so the strlen()-based size computation depended on uninitialised buffer content; the NUL now lands directly after the content. - version.h was .PHONY, so every build rewrote it while parallel builds could read a half-written copy. - the file list was unscoped and missed .css/.ico; it is now scoped to html/ and covers all embedded types. Measured with make MACHINE=KP_9000_6XHML_X2: data block end moves from 0x5b977 to 0x487e0 (~78 KB freed on a 512 KB image). Verified with MACHINE=PCB_K0402WS_V3 and on real hardware (PCB-K0402WS-V3). --- Makefile | 23 +++++++++++---- httpd/httpd.c | 5 +++- tools/Makefile | 2 +- tools/fileadder.c | 69 +++++++++++++++++++++++++++++++++++++++------ tools/minify.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 15 deletions(-) create mode 100644 tools/minify.py diff --git a/Makefile b/Makefile index 0cbc04f8..b2069549 100644 --- a/Makefile +++ b/Makefile @@ -75,10 +75,23 @@ SRCS += \ OBJS = ${SRCS:%.c=$(BUILDDIR)/%.rel} DEPS := ${SRCS:%.c=$(BUILDDIR)/%.d} -HTML := $(shell find $(html) -name '*.js' -or -name '*.html' -or -name '*.svg') +HTML := $(shell find html -name '*.js' -or -name '*.html' -or -name '*.svg' -or -name '*.css' -or -name '*.ico') -html_data.c html_data.h: $(HTML) tools/output/fileadder - tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -b BANK1 -d html -p html_data +# Minified copy of the web UI sources, used as the fileadder input. +# The raw html/ sources stay untouched for development; the minified +# copy is a build artifact under output/. +HTML_MIN := output/html_min +.PHONY: html_min +html_min: $(HTML) + rm -rf $(HTML_MIN) + mkdir -p $(HTML_MIN) + @for f in $(HTML); do python3 tools/minify.py $$f $(HTML_MIN)/$$(basename $$f) || exit 1; done + +html_data.c html_data.h: $(HTML) tools/output/fileadder html_min + tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -b BANK1 -z -d $(HTML_MIN) -p html_data +# httpd.c includes html_data.h; declare it explicitly so a parallel +# build (-j) does not compile httpd before the header is generated. +$(BUILDDIR)/httpd/httpd.rel: html_data.h $(VERSION_HEADER): @echo "#ifndef VERSION_H" > $(VERSION_HEADER) @@ -118,11 +131,11 @@ $(BUILDDIR)/rtlplayground-$(FILENAME_EXTENSION).bin: $(BUILDDIR)/rtlplayground.i tools/output/imagebuilder -i $^ $@ tools/output/fileadder -a $(DEFAULT_CONFIG_LOCATION) -s $(IMAGESIZE) -d config.txt $@ tools/output/fileadder -a $(CONFIG_LOCATION) -s $(IMAGESIZE) -d config.txt $@ - tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -d html -p html_data -b BANK1 $@ + tools/output/fileadder -a $(HTML_LOCATION) -s $(IMAGESIZE) -z -d $(HTML_MIN) -p html_data -b BANK1 $@ tools/output/crc_calculator -u $@ ln -sf $(MACHINE)/rtlplayground-$(FILENAME_EXTENSION).bin output/rtlplayground.bin -.PHONY: clean all $(SUBDIRS) $(VERSION_HEADER) +.PHONY: clean all $(SUBDIRS) .PHONY: machine_check: diff --git a/httpd/httpd.c b/httpd/httpd.c index 561bd936..622cacb2 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -719,7 +719,10 @@ void httpd_appcall(void) * response; without advertising it a browser reuses the socket * from its keep-alive pool and the next request hits the already * closed connection (a POST is then dropped without a retry). */ - slen += strtox(outbuf + slen, "; charset=UTF-8\r\nCache-Control: max-age=60, must-revalidate\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\nContent-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; form-action 'self'\r\n\r\n"); + slen += strtox(outbuf + slen, "; charset=UTF-8\r\n"); + if (f_data[entry].gzip) + slen += strtox(outbuf + slen, "Content-Encoding: gzip\r\n"); + slen += strtox(outbuf + slen, "Cache-Control: max-age=60, must-revalidate\r\nConnection: close\r\nAccess-Control-Allow-Origin: *\r\nContent-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; form-action 'self'\r\n\r\n"); len_left = f_data[entry].len; if (len_left > (TCP_OUTBUF_SIZE - slen)) { diff --git a/tools/Makefile b/tools/Makefile index c7143c53..1b1c6685 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -15,7 +15,7 @@ $(BUILDDIR)/injector: injector.c gcc $^ $(CCFLAGS) $@ $(BUILDDIR)/fileadder: fileadder.c - gcc $^ $(CCFLAGS) $@ + gcc $^ $(CCFLAGS) $@ -lz $(BUILDDIR)/crc_calculator: crc_calculator.c gcc $^ $(CCFLAGS) $@ diff --git a/tools/fileadder.c b/tools/fileadder.c index 71d862b3..77c53253 100644 --- a/tools/fileadder.c +++ b/tools/fileadder.c @@ -11,6 +11,7 @@ #include #include #include +#include #define OFFSET 2 @@ -33,6 +34,7 @@ int fbuf_p; int xbuf_p; bool addsDir = false; int callNum = 0; +struct arguments arguments; const char *argp_program_version = "fileadder 0.1"; const char *argp_program_bug_address = ""; @@ -45,6 +47,7 @@ static struct argp_option options[] = { { "address", 'a', "SIZE", 0, "Address where data is placed, default is 0x1000000 if option is used, otherwise 0x1fd000"}, { "prefix", 'p', "FILE", 0, "Prefix for header and index file generation"}, { "bank", 'b', "BANKNAME", 0, "Generate #pragma with given bank-name"}, + { "gzip", 'z', 0, 0, "gzip-compress the added files"}, { 0 } }; @@ -57,6 +60,7 @@ struct arguments { char *bank; bool overwrite; bool add_zero; + bool gzip; }; @@ -86,6 +90,9 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) case 'b': arguments->bank = arg? arg : "BANK1"; break; + case 'z': + arguments->gzip = true; + break; default: return ARGP_ERR_UNKNOWN; } @@ -118,6 +125,47 @@ int addfile(const char *name, int addr) } +/* + * gzip-compresses the data at buffer[addr] in place (gzip format, as + * expected by the Content-Encoding: gzip header the httpd serves it + * with). Returns the compressed size. + */ +int gzipBuffer(int addr, int len) +{ + uLong bound = compressBound(len); + unsigned char *out = malloc(bound); + if (!out) { + perror("malloc"); + return 0; + } + + z_stream zs; + memset(&zs, 0, sizeof(zs)); + if (deflateInit2(&zs, Z_BEST_COMPRESSION, Z_DEFLATED, 15 + 16, 8, + Z_DEFAULT_STRATEGY) != Z_OK) { + fprintf(stderr, "gzip: deflateInit2 failed\n"); + free(out); + return 0; + } + zs.next_in = (Bytef *)&buffer[addr]; + zs.avail_in = len; + zs.next_out = out; + zs.avail_out = bound; + if (deflate(&zs, Z_FINISH) != Z_STREAM_END) { + fprintf(stderr, "gzip: deflate failed\n"); + deflateEnd(&zs); + free(out); + return 0; + } + int out_len = zs.total_out; + deflateEnd(&zs); + + memcpy(&buffer[addr], out, out_len); + free(out); + return out_len; +} + + int hasSuffix(const char *str, const char *suffix) { if (!str || !suffix) @@ -164,9 +212,9 @@ int addidx(const char *name, int addr, int len) defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "#define FDATA_START_%s 0x%x\n", s, addr); defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "#define FDATA_SIZE_%s %d\n", s, len); - ibuf_p += snprintf(&ibuf[ibuf_p], INDEX_SIZE - ibuf_p, " {\"/%s\", FDATA_START_%s, FDATA_SIZE_%s, %s},\n", name, s, s, getMime(name)); + ibuf_p += snprintf(&ibuf[ibuf_p], INDEX_SIZE - ibuf_p, " {\"/%s\", FDATA_START_%s, FDATA_SIZE_%s, %s, %d},\n", name, s, s, getMime(name), arguments.gzip? 1 : 0); if (!strcmp(name, "index.html")) - ibuf_p += snprintf(&ibuf[ibuf_p], INDEX_SIZE - ibuf_p, " {\"/\", FDATA_START_%s, FDATA_SIZE_%s, mime_HTML},\n", s, s); + ibuf_p += snprintf(&ibuf[ibuf_p], INDEX_SIZE - ibuf_p, " {\"/\", FDATA_START_%s, FDATA_SIZE_%s, mime_HTML, %d},\n", s, s, arguments.gzip? 1 : 0); return 0; } @@ -216,7 +264,6 @@ int replaceCalls(int pos) int main(int argc, char **argv) { - struct arguments arguments; int arg_index; char tmpfilename[] = "image_XXXXXX"; struct stat s; @@ -227,6 +274,7 @@ int main(int argc, char **argv) arguments.overwrite = true; arguments.prefix = 0; arguments.bank = NULL; + arguments.gzip = false; argp_parse(&argp, argc, argv, 0, &arg_index, &arguments); if (!arg_index) @@ -269,7 +317,7 @@ int main(int argc, char **argv) defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "#include \n\n"); defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "typedef enum mime_type_e {\n mime_HTML = 0,\n mime_SVG,\n mime_ICO,\n mime_PNG,\n mime_JS,\n mime_CSS,\n mime_TXT\n} mime_type_t;\n\n"); - defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "struct f_data {\n __code char *file;\n uint32_t start;\n uint16_t len;\n mime_type_t mime;\n};\n\n"); + defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "struct f_data {\n __code char *file;\n uint32_t start;\n uint16_t len;\n mime_type_t mime;\n uint8_t gzip;\n};\n\n"); // defbuf_p += snprintf(&dbuf[defbuf_p], DEF_SIZE - defbuf_p, "typedef uint16_t (* fcall_ptr)(void);\n\n"); ibuf_p += snprintf(&ibuf[ibuf_p], INDEX_SIZE - ibuf_p, "// This file is automatically generated, do not edit!\n\n"); @@ -309,16 +357,21 @@ int main(int argc, char **argv) size_t data_read = addfile(pathbuffer, addr); if (data_read) { - if (arguments.add_zero) { - buffer[addr + data_read + 1] = '\0'; - data_read++; - } + if (arguments.add_zero) { + /* NUL-terminate the data so the file table and the + * strlen()-based size computation are deterministic + * (the buffer past the file is otherwise uninitialised) */ + buffer[addr + data_read] = '\0'; + data_read++; + } printf("Data inserted from %s at 0x%x, size: %ld\n", pathbuffer, addr, data_read); } int old_len = data_read; data_read = replaceCalls(addr); if (old_len > data_read) memset(buffer + addr + data_read, 0, old_len - data_read); + if (arguments.gzip) + data_read = gzipBuffer(addr, data_read); addidx(in_file->d_name, addr, data_read); addr += data_read; } diff --git a/tools/minify.py b/tools/minify.py new file mode 100644 index 00000000..8c033044 --- /dev/null +++ b/tools/minify.py @@ -0,0 +1,72 @@ +#!/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. + +Binary files (.ico) are copied through unchanged. + +Usage: minify.py +""" +import re +import shutil +import sys + + +def js_min(s): + out = [] + i = 0 + n = len(s) + while i < n: + c = s[i] + if c == '"' or c == "'": + j = i + 1 + while j < n and s[j] != c: + if s[j] == '\\': + j += 1 + j += 1 + out.append(s[i:j + 1]) + i = j + 1 + elif c == '/' and i + 1 < n and s[i + 1] == '/': + j = s.find('\n', i) + i = j if j >= 0 else n + elif c == '/' and i + 1 < n and s[i + 1] == '*': + j = s.find('*/', i + 2) + i = j + 2 if j >= 0 else n + else: + out.append(c) + i += 1 + lines = [l.strip() for l in ''.join(out).split('\n')] + return '\n'.join(l for l in lines if l) + '\n' + + +def html_min(s): + s = re.sub(r'', '', s, flags=re.S) + lines = [l.strip() for l in s.split('\n')] + return '\n'.join(l for l in lines if l) + '\n' + + +def main(): + if len(sys.argv) != 3: + print('usage: minify.py ', file=sys.stderr) + sys.exit(1) + src, dst = sys.argv[1], sys.argv[2] + if src.endswith('.ico'): + shutil.copyfile(src, dst) + print('%s: copied %d bytes' % (src.split('/')[-1], len(open(src, 'rb').read()))) + return + with open(src, 'r', encoding='utf-8', errors='replace') as f: + s = f.read() + if src.endswith('.js'): + out = js_min(s) + else: + out = html_min(s) + with open(dst, 'w', encoding='utf-8') as f: + f.write(out) + print('%s: %d -> %d bytes' % (src.split('/')[-1], len(s), len(out))) + + +if __name__ == '__main__': + main() From 53f33bd6060f8855231f7335bbd5a60c4bfb69d3 Mon Sep 17 00:00:00 2001 From: eraiza0816 <10572974+eraiza0816@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:06:45 +0900 Subject: [PATCH 02/12] docs(webui): describe the minify + gzip asset compression Pipeline, measured per-stage sizes (113,015 -> 101,498 -> 34,784 bytes) and how to verify a build against the embedded image. --- doc/webui-compression.md | 83 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 doc/webui-compression.md diff --git a/doc/webui-compression.md b/doc/webui-compression.md new file mode 100644 index 00000000..fea2cd19 --- /dev/null +++ b/doc/webui-compression.md @@ -0,0 +1,83 @@ +# Compressing the Web UI Assets + +The Web UI (all files under `html/`) is embedded into the flash image and +served over HTTP. Two build-time steps keep it small: a safe minifier and +gzip compression of the embedded files. + +## How it works + +``` +html/ --(minify.py)--> output/html_min/ --(fileadder -z)--> flash image +``` + +- `tools/minify.py` removes comments (``, `//`, `/* */`) and + line-leading whitespace without touching string literals, so the output + is functionally identical to the input. Binary files (`.ico`) are + copied through unchanged. The raw sources in `html/` stay untouched + for development; the minified copy in `output/` is a pure build + artifact. Comments and indentation are pure overhead for the served + bytes, so minification alone already reduces the transfer size from + 113,015 to 101,498 bytes, independent of gzip. +- `fileadder -z` gzip-compresses every file (zlib, gzip format, + `Z_BEST_COMPRESSION`) when it generates the file table + (`html_data.c`/`html_data.h`) and when it embeds the files into the + final image. Both invocations run on `output/html_min/`, so the table + and the image always agree. +- The httpd serves each file with `Content-Encoding: gzip` according to + the `gzip` flag in its `f_data` entry. The 8051 only copies compressed + bytes from flash to the socket; decompression happens in the browser. +- Only the static files are compressed. The JSON API responses + (`/information.json`, `/vlanlist`, ...) are generated on the fly and + stay uncompressed. + +## Measured effect + +Build on any machine (the Web UI is machine-independent): + +| | bytes | +|---|---:| +| raw sources | 113,015 | +| minified (comments and indentation removed) | 101,498 | +| gzip (flash + wire) | 34,784 (30.8 %) | + +The minified assets are also what the browser receives when gzip support +is missing or disabled, so the transfer size drops in both stages: +113,015 → 101,498 bytes from minification, then → 34,784 bytes from gzip. + +The embedded data block ends at `0x5b977` without compression and at +`0x487e0` with it, freeing ~78 KB of the 512 KB image. + +## Notes + +- The files stay well below the `uint16_t` size limit of the file table + (largest gzip output: 5.4 KB for `i18n.js`). +- `fileadder` terminates the embedded files with a NUL directly after the + content (previously at `data_read + 1`), so the `strlen()`-based size + computation no longer depends on uninitialised buffer content. +- `version.h` is no longer `.PHONY`; every build previously rewrote it + while a parallel build (`make -j`) could read a half-written copy. + +## Verification + +Decompress what the firmware embeds and compare it with the minified +sources: + +```python +import gzip, re +h = open('html_data.h').read(); c = open('html_data.c').read() +starts = dict((k, int(v, 16)) for k, v in + re.findall(r'#define FDATA_START_(\w+) 0x([0-9a-f]+)', h)) +sizes = dict((k, int(v)) for k, v in + re.findall(r'#define FDATA_SIZE_(\w+) (\d+)', h)) +entries = [(p, sizes[z], starts[s]) for p, s, z in + re.findall(r'\{"/([^"]+)", FDATA_START_(\w+), ' + r'FDATA_SIZE_(\w+), \w+, 1\}', c)] +d = open('output/rtlplayground.bin', 'rb').read() +for name, size, start in entries: + assert gzip.decompress(d[start:start+size]) == \ + open('output/html_min/' + name, 'rb').read() +``` + +`tools/httpd_sim` serves the raw files from `html/` and does not exercise +the gzip path; a browser session against a flashed switch (or `curl +--compressed`) is the final check. From caa7874bb36f3cc75423bce7b26cfde60eb8bc3d Mon Sep 17 00:00:00 2001 From: eraiza0816 <10572974+eraiza0816@users.noreply.github.com> Date: Tue, 11 Aug 2026 05:40:01 +0900 Subject: [PATCH 03/12] feat(webui): consolidate the page scripts into a single main.js bundle All 17 page scripts (i18n, shared helpers, navigation and the per-page scripts) are merged into one html/main.js that every page loads as its only script, so the browser fetches it once and gzip can compress across all of it. The page-specific initialisation is guarded by the presence of the matching page elements (it used to be selected implicitly by which script a page loaded); the sidebar fill and the stat popup wiring moved into DOMContentLoaded handlers because the bundle loads in , before the body exists. tools/httpd_sim.h: BUFFER_SIZE 24000 -> 131072 so the simulator can serve the larger bundle (the firmware is unaffected, it serves the gzip'd bundle of ~17 KB). Measured (make MACHINE=KP_9000_6XHML_X2): the JS drops from 23,302 to 16,948 bytes gzip; the whole Web UI from 34,784 to 28,200 bytes (25.3 % of the raw 111,280); the data block ends at 0x46e28. Verified with a headless browser against httpd_sim and against a real PCB-K0402WS-V3: all pages render, the sidebar and the port strip are drawn, no JavaScript errors. --- html/bandwidth.html | 3 - html/bandwidth.js | 138 --- html/config.js | 111 --- html/eee.html | 4 - html/eee.js | 52 -- html/eee_sub.js | 18 - html/i18n.js | 603 ------------ html/index.html | 3 - html/l2.html | 3 - html/l2.js | 194 ---- html/lag.html | 3 - html/lag.js | 81 -- html/login.html | 2 +- html/main.js | 2166 +++++++++++++++++++++++++++++++++++++++++++ html/main_info.js | 22 - html/mirror.html | 4 - html/mirror.js | 64 -- html/mirror_sub.js | 43 - html/navigation.js | 20 - html/ports.html | 3 - html/ports.js | 149 --- html/stat.html | 3 - html/stat.js | 207 ----- html/system.html | 5 +- html/system.js | 208 ----- html/update.html | 3 +- html/vlan.html | 4 - html/vlan.js | 211 ----- html/vlan_sub.js | 36 - tools/httpd_sim.h | 2 +- 30 files changed, 2170 insertions(+), 2195 deletions(-) delete mode 100644 html/bandwidth.js delete mode 100644 html/config.js delete mode 100644 html/eee.js delete mode 100644 html/eee_sub.js delete mode 100644 html/i18n.js delete mode 100644 html/l2.js delete mode 100644 html/lag.js delete mode 100644 html/main_info.js delete mode 100644 html/mirror.js delete mode 100644 html/mirror_sub.js delete mode 100644 html/navigation.js delete mode 100644 html/ports.js delete mode 100644 html/stat.js delete mode 100644 html/system.js delete mode 100644 html/vlan.js delete mode 100644 html/vlan_sub.js diff --git a/html/bandwidth.html b/html/bandwidth.html index 9b6b96f8..57ce9459 100644 --- a/html/bandwidth.html +++ b/html/bandwidth.html @@ -1,7 +1,6 @@ - Ingress and Egress Bandwidth @@ -14,8 +13,6 @@ Ingress Egress Port Limit Bandwidth [kBit/s] Flow Control Limit Bandwidth [kBit/s] Apply - - diff --git a/html/bandwidth.js b/html/bandwidth.js deleted file mode 100644 index 1c020ef1..00000000 --- a/html/bandwidth.js +++ /dev/null @@ -1,138 +0,0 @@ -const iLayout = '" type="number" maxlength="10" size="10" onfocus="inputFocus('; -function createBW() { - var tbl = document.getElementById('bwtable'); - const limit = '' - if (tbl.rows.length <= 2 && numPorts) { - console.log("CREATING TABLE ", tbl.rows.length); - for (let i = 2; i < 2 + numPorts; i++) { - const tr = tbl.insertRow(); - let td = tr.insertCell(); td.appendChild(document.createTextNode(t('common_port') + (i-1))); - td = tr.insertCell(); - td.innerHTML = limit.replaceAll("limit_port", "ilimit_port_" + i).replace("exec()", "iClicked(" + i + ")"); - td = tr.insertCell(); - td.innerHTML = t('bw_unlimited'); - td = tr.insertCell(); - td.innerHTML = limit.replaceAll("limit_port", "fc_port_" + i).replace("exec()", "document.getElementById('bwapply_" + i + "').disabled=false;"); - td = tr.insertCell(); - td.innerHTML = limit.replaceAll("limit_port", "elimit_port_" + i).replace("exec()", "eClicked(" + i + ")"); - td = tr.insertCell(); - td.innerHTML = t('bw_unlimited'); - var button = ''; - td = tr.insertCell(); - td.innerHTML = button; - document.getElementById("bwapply_" + i).disabled = true; - } - } -} - -function iClicked(i) -{ - document.getElementById("bwapply_" + i).disabled=false; - var tbl = document.getElementById('bwtable'); - var tr = tbl.rows[i]; - if (!document.getElementById("ilimit_port_" + i).checked) { - tr.cells[2].innerHTML = t('bw_unlimited'); - document.getElementById("fc_port_" + i).disabled = true; - document.getElementById("fc_port_" + i).checked = true; - } else { - tr.cells[2].innerHTML = ''; - document.getElementById("fc_port_" + i).disabled = false; - document.getElementById("fc_port_" + i).checked = true; - } -} - -function eClicked(i) -{ - document.getElementById("bwapply_" + i).disabled=false; - var tbl = document.getElementById('bwtable'); - var tr = tbl.rows[i]; - if (!document.getElementById("elimit_port_" + i).checked) { - tr.cells[5].innerHTML = t('bw_unlimited'); - } else { - tr.cells[5].innerHTML = ''; - } -} - -function inputFocus(i) -{ - document.getElementById("bwapply_" + i).disabled=false; -} - -async function doCMD(cmd) -{ - console.log("Sending >" + cmd + "<"); - try { - const response = await fetch('/cmd', { - method: 'POST', - body: cmd - }); - console.log('Completed!', response); - } catch(err) { - console.error(`Error: ${err}`); - } -} - -async function applyBandwidth(i) { - var tbl = document.getElementById('bwtable'); - var tr = tbl.rows[i]; - var cmd = "bw in " + (i-1) + " off"; - if (document.getElementById("ilimit_port_" + i).checked) - cmd = 'bw in ' + (i-1) + ' ' + parseInt(document.getElementById("ibw_" + i).value).toString(16).padStart(4, "0");; - doCMD(cmd); - if (document.getElementById("ilimit_port_" + i).checked) { - if (!document.getElementById("fc_port_" + i).checked) - cmd = "bw in " + (i-1) + " drop"; - doCMD(cmd); - } - var cmd = "bw out " + (i-1) + " off"; - if (document.getElementById("elimit_port_" + i).checked) - cmd = 'bw out ' + (i-1) + ' ' + parseInt(document.getElementById("ebw_" + i).value).toString(16).padStart(4, "0");; - doCMD(cmd); -} - -function getBW() { - var xhttp = new XMLHttpRequest(); - xhttp.onreadystatechange = function() { - console.log("IN getBW "); - if (this.readyState == 4 && this.status == 200) { - const s = JSON.parse(xhttp.responseText); - console.log("BW: ", JSON.stringify(s)); - var tbl = document.getElementById('bwtable'); - if (tbl.rows.length > 2 && numPorts) { - for (let i = 2; i < 2 + numPorts; i++) { - p = s[i-2]; - let n = p.portNum; - let tr = tbl.rows[n+1]; - if (!document.getElementById("bwapply_" + (n+1)).disabled) - continue; - console.log("Table Update row: " + i + " portNum is " + n + ", pState is " + pState[i-2] + ", row number is " + (n+1)); - let iBW = parseInt(p.iBW,16) * 16; let eBW = parseInt(p.eBW,16) * 16; - document.getElementById("ilimit_port_" + (n+1)).checked = p.iLimited; - document.getElementById("elimit_port_" + (n+1)).checked = p.eLimited; - if (!p.iLimited) { - tr.cells[2].innerHTML = t('bw_unlimited'); - } else { - tr.cells[2].innerHTML = ''; - } - if (!p.eLimited) { - tr.cells[5].innerHTML = t('bw_unlimited'); - } else { - tr.cells[5].innerHTML = ''; - } - document.getElementById("fc_port_" + (n+1)).checked = p.iFC==1?true:false; - document.getElementById("fc_port_" + (n+1)).disabled = p.iLimited==1?false:true; - } - } - } - }; - xhttp.open("GET", "/bandwidth.json", true); - xhttp.timeout = 1500; sendXHTTP(xhttp); -} - -window.addEventListener("load", function() { - update( () => { - createBW(); - getBW(); - const interval = setInterval(update, 2000); - }); -}); diff --git a/html/config.js b/html/config.js deleted file mode 100644 index 8af3fbaa..00000000 --- a/html/config.js +++ /dev/null @@ -1,111 +0,0 @@ -var configInterval = Number(); -var configuration = []; -const conf_cmds = [ - /^ip\s+(\d{1,3}\.){3}\d{1,3}$/, - /^ip\s+dhcp$/, - /^gw\s+(\d{1,3}\.){3}\d{1,3}$/, - /^netmask\s+(\d{1,3}\.){3}\d{1,3}$/, - /^syslog\s+(on|off)$/, - /^syslog\s+ip\s+(\d{1,3}\.){3}\d{1,3}$/, - /^passwd\s+\S+$/, - /^vlan\s+\d{1,4}\s+d$/, - /^vlan\s+\d{1,4}\s+mgmt$/, - /^vlan\s+\d{1,4}(\s+[a-zA-Z]\w*)?(\s+\d{1,2}[tu]?)+$/, - /^pvid\s+\d{1,2}\s+\d{1,4}$/, - /^ingress(\s+\d{1,2}[tua])+$/, - /^ingress\s+[tua]$/, - /^port\s+\d{1,2}\s+(10m|100m|1g|2g5|5g|10g|auto|on|off)(\s+(half|full))?$/, - /^port\s+\d{1,2}\s+name\s+\S+$/, - /^eee(\s+\d{1,2})?\s+(on|off)$/, - /^mirror(\s+\d{1,2})(\s+\d{1,2}[tr]?)+$/, - /^lag\s+\d(\s+\d{1,2})+$/, - /^laghash\s+\d(\s+\w+)+$/, - /^isolate\s+\d{1,2}(\s+(off|\d{1,2}))+$/, - /^stp\s+(on|off)$/, - /^igmp\s+(on|off)$/, - /^mtu\s+\d{1,2}\s+\d+$/, - /^bw\s+(in|out)\s+\d{1,2}\s+\S+$/, - /^hostname\s+.{1,23}$/, -]; -const conf_overwrite = [ - /^ip\b/, - /^gw\b/, - /^netmask\b/, - /^syslog\s+ip\b/, - /^syslog\b/, - /^passwd\b/, - /^vlan\s+\d{1,4}\s+mgmt$/, - /^vlan\s+\d{1,4}(?!\s+mgmt\b)/, - /^pvid\s+\d{1,2}\b/, - /^ingress\b/, - /^port\s+\d{1,2}(?!\s+name\b)/, - /^port\s+\d{1,2}\s+name\b/, - /^eee\s+\d{1,2}\b/, - /^eee\b/, - /^mirror\b/, - /^lag\s+\d+\b/, - /^laghash\b/, - /^isolate\s+\d{1,2}\b/, - /^stp\b/, - /^igmp\b/, - /^mtu\s+\d{1,2}\b/, - /^bw\s+(in|out)\s+\d{1,2}\b/, - /^hostname\b/, -]; - -function parseConf(s){ - var a = s.split(/\r\n|\n/); - for (var l = 0; l < a.length; l++) { - var line = a[l].trim().replace(/\s+/g, ' '); - if (!line.length) continue; - const deleteMatch = line.match(/^vlan\s+(\d{1,4})\s+d$/); - if (deleteMatch) { - const prefix = "vlan " + deleteMatch[1] + " "; - configuration = configuration.filter(c => !c.startsWith(prefix)); - continue; - } - console.log(l + ' --> ' + line); - var ignore = true; - for (const x of conf_cmds) - if (x.test(line)) { ignore = false; break; } - if (ignore) continue; - for (const x of conf_overwrite) { - if (x.test(line)) { - let m = line.match(x); - let matchStr = m[0]; - configuration = configuration.filter(item => - !(item === matchStr || (item.startsWith(matchStr + " ") && !item.endsWith(" mgmt") && !item.startsWith(matchStr + " name ")))); - break; - } - } - // Only one management VLAN can be active, so drop any previous mgmt entry - if (/^vlan\s+\d{1,4}\s+mgmt$/.test(line)) - configuration = configuration.filter(item => !/^vlan\s+\d{1,4}\s+mgmt$/.test(item)); - configuration.push(line); - } - console.log("Configuration now:"); - for (const x of configuration) { console.log(x); } -} - -async function fetchConfig() { - try { - const response = await fetch('/config'); - console.log("CONFIG: ", response); - const t = await response.text(); - return t; - } catch(err) { - console.error("Error: ", err); - } -} - -async function fetchCmdLog() { - try { - const response = await fetch('/cmd_log'); - console.log("CMD-Log: ", response); - const t = await response.text(); - return t; - } catch(err) { - console.error("Error: ", err); - return ""; - } -} diff --git a/html/eee.html b/html/eee.html index 499c223a..a2c10eca 100644 --- a/html/eee.html +++ b/html/eee.html @@ -1,7 +1,6 @@ - EEE Configuration @@ -18,9 +17,6 @@ - - - diff --git a/html/eee.js b/html/eee.js deleted file mode 100644 index 9225569b..00000000 --- a/html/eee.js +++ /dev/null @@ -1,52 +0,0 @@ -function createEEE() { - var tbl = document.getElementById('eeetable'); - if (tbl.rows.length <= 2 && numPorts) { - console.log("CREATING TABLE ", tbl.rows.length); - for (let i = 2; i < 2 + numPorts; i++) { - console.log("Table row: " + i + "pState: " + pState[i-2]); - const tr = tbl.insertRow(); - let td = tr.insertCell(); td.appendChild(document.createTextNode(t('common_port') + (i-1))); - for (let j = 0; j < 7; j++) { - td = tr.insertCell(); td.appendChild(document.createTextNode(" ")); - } - } - } -} - -function getEEE() { - var xhttp = new XMLHttpRequest(); - xhttp.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) { - const s = JSON.parse(xhttp.responseText); - console.log("EEE: ", JSON.stringify(s)); - var tbl = document.getElementById('eeetable'); - if (tbl.rows.length > 2 && numPorts) { - for (let i = 2; i < 2 + numPorts; i++) { - p = s[i-2]; - let n = p.portNum; - console.log("Table Update row: " + i + " portNum is " + n + ", pState is " + pState[i-2]); - let tr = tbl.rows[n+1]; - if (!p.isSFP) { - let eee = parseInt(p.eee,2); let lp = parseInt(p.eee_lp,2); - tr.cells[1].innerHTML = `${eee&4?t('eee_on'):t('eee_off')}`; tr.cells[2].innerHTML = `${eee&2?t('eee_on'):t('eee_off')}`; tr.cells[3].innerHTML = `${eee&1?t('eee_on'):t('eee_off')}`; - tr.cells[4].innerHTML = `${lp&4?t('eee_on'):t('eee_off')}`; tr.cells[5].innerHTML = `${lp&2?t('eee_on'):t('eee_off')}`; tr.cells[6].innerHTML = `${lp&1?t('eee_on'):t('eee_off')}`; - tr.cells[7].innerHTML = `${p.active}`; - tr.classList.toggle('disabled', pState[i-2] < 0); tr.classList.toggle('isNOK', !p.active); tr.classList.toggle('isOK', p.active); - } - tr.classList.toggle('isSFP', p.isSFP); - } - } - } - }; - xhttp.open("GET", "/eee.json", true); - xhttp.timeout = 1500; sendXHTTP(xhttp); -} - -window.addEventListener("load", function() { - update( () => { - createEEE(); - getEEE(); - const interval = setInterval(update, 2000); - const iCount = setInterval(getEEE, 2000); - }); -}); diff --git a/html/eee_sub.js b/html/eee_sub.js deleted file mode 100644 index 6a72f92d..00000000 --- a/html/eee_sub.js +++ /dev/null @@ -1,18 +0,0 @@ -async function eeeSub(port, enable) { - var cmd = "eee "; - if (enable) - cmd = cmd + "on"; - else - cmd = cmd + "off"; - console.log("eeeSub port " + port, ", value " + enable); - try { - const response = await fetch('/cmd', { - method: 'POST', - body: cmd - }); - console.log('Completed!', response); - } catch(err) { - console.error(`Error: ${err}`); - } -} - diff --git a/html/i18n.js b/html/i18n.js deleted file mode 100644 index 850d9ac1..00000000 --- a/html/i18n.js +++ /dev/null @@ -1,603 +0,0 @@ -var LANG = { - en: { - nav_overview: 'Overview', - nav_port_config: 'Port Configuration', - nav_port_stat: 'Port Statistics', - nav_l2: 'L2 Configuration', - nav_mirror: 'Mirroring', - nav_lag: 'Link Aggregation', - nav_eee: 'EEE', - nav_bandwidth: 'Bandwidth Limits', - nav_system: 'System Settings', - nav_fw_update: 'Firmware Update', - - port_name: 'Name', - port_status: 'Status', - port_not_enabled: 'Not enabled.', - port_link_speed: 'Link speed', - port_vendor: 'Vendor', - port_model: 'Model', - port_serial: 'Serial', - port_temp: 'Temp', - port_vcc: 'Vcc', - port_tx_fault: 'TX-Fault', - port_tx_disabled: 'TX-Disabled', - port_tx_bias: 'TX-Bias', - port_tx_power: 'TX-Power', - port_rx_power: 'RX-Power', - port_rx_los: 'RX-LOS', - - speed_disabled: 'Disabled', - speed_down: 'Down', - - port_title: 'FreeSwitchOS Port Configuration', - port_heading: 'Port Configuration', - port_col_port: 'Port', - port_col_name: 'Name', - port_col_speed: 'Current Link Speed', - port_col_set_speed: 'Set Speed', - port_col_disabled: 'Disabled', - port_col_apply: 'Apply', - port_mtu_heading: 'Configure Maximum Frame Size (MTU) forwarded at Port', - port_auto: 'Auto', - port_2500m: '2500MBit/Full', - port_1000m: '1000MBit/Full', - port_100m_f: '100MBit/Full', - port_100m_h: '100MBit/Half', - port_10m_f: '10MBit/Full', - port_10m_h: '10MBit/Half', - port_apply: 'Apply', - - stat_title: 'FreeSwitchOS Port Statistics', - stat_heading: 'Port Statistics', - stat_detailed: 'Detailed Port Statistics', - stat_close: 'Close', - stat_col_port: 'Port', - stat_col_name: 'Name', - stat_col_link: 'link', - stat_col_tx_good: 'TX Good', - stat_col_tx_bad: 'TX Bad', - stat_col_rx_good: 'RX Good', - stat_col_rx_bad: 'RX Bad', - stat_col_all: 'All Counters', - stat_counter: 'Counter', - stat_value: 'Value', - stat_show: 'Show', - - vlan_title: 'FreeSwitchOS VLAN Configuration', - vlan_heading: 'VLAN Configuration', - vlan_select: 'VLAN Select:', - vlan_choose: '— VLAN Choose —', - vlan_id: 'VLAN ID:', - vlan_get_config: 'Get Configuration', - vlan_name: 'VLAN Name:', - vlan_tagged: 'Tagged Ports', - vlan_untagged: 'Untagged Ports', - vlan_select_all: 'Select all', - vlan_pvid: 'Use as default VLAN for incoming traffic (PVID)', - vlan_update: 'Update / Create', - vlan_configured: 'Configured VLANs', - vlan_col_name: 'Name', - vlan_col_member: 'Member Ports', - vlan_col_tagged: 'Tagged Ports', - vlan_col_untagged: 'Untagged Ports', - vlan_col_pvid: 'PVID Ports', - vlan_col_delete: 'Delete', - vlan_set_id_first: 'Set VLAN ID first', - vlan_delete_confirm: 'Delete VLAN ', - - lag_title: 'Link Aggregation Configuration', - lag_heading: 'Link Aggregation Groups Configuration', - lag_update: 'Update / Create', - - mirror_title: 'Mirror Configuration', - mirror_heading: 'Mirror Configuration', - mirror_enabled: 'Enabled:', - mirror_port: 'Mirroring Port:', - mirror_tx: 'Mirrored Ports (TX)', - mirror_rx: 'Mirrored Ports (RX)', - mirror_update: 'Update / Create', - mirror_disable: 'Disable Mirroring', - mirror_set_port_first: 'Set Mirroring Port first', - mirror_select_ports: 'Select Mirrored Ports', - - eee_title: 'EEE Configuration', - eee_heading: 'EEE Status', - eee_advertising: 'Advertising', - eee_partner: 'Link-Partner advertises', - eee_port: 'Port', - eee_active: 'Active?', - eee_enable: 'Enable EEE', - eee_disable: 'Disable EEE', - eee_on: 'ON', - eee_off: 'OFF', - - l2_title: 'FreeSwitchOS L2 Configuration', - l2_heading: 'L2 Configuration', - 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', - - bw_title: 'Ingress and Egress Bandwidth', - bw_heading: 'Ingress and Egress Bandwidth', - bw_ingress: 'Ingress', - bw_egress: 'Egress', - bw_col_port: 'Port', - bw_col_limit: 'Limit', - bw_col_bandwidth: 'Bandwidth [kBit/s]', - bw_col_flow: 'Flow Control', - bw_col_apply: 'Apply', - bw_unlimited: 'UNLIMITED', - - sys_title: 'System Settings', - sys_tab_system: 'System', - sys_tab_advanced: 'Advanced', - sys_tab_console: 'Console', - sys_heading: 'System Settings', - sys_ip: 'IP address:', - sys_model: 'Model:', - sys_hostname: 'Hostname:', - sys_apply: 'Apply', - sys_netmask: 'Netmask:', - sys_gateway: 'Gateway:', - sys_language: 'Language:', - sys_mgmt_vlan: 'Management VLAN:', - sys_mgmt_untagged: 'untagged', - sys_mgmt_confirm: 'Move switch management to VLAN ', - sys_mgmt_warn: 'The switch will start tagging its own traffic with that VLAN. If the port you are connected through does not carry it, this page becomes unreachable and the setting can only be undone over the console. Continue?', - sys_ip_note: 'When updating the above settings, remember to point your browser to the new IP afterwards:', - sys_update: 'Update Settings', - sys_save_label: 'Save all current settings to Flash:', - sys_save: 'Save Settings to Flash', - sys_advanced: 'Advanced Settings', - sys_startup_config: 'Startup configuration:', - sys_startup_warn: 'Be careful when saving the directly edited startup configuration, you can lock yourself out:', - sys_clear_config: 'Clear Startup Config', - sys_save_startup: 'Save Startup Settings to Flash', - sys_reset: 'Reset Switch', - sys_console: 'Console Command', - sys_enter_cmd: 'Enter command:', - sys_send_cmd: 'Send Command', - sys_console_warn: 'Be careful when entering console commands, you can lock yourself out!', - sys_invalid_ip: 'Invalid ip:', - sys_reset_confirm: 'Are you sure you want to reset the switch?', - sys_resetting: 'Switch is resetting. Please wait and refresh the page.', - - login_title: 'RTL Switch Login', - login_heading: 'RTL Switch Login', - login_wrong: 'Wrong password!', - login_password: 'Password', - login_login: 'Login', - - index_title: 'FreeSwitchOS Main Page', - index_heading: 'Switch Configuration', - index_settings: 'Settings', - - update_title: 'Firmware update', - update_heading: 'Firmware Update', - update_instruction: 'Choose a firmware update file to upload:', - update_upload: 'Upload File', - - common_port: 'Port ', - common_pkts: ' pkts', - }, - - ja: { - nav_overview: '概要', - nav_port_config: 'ポート設定', - nav_port_stat: 'ポート統計', - nav_l2: 'L2 設定', - nav_mirror: 'ミラーリング', - nav_lag: 'リンクアグリゲーション', - nav_eee: 'EEE', - nav_bandwidth: '帯域制限', - nav_system: 'システム設定', - nav_fw_update: 'ファームウェア更新', - - port_name: '名前', - port_status: '状態', - port_not_enabled: '無効', - port_link_speed: 'リンク速度', - port_vendor: 'ベンダー', - port_model: 'モデル', - port_serial: 'シリアル', - port_temp: '温度', - port_vcc: '電圧', - port_tx_fault: 'TX 障害', - port_tx_disabled: 'TX 無効', - port_tx_bias: 'TX バイアス', - port_tx_power: 'TX 電力', - port_rx_power: 'RX 電力', - port_rx_los: 'RX 信号ロス', - - speed_disabled: '無効', - speed_down: 'リンクダウン', - - port_title: 'FreeSwitchOS ポート設定', - port_heading: 'ポート設定', - port_col_port: 'ポート', - port_col_name: '名前', - port_col_speed: '現在のリンク速度', - port_col_set_speed: '速度設定', - port_col_disabled: '無効', - port_col_apply: '適用', - port_mtu_heading: 'ポートの最大フレームサイズ (MTU) 設定', - port_auto: '自動', - port_2500m: '2500Mbps/全二重', - port_1000m: '1000Mbps/全二重', - port_100m_f: '100Mbps/全二重', - port_100m_h: '100Mbps/半二重', - port_10m_f: '10Mbps/全二重', - port_10m_h: '10Mbps/半二重', - port_apply: '適用', - - stat_title: 'FreeSwitchOS ポート統計', - stat_heading: 'ポート統計', - stat_detailed: '詳細ポート統計', - stat_close: '閉じる', - stat_col_port: 'ポート', - stat_col_name: '名前', - stat_col_link: 'リンク', - stat_col_tx_good: 'TX 正常', - stat_col_tx_bad: 'TX 異常', - stat_col_rx_good: 'RX 正常', - stat_col_rx_bad: 'RX 異常', - stat_col_all: '全カウンタ', - stat_counter: 'カウンタ', - stat_value: '値', - stat_show: '表示', - - vlan_title: 'FreeSwitchOS VLAN 設定', - vlan_heading: 'VLAN 設定', - vlan_select: 'VLAN 選択:', - vlan_choose: '— VLAN 選択 —', - vlan_id: 'VLAN ID:', - vlan_get_config: '設定取得', - vlan_name: 'VLAN 名:', - vlan_tagged: 'タグ付きポート', - vlan_untagged: 'タグ無しポート', - vlan_select_all: 'すべて選択', - vlan_pvid: '受信トラフィックのデフォルト VLAN (PVID)', - vlan_update: '更新 / 作成', - vlan_configured: '設定済み VLAN', - vlan_col_name: '名前', - vlan_col_member: 'メンバーポート', - vlan_col_tagged: 'タグ付きポート', - vlan_col_untagged: 'タグ無しポート', - vlan_col_pvid: 'PVID ポート', - vlan_col_delete: '削除', - vlan_set_id_first: 'VLAN ID を先に設定してください', - vlan_delete_confirm: 'VLAN 削除 ', - - lag_title: 'リンクアグリゲーション設定', - lag_heading: 'リンクアグリゲーショングループ設定', - lag_update: '更新 / 作成', - - mirror_title: 'ミラーリング設定', - mirror_heading: 'ミラーリング設定', - mirror_enabled: '有効:', - mirror_port: 'ミラーポート:', - mirror_tx: 'ミラー元ポート (TX)', - mirror_rx: 'ミラー元ポート (RX)', - mirror_update: '更新 / 作成', - mirror_disable: 'ミラーリング無効化', - mirror_set_port_first: 'ミラーポートを先に設定してください', - mirror_select_ports: 'ミラー元ポートを選択してください', - - eee_title: 'EEE 設定', - eee_heading: 'EEE 状態', - eee_advertising: 'EEE アドバタイジング', - eee_partner: 'リンクパートナー広告', - eee_port: 'ポート', - eee_active: '有効?', - eee_enable: 'EEE 有効化', - eee_disable: 'EEE 無効化', - eee_on: 'オン', - eee_off: 'オフ', - - l2_title: 'FreeSwitchOS L2 設定', - l2_heading: 'L2 設定', - l2_col_port: 'ポート', - l2_col_type: 'タイプ', - l2_col_remove: 'エントリ削除', - l2_shown: 'Shown:', - l2_delete: '削除', - l2_static: '静的', - l2_learned: '学習', - - bw_title: '入力/出力帯域制限', - bw_heading: '入力/出力帯域制限', - bw_ingress: '入力', - bw_egress: '出力', - bw_col_port: 'ポート', - bw_col_limit: '制限', - bw_col_bandwidth: '帯域 [kbps]', - bw_col_flow: 'フロー制御', - bw_col_apply: '適用', - bw_unlimited: '制限無し', - - sys_title: 'システム設定', - sys_tab_system: 'システム', - sys_tab_advanced: '詳細設定', - sys_tab_console: 'コンソール', - sys_heading: 'システム設定', - sys_ip: 'IP アドレス:', - sys_model: 'モデル:', - sys_hostname: 'ホスト名:', - sys_apply: '適用', - sys_netmask: 'ネットマスク:', - sys_gateway: 'ゲートウェイ:', - sys_language: '言語:', - sys_mgmt_vlan: 'Management VLAN:', - sys_mgmt_untagged: 'untagged', - sys_mgmt_confirm: 'Move switch management to VLAN ', - sys_mgmt_warn: 'The switch will start tagging its own traffic with that VLAN. If the port you are connected through does not carry it, this page becomes unreachable and the setting can only be undone over the console. Continue?', - sys_ip_note: '上記設定を変更した場合は、ブラウザで新しい IP にアクセスしてください:', - sys_update: '設定更新', - sys_save_label: '現在の設定をフラッシュに保存:', - sys_save: '設定をフラッシュに保存', - sys_advanced: '詳細設定', - sys_startup_config: '起動設定:', - sys_startup_warn: '起動設定を直接編集する際は注意してください。ロックアウトされる可能性があります:', - sys_clear_config: '起動設定クリア', - sys_save_startup: '起動設定をフラッシュに保存', - sys_reset: 'スイッチ再起動', - sys_console: 'コンソールコマンド', - sys_enter_cmd: 'コマンド入力:', - sys_send_cmd: 'コマンド送信', - sys_console_warn: 'コンソールコマンドは注意して入力してください。ロックアウトされる可能性があります!', - sys_invalid_ip: '無効な IP: ', - sys_reset_confirm: 'スイッチを再起動してもよろしいですか?', - sys_resetting: 'スイッチを再起動中です。しばらく待ってからページをリロードしてください。', - - login_title: 'RTL スイッチ ログイン', - login_heading: 'RTL スイッチ ログイン', - login_wrong: 'パスワードが違います!', - login_password: 'パスワード', - login_login: 'ログイン', - - index_title: 'FreeSwitchOS メインページ', - index_heading: 'スイッチ設定', - index_settings: '設定', - - update_title: 'ファームウェア更新', - update_heading: 'ファームウェア更新', - update_instruction: 'アップロードするファームウェアファイルを選択:', - update_upload: 'ファイルをアップロード', - - common_port: 'ポート ', - common_pkts: ' pkts', - }, - - zh: { - nav_overview: '概览', - nav_port_config: '端口配置', - nav_port_stat: '端口统计', - nav_l2: 'L2 配置', - nav_mirror: '端口镜像', - nav_lag: '链路聚合', - nav_eee: 'EEE', - nav_bandwidth: '带宽限制', - nav_system: '系统设置', - nav_fw_update: '固件升级', - - port_name: '名称', - port_status: '状态', - port_not_enabled: '未启用。', - port_link_speed: '链路速率', - port_vendor: '厂商', - port_model: '型号', - port_serial: '序列号', - port_temp: '温度', - port_vcc: '供电电压', - port_tx_fault: 'TX 故障', - port_tx_disabled: 'TX 禁用', - port_tx_bias: 'TX 偏置电流', - port_tx_power: 'TX 光功率', - port_rx_power: 'RX 光功率', - port_rx_los: 'RX 信号丢失', - - speed_disabled: '禁用', - speed_down: '未连接', - - port_title: 'FreeSwitchOS 端口配置', - port_heading: '端口配置', - port_col_port: '端口', - port_col_name: '名称', - port_col_speed: '当前链路速率', - port_col_set_speed: '设置速率', - port_col_disabled: '禁用', - port_col_apply: '应用', - port_mtu_heading: '配置端口转发的最大帧大小 (MTU)', - port_auto: '自动', - port_2500m: '2500Mbps/全双工', - port_1000m: '1000Mbps/全双工', - port_100m_f: '100Mbps/全双工', - port_100m_h: '100Mbps/半双工', - port_10m_f: '10Mbps/全双工', - port_10m_h: '10Mbps/半双工', - port_apply: '应用', - - stat_title: 'FreeSwitchOS 端口统计', - stat_heading: '端口统计', - stat_detailed: '详细端口统计', - stat_close: '关闭', - stat_col_port: '端口', - stat_col_name: '名称', - stat_col_link: '链路', - stat_col_tx_good: 'TX 正常包', - stat_col_tx_bad: 'TX 错误包', - stat_col_rx_good: 'RX 正常包', - stat_col_rx_bad: 'RX 错误包', - stat_col_all: '所有计数器', - stat_counter: '计数器', - stat_value: '值', - stat_show: '查看', - - vlan_title: 'FreeSwitchOS VLAN 配置', - vlan_heading: 'VLAN 配置', - vlan_select: 'VLAN 选择:', - vlan_choose: '-- 请选择 VLAN --', - vlan_id: 'VLAN ID:', - vlan_get_config: '获取配置', - vlan_name: 'VLAN 名称:', - vlan_tagged: 'Tagged 端口', - vlan_untagged: 'Untagged 端口', - vlan_select_all: '全选', - vlan_pvid: '作为入方向流量的默认 VLAN (PVID)', - vlan_update: '更新 / 创建', - vlan_configured: '已配置 VLAN', - vlan_col_name: '名称', - vlan_col_member: '成员端口', - vlan_col_tagged: 'Tagged 端口', - vlan_col_untagged: 'Untagged 端口', - vlan_col_pvid: 'PVID 端口', - vlan_col_delete: '删除', - vlan_set_id_first: '请先设置 VLAN ID', - vlan_delete_confirm: '删除 VLAN ', - - lag_title: '链路聚合配置', - lag_heading: '链路聚合组配置', - lag_update: '更新 / 创建', - - mirror_title: '端口镜像配置', - mirror_heading: '端口镜像配置', - mirror_enabled: '启用:', - mirror_port: '镜像目的端口:', - mirror_tx: '被镜像端口 (TX)', - mirror_rx: '被镜像端口 (RX)', - mirror_update: '更新 / 创建', - mirror_disable: '禁用端口镜像', - mirror_set_port_first: '请先设置镜像目的端口', - mirror_select_ports: '请选择被镜像端口', - - eee_title: 'EEE 配置', - eee_heading: 'EEE 状态', - eee_advertising: '本端通告', - eee_partner: '链路伙伴通告', - eee_port: '端口', - eee_active: '已生效?', - eee_enable: '启用 EEE', - eee_disable: '禁用 EEE', - eee_on: '开', - eee_off: '关', - - l2_title: 'FreeSwitchOS L2 配置', - l2_heading: 'L2 配置', - l2_col_port: '端口', - l2_col_type: '类型', - l2_col_remove: '删除条目', - l2_shown: 'Shown:', - l2_delete: '删除', - l2_static: '静态', - l2_learned: '动态学习', - - bw_title: '入方向/出方向带宽限制', - bw_heading: '入方向/出方向带宽限制', - bw_ingress: '入方向', - bw_egress: '出方向', - bw_col_port: '端口', - bw_col_limit: '限速', - bw_col_bandwidth: '带宽 [kbit/s]', - bw_col_flow: '流量控制', - bw_col_apply: '应用', - bw_unlimited: '不限速', - - sys_title: '系统设置', - sys_tab_system: '系统', - sys_tab_advanced: '高级', - sys_tab_console: '控制台', - sys_heading: '系统设置', - sys_ip: 'IP 地址:', - sys_model: '型号:', - sys_hostname: '主机名:', - sys_apply: '应用', - sys_netmask: '子网掩码:', - sys_gateway: '网关:', - sys_language: '语言:', - sys_mgmt_vlan: 'Management VLAN:', - sys_mgmt_untagged: 'untagged', - sys_mgmt_confirm: 'Move switch management to VLAN ', - sys_mgmt_warn: 'The switch will start tagging its own traffic with that VLAN. If the port you are connected through does not carry it, this page becomes unreachable and the setting can only be undone over the console. Continue?', - sys_ip_note: '更新上述设置后,请使用新的 IP 地址重新访问管理界面:', - sys_update: '更新设置', - sys_save_label: '将当前全部设置保存到 Flash:', - sys_save: '保存设置到 Flash', - sys_advanced: '高级设置', - sys_startup_config: '启动配置:', - sys_startup_warn: '直接编辑启动配置时请谨慎,错误配置可能导致无法访问设备:', - sys_clear_config: '清除启动配置', - sys_save_startup: '保存启动配置到 Flash', - sys_reset: '重启交换机', - sys_console: '控制台命令', - sys_enter_cmd: '输入命令:', - sys_send_cmd: '发送命令', - sys_console_warn: '输入控制台命令时请谨慎,错误命令可能导致无法访问设备!', - sys_invalid_ip: '无效 IP: ', - sys_reset_confirm: '确定要重启交换机吗?', - sys_resetting: '交换机正在重启。请稍候并刷新页面。', - - login_title: 'RTL 交换机登录', - login_heading: 'RTL 交换机登录', - login_wrong: '密码错误!', - login_password: '密码', - login_login: '登录', - - index_title: 'FreeSwitchOS 主页', - index_heading: '交换机配置', - index_settings: '设置', - - update_title: '固件升级', - update_heading: '固件升级', - update_instruction: '请选择要上传的固件文件:', - update_upload: '上传文件', - - common_port: '端口 ', - common_pkts: ' 个包', - } -}; - -var rtlLang = (function() { - var saved = localStorage.getItem('rtl_lang'); - if (saved && LANG[saved]) return saved; - var browser = (navigator.language || navigator.userLanguage || 'en').substring(0, 2); - return LANG[browser] ? browser : 'en'; -})(); - -function t(key) { - return LANG[rtlLang][key] || LANG['en'][key] || key; -} - -function setLang(lang) { - if (LANG[lang]) { - localStorage.setItem('rtl_lang', lang); - rtlLang = lang; - document.querySelectorAll('[data-i18n]').forEach(function(el) { - applyTranslation(el); - }); - } -} - -function applyTranslation(el) { - var key = el.getAttribute('data-i18n'); - if (!key) return; - if (el.tagName === 'INPUT' && (el.type === 'submit' || el.type === 'button')) { - el.value = t(key); - } else if (el.tagName === 'OPTION') { - el.textContent = t(key); - } else if (el.tagName === 'TITLE') { - el.textContent = t(key); - } else { - el.innerHTML = t(key); - } -} - -document.addEventListener('DOMContentLoaded', function() { - document.querySelectorAll('[data-i18n]').forEach(function(el) { - applyTranslation(el); - }); -}); diff --git a/html/index.html b/html/index.html index 4975df2b..e0de34ab 100644 --- a/html/index.html +++ b/html/index.html @@ -1,8 +1,6 @@ - - diff --git a/html/l2.html b/html/l2.html index 61343725..7ba2e1d1 100644 --- a/html/l2.html +++ b/html/l2.html @@ -1,7 +1,6 @@ - FreeSwitchOS L2 Configuration @@ -22,9 +21,7 @@ Type
Remove Entry - - diff --git a/html/l2.js b/html/l2.js deleted file mode 100644 index d3a058bc..00000000 --- a/html/l2.js +++ /dev/null @@ -1,194 +0,0 @@ -var l2GetInterval; -var l2Entries = []; -var l2CurrentEntry = 0; - -function fillStats() { - var tbl = document.getElementById('statstable'); - if (!numPorts) - return; - if (tbl.rows.length > 1) { - for (let i = 0; i < numPorts; i++) { - console.log("Table Update row: " + i + " state " + pState[i] + " is " + linkS[pState[i] +1]); - tbl.rows[i+1].cells[1].innerHTML = linkText(pState[i]+1); - tbl.rows[i+1].cells[2].innerHTML = `${txG[i]}` + t('common_pkts'); - tbl.rows[i+1].cells[3].innerHTML = `${txB[i]}` + t('common_pkts'); - tbl.rows[i+1].cells[4].innerHTML = `${rxG[i]}` + t('common_pkts'); - tbl.rows[i+1].cells[5].innerHTML = `${rxB[i]}` + t('common_pkts'); - } - } else { - for (let i = 0; i < numPorts; i++) { - console.log("Table row: " + i); - const tr = tbl.insertRow(); - let td = tr.insertCell(); td.appendChild(document.createTextNode(t('common_port') + (i+1))); - td = tr.insertCell(); td.appendChild(document.createTextNode(linkText(pState[i]+1))); - td = tr.insertCell(); td.appendChild(document.createTextNode(`${txG[i]}` + t('common_pkts'))); - td = tr.insertCell();td.appendChild(document.createTextNode(`${txB[i]}` + t('common_pkts'))); - td = tr.insertCell();td.appendChild(document.createTextNode(`${rxG[i]}` + t('common_pkts'))); - td = tr.insertCell();td.appendChild(document.createTextNode(`${rxB[i]}` + t('common_pkts'))); - } - } -} - -function l2CMP(a, b) -{ - if (a.port < b.port) - return -1; - if (a.port > b.port) - return 1; - if (a.mac < b.mac) - return -1; - if (a.mac > b.mac) - return 1; - if (a.vlan < b.vlan) - return -1; - if (a.vlan > b.vlan) - return 1; - return 0; -} - -function uniq(a) { - return a.filter(function(item, pos, ary) { - return !pos || item.idx != ary[pos - 1].idx; - }); -} - -function delL2(idx) { - var xhttp = new XMLHttpRequest(); - xhttp.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) { - var s = JSON.parse(xhttp.responseText); - console.log("Entry deletion result: ", s.result); - } - }; - xhttp.open("GET", "/l2_del.json?idx=" + idx, true); - xhttp.timeout = 1500; xhttp.send(); -} - -var l2All = []; -const l2Cols = ['port', 'mac', 'vlan', 'type']; -var l2SortCol = 'port'; -var l2SortDir = 1; - -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 = {}; - l2Cols.forEach(function(c) { - var el = document.getElementById('l2f_' + c); - f[c] = el ? el.value.trim().toLowerCase() : ''; - }); - var rows = l2All.filter(function(e) { - return l2Cols.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; - }); - l2Cols.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; -} - -function fillL2(s) -{ - var tbl = document.getElementById('l2table'); - if (!s.length) - return; - s.sort(l2CMP); - s = uniq(s); - 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]; - console.log(i, e); - if (tbl.rows[i+1]) { - 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(); - let td = tr.insertCell(); td.innerHTML = `${e.port}`; - td = tr.insertCell(); td.innerHTML = `${e.mac}`; - td = tr.insertCell(); td.innerHTML = `${e.vlan}`; - td = tr.insertCell(); td.innerHTML = `${e.type}`; - td = tr.insertCell(); td.innerHTML = ''; - } - } - for (let i = tbl.rows.length - 1; i > s.length; i--) - tbl.deleteRow(i); -} - -function getL2() { - var xhttp = new XMLHttpRequest(); - xhttp.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) { - var s = JSON.parse(xhttp.responseText); - var s = s.map(function(e) { - 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 ? 'CPU' : logToPhysPort[e.port]; - return e; - }); - l2Entries.push(...s); - if (l2Entries >= 4096) { - l2Entries = []; - l2CurrentEntry = 0; - clearInterval(l2GetInterval); - return; - } - var w = 0; - for (var i = l2Entries.length-1; i > 0; i--) { - if (l2Entries[0].idx == l2Entries[i].idx) { - w = 1; - break; - } - } - if (w) { - l2CurrentEntry = 0; - fillL2(l2Entries); - } else { - l2CurrentEntry = s[s.length-1].idx + 1; - } - } - }; - xhttp.open("GET", "/l2.json?idx=" + l2CurrentEntry, true); - xhttp.timeout = 1500; sendXHTTP(xhttp); -} - -window.addEventListener("load", function() { - update( () => { - getL2(); - const interval = setInterval(update, 2000); - l2GetInterval = setInterval(getL2, 1000); - });; -}); - diff --git a/html/lag.html b/html/lag.html index 7a5e5f76..8e3e9cbc 100644 --- a/html/lag.html +++ b/html/lag.html @@ -1,7 +1,6 @@ - Link Aggregation Configuration @@ -21,8 +20,6 @@

LAG 4

- - diff --git a/html/lag.js b/html/lag.js deleted file mode 100644 index 2c56d1a5..00000000 --- a/html/lag.js +++ /dev/null @@ -1,81 +0,0 @@ -var lagInterval = Number(); - -function lagForm() { - if (!numPorts) - return; - for (let j=0; j < 4; j++) { - var lag = "mLAG" + j - console.log("Adding LAG " + lag) - var m = document.getElementById(lag); - for (let i = 1; i <= numPorts; i++) { - const d = document.createElement("div"); - d.classList.add("cbgroup"); - const l = document.createElement("label"); - l.innerHTML = "" + i; - l.classList.add("cbgroup"); - const inp = document.createElement("input"); - inp.type = "checkbox"; inp.setAttribute("class","psel"); - inp.id = "p_" + lag + "_" + i; - const o = document.createElement("img"); - if (pIsSFP[i - 1]) { - o.src = "sfp.svg"; o.width ="60"; o.height ="60"; - } else { - o.src = "port.svg"; o.width = "40"; o.height = "40"; - } - l.appendChild(inp); l.appendChild(o); - d.appendChild(l) - m.appendChild(d); - } - } - fetchLag(); -} - -function setL(p, c){ - console.log("LAG setting: ", p, " to ", c); - document.getElementById(p).checked=c; -} - -function fetchLag() { - var xhttp = new XMLHttpRequest(); - xhttp.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) { - const s = JSON.parse(xhttp.responseText); - console.log("LAG: ", JSON.stringify(s)); - for (let l = 0; l < 4; l++) { - let members = parseInt(s[l].members, 2); - let hash = parseInt(s[l].hash, 16); - for (let i = 1; i <= numPorts; i++) { - let p = i - 1; - if (numPorts < 9) - p = physToLogPort[p]; - setL("p_mLAG"+l+"_"+i, members & (1< { - lagForm(); - const interval = setInterval(update, 2000); - }); -}); diff --git a/html/login.html b/html/login.html index 119a9339..fda7f070 100644 --- a/html/login.html +++ b/html/login.html @@ -2,7 +2,7 @@ RTL Switch Login - + - Mirror Configuration @@ -19,9 +18,6 @@

- - - diff --git a/html/mirror.js b/html/mirror.js deleted file mode 100644 index d09a7b09..00000000 --- a/html/mirror.js +++ /dev/null @@ -1,64 +0,0 @@ -var mirrorInterval = Number(); -const mirrors = ["mPortsTX", "mPortsRX"]; - -function mirrorForm() { - if (!numPorts) - return; - for (let j=0; j < mirrors.length; j++) { - console.log("Adding Mirror " + j) - var m = document.getElementById(mirrors[j]); - for (let i = 1; i <= numPorts; i++) { - const d = document.createElement("div"); - d.classList.add("cbgroup"); - const l = document.createElement("label"); - l.innerHTML = "" + i; - l.classList.add("cbgroup"); - const inp = document.createElement("input"); - inp.type = "checkbox"; inp.setAttribute("class","psel"); - inp.id = mirrors[j] + i; - const o = document.createElement("img"); - if (pIsSFP[i - 1]) { - o.src = "sfp.svg"; o.width ="60"; o.height ="60"; - } else { - o.src = "port.svg"; o.width = "40"; o.height = "40"; - } - l.appendChild(inp); l.appendChild(o); - d.appendChild(l) - m.appendChild(d); - } - } - fetchMirror(); -} - -function setM(p, c){ - document.getElementById(p).checked=c; -} - -function fetchMirror() { - var xhttp = new XMLHttpRequest(); - xhttp.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) { - const s = JSON.parse(xhttp.responseText); - console.log("MIRROR: ", JSON.stringify(s)); - document.getElementById('me').checked = s.enabled; - document.getElementById('mp').value = s.mPort; - let m_tx = parseInt(s.mirror_tx, 2); - let m_rx = parseInt(s.mirror_rx, 2); - for (let i = 1; i <= numPorts; i++) { - let p = i - 1; - if (numPorts < 9) - p = physToLogPort[p]; - setM("mPortsTX"+i, m_tx&(1< { - mirrorForm(); - const interval = setInterval(update, 2000); - }); -}); diff --git a/html/mirror_sub.js b/html/mirror_sub.js deleted file mode 100644 index da5b6cd1..00000000 --- a/html/mirror_sub.js +++ /dev/null @@ -1,43 +0,0 @@ -async function mirrorSub() { - var cmd = "mirror "; - var mp=document.getElementById('mp').value - if (!mp) { - alert(t('mirror_set_port_first')); - return; - } - document.getElementById(mirrors[0]+mp).checked=false;document.getElementById(mirrors[1]+mp).checked=false; - cmd = cmd + mp; - for (let i = 1; i <= numPorts; i++) { - if (document.getElementById(mirrors[0] + i).checked && document.getElementById(mirrors[1] + i).checked) - cmd = cmd + ` ${i}`; - else if (document.getElementById(mirrors[0] + i).checked) - cmd = cmd + ` ${i}t`; - else if (document.getElementById(mirrors[1] + i).checked) - cmd = cmd + ` ${i}r`; - } - if (cmd.length < 10) { - alert(t('mirror_select_ports')); - return; - } - try { - const response = await fetch('/cmd', { - method: 'POST', - body: cmd - }); - console.log('Completed!', response); - } catch(err) { - console.error(`Error: ${err}`); - } -} -async function mirrorDel() { - var cmd = "mirror off"; -try { - const response = await fetch('/cmd', { - method: 'POST', - body: cmd - }); - location.reload(); - } catch(err) { - console.error(`Error: ${err}`); - } -} diff --git a/html/navigation.js b/html/navigation.js deleted file mode 100644 index b42f8fd6..00000000 --- a/html/navigation.js +++ /dev/null @@ -1,20 +0,0 @@ -document.getElementById('sidebar').innerHTML = - ""; - -document.addEventListener('DOMContentLoaded', function() { - var links = document.querySelectorAll('#sidebar a[data-i18n]'); - links.forEach(function(el) { - var key = el.getAttribute('data-i18n'); - if (key) el.textContent = t(key); - }); -}); diff --git a/html/ports.html b/html/ports.html index 8d9409f2..233be139 100644 --- a/html/ports.html +++ b/html/ports.html @@ -1,7 +1,6 @@ - FreeSwitchOS Port Configuration @@ -17,9 +16,7 @@

Configure Maximum Frame Size (MTU) forwarded at Port

- - diff --git a/html/ports.js b/html/ports.js deleted file mode 100644 index 9079ca1d..00000000 --- a/html/ports.js +++ /dev/null @@ -1,149 +0,0 @@ -var mtus = new Int16Array(10); -var clicked = new Int8Array(10); -function createPortTable() { - var tbl = document.getElementById('speedtable'); - if (tbl.rows.length <= 2 && numPorts) { - const sSelect = ''; - const dSwitch = '' - for (let i = 1; i <= numPorts; i++) { - if (pIsSFP[i-1]) - continue; - console.log("Table row: " + i + "pState: " + pState[i-2]); - const tr = tbl.insertRow(); - let td = tr.insertCell(); td.appendChild(document.createTextNode(t('common_port') + i)); - let portName = portNames[physToLogPort[i-1]] || ''; - td = tr.insertCell(); td.appendChild(document.createTextNode(portName)); - td = tr.insertCell(); td.innerHTML = linkText(pState[i] + 1); - td = tr.insertCell(); td.innerHTML = sSelect.replaceAll("speed_sel", "speed_sel_" + i); - td = tr.insertCell(); td.innerHTML = dSwitch.replaceAll("disable_port", "disable_port_" + i) - .replace("portOnOff()", "portOnOff(" + i + ")"); - var button = ''; - td = tr.insertCell(); - td.innerHTML = button; - } - } - tbl = document.getElementById('mtutable'); - if (tbl.rows.length <= 2 && numPorts) { - const mSelect = ''; - var tr = tbl.insertRow(); - for (let i = 1; i <= numPorts; i++) { - let td = tr.insertCell(); - if (pIsSFP[i-1]) - td.innerHTML = '' - else - td.innerHTML = '' - } - tr = tbl.insertRow(); - for (let i = 1; i <= numPorts; i++) { - let td = tr.insertCell(); - td.innerHTML = mSelect.replaceAll("mtu_sel", "mtu_sel_" + i); - } - tr = tbl.insertRow(); - for (let i = 1; i <= numPorts; i++) { - let td = tr.insertCell(); - td.innerHTML = ''; - } - } -} - -function updatePortTable() { - console.log("updatePortTable called"); - var tbl = document.getElementById('speedtable'); - if (tbl.rows.length <= 2 || !numPorts) - return; - for (let i = 1; i <= numPorts ; i++) { - if (pIsSFP[i-1]) - continue; - tbl.rows[i].cells[2].innerHTML = linkText(pState[i-1]+1); - if (!clicked[i] && pState[i - 1] < 0) { - document.getElementById('speed_sel_' + i).disabled = true; - document.getElementById('disable_port_' + i).checked = true; - } - } -} - -async function applySpeed(port) { - var speed = document.getElementById('speed_sel_' + port).value; - var disabled = document.getElementById('disable_port_' + port).checked; - var cmd = "port " + port + " "; - if (!disabled) - cmd = cmd + speed; - else - cmd = cmd + "off"; - console.log("CMD: " + cmd); - try { - const response = await fetch('/cmd', { - method: 'POST', - body: cmd - }); - console.log('Completed!', response); - } catch(err) { - console.error(`Error: ${err}`); - } -} - -async function portOnOff(p) { - var disabled = document.getElementById('disable_port_' + p).checked; - document.getElementById('speed_sel_' + p).disabled = disabled; - clicked[p] = 1; -} - -async function applyMTU(port) { - var mtu = document.getElementById('mtu_sel_' + port).value; - var cmd = "mtu " + port + " " + mtu; - try { - const response = await fetch('/cmd', { - method: 'POST', - body: cmd - }); - console.log('MTU Completed!', response); - getMTUs(); - } catch(err) { - console.error(`Error: ${err}`); - } -} - -function getMTUs() { - var xhttp = new XMLHttpRequest(); - xhttp.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) { - const s = JSON.parse(xhttp.responseText); - console.log("MTUS: ", JSON.stringify(s)); - for (let i = 0; i < s.length; i++) { - p = s[i]; - let n = p.portNum; - mtus[n] = parseInt(p.mtu, 16); - var mtu = document.getElementById('mtu_sel_' + n); - if (!mtu) - continue; - mtu.value = mtus[n]; - } - } - }; - xhttp.open("GET", "/mtu.json", true); - xhttp.timeout = 1500; sendXHTTP(xhttp); -} - -window.addEventListener("load", function() { - update( () => { - createPortTable(); - updatePortTable(); - getMTUs() - const interval = setInterval(update, 2000); - const updatePortTableInterval = setInterval(updatePortTable, 1000); - }); -}); diff --git a/html/stat.html b/html/stat.html index 993eeded..ba114fcd 100644 --- a/html/stat.html +++ b/html/stat.html @@ -1,7 +1,6 @@ - FreeSwitchOS Port Statistics + + + +
- - -
-
-

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' From c4928e5546f9c55f36ef148eacab5ddc5cd0837d Mon Sep 17 00:00:00 2001 From: eraiza0816 <10572974+eraiza0816@users.noreply.github.com> Date: Tue, 11 Aug 2026 06:38:55 +0900 Subject: [PATCH 06/12] docs(webui): describe the single-page layout and updated numbers The Web UI is now 8 files at 25,302 bytes gzip (23.0 % of the raw 109,814); section switching measured on a real device. --- doc/webui-compression.md | 56 +++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/doc/webui-compression.md b/doc/webui-compression.md index 4839e08f..39f2b1d1 100644 --- a/doc/webui-compression.md +++ b/doc/webui-compression.md @@ -11,18 +11,24 @@ html/ --(minify.py)--> output/html_min/ --(fileadder -z)--> flash image ``` - `tools/minify.py` removes comments (``, `//`, `/* */`) and - line-leading whitespace without touching string literals, so the output - is functionally identical to the input. Binary files (`.ico`) are - copied through unchanged. The raw sources in `html/` stay untouched - for development; the minified copy in `output/` is a pure build - artifact. Comments and indentation are pure overhead for the served - bytes, so minification alone already reduces the transfer size from - 111,280 to 99,715 bytes, independent of gzip. -- The per-page scripts are consolidated into a single `html/main.js` - bundle (i18n, shared helpers, navigation and the page scripts). Every - page loads only this one script, so the browser fetches it once and - gzip can compress across all of it. The page-specific initialisation - runs only when the matching page elements are present. + line-leading whitespace without touching string or regex literals, so + the output is functionally identical to the input. Binary files + (`.ico`) are copied through unchanged. The raw sources in `html/` stay + untouched for development; the minified copy in `output/` is a pure + build artifact. Comments and indentation are pure overhead for the + served bytes, so minification alone already reduces the transfer size + from 109,814 to 97,730 bytes, independent of gzip. +- The Web UI is a single-page application: `index.html` holds all pages + as sections (`#/ports`, `#/vlan`, ...) that the sidebar switches + between via the URL hash. `login.html` stays separate because it is + the authentication gate. Only these two pages plus `main.js`, + `style.css`, the three SVG port images and the favicon are embedded. +- All JavaScript (i18n, shared helpers, navigation and the per-section + scripts) lives in one `html/main.js` bundle that the pages load as + their only script, so the browser fetches it once and gzip can + compress across all of it. 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. - `fileadder -z` gzip-compresses every file (zlib, gzip format, `Z_BEST_COMPRESSION`) when it generates the file table (`html_data.c`/`html_data.h`) and when it embeds the files into the @@ -41,27 +47,31 @@ Build on any machine (the Web UI is machine-independent): | | bytes | |---|---:| -| raw sources | 111,280 | -| minified (comments and indentation removed) | 99,715 | -| gzip (flash + wire) | 28,200 (25.3 %) | +| raw sources | 109,814 | +| minified (comments and indentation removed) | 97,730 | +| gzip (flash + wire) | 25,302 (23.0 %) | The minified assets are also what the browser receives when gzip support is missing or disabled, so the transfer size drops in both stages: -111,280 → 99,715 bytes from minification, then → 28,200 bytes from gzip. +109,814 → 97,730 bytes from minification, then → 25,302 bytes from gzip. The embedded data block ends at `0x5b977` without compression and at -`0x46e28` with it, freeing ~83 KB of the 512 KB image. +`0x462d6` with it, freeing ~86 KB of the 512 KB image. -Merging the JS files into one bundle is worth more than the minifier and -gzip combined: served per file, the 17 scripts gzip to 23,302 bytes; as -one `main.js` bundle they gzip to 16,948 bytes, because the gzip -dictionary spans all scripts and every page fetches the bundle only -once. +The consolidation helps at every step. The 17 scripts gzip to 23,302 +bytes as separate files but to 17,231 bytes as one `main.js` bundle, +because the gzip dictionary spans all of them. The single-page layout +then removes the per-page HTML and, more importantly, the repeated +downloads: with the multi-page UI the browser fetched `main.js` on every +navigation; the SPA fetches everything once and section switches are +pure in-page JavaScript (measured on a real device: cold load ~450 ms, +section switch ~60-160 ms, the only network traffic afterwards is the +JSON polling of the visible section). ## Notes - The files stay well below the `uint16_t` size limit of the file table - (largest gzip output: 16.9 KB for the merged `main.js`). + (largest gzip output: 17.2 KB for the merged `main.js`). - `fileadder` terminates the embedded files with a NUL directly after the content (previously at `data_read + 1`), so the `strlen()`-based size computation no longer depends on uninitialised buffer content. From 4c4232cc07e6251d0e2682fc5b0746080eb7521d Mon Sep 17 00:00:00 2001 From: eraiza0816 <10572974+eraiza0816@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:15:44 +0900 Subject: [PATCH 07/12] fix(makefile): undo --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b2069549..d8a631f9 100644 --- a/Makefile +++ b/Makefile @@ -135,7 +135,7 @@ $(BUILDDIR)/rtlplayground-$(FILENAME_EXTENSION).bin: $(BUILDDIR)/rtlplayground.i tools/output/crc_calculator -u $@ ln -sf $(MACHINE)/rtlplayground-$(FILENAME_EXTENSION).bin output/rtlplayground.bin -.PHONY: clean all $(SUBDIRS) +.PHONY: clean all $(SUBDIRS) $(VERSION_HEADER) .PHONY: machine_check: From 3f5093f180ec6ef50b4d47d882e08808afb84533 Mon Sep 17 00:00:00 2001 From: eraiza0816 <10572974+eraiza0816@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:18:08 +0900 Subject: [PATCH 08/12] Delete: go-lang --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6ed31e03..e4b393a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,6 @@ RUN apt-get update && apt-get install -y \ xxd \ python3 \ libjson-c-dev \ - golang-go \ git \ && rm -rf /var/lib/apt/lists/* From 774e04816e49f5031784bf48a24ca34ec46c0339 Mon Sep 17 00:00:00 2001 From: eraiza0816 <10572974+eraiza0816@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:31:14 +0900 Subject: [PATCH 09/12] Fix login loop and system input wipe --- html/main.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/html/main.js b/html/main.js index a549a7a7..e020bd3f 100644 --- a/html/main.js +++ b/html/main.js @@ -935,7 +935,9 @@ var sectionIntervals = []; var sectionInits = {}; function setSectionInterval(fn, ms) { - sectionIntervals.push(setInterval(fn, ms)); + var id = setInterval(fn, ms); + sectionIntervals.push(id); + return id; } function clearSectionIntervals() { @@ -961,6 +963,7 @@ window.addEventListener('hashchange', function() { }); document.addEventListener('DOMContentLoaded', function() { + if (!document.getElementById('page-overview')) return; // not index.html (e.g. login.html) showSection((location.hash || '#/overview').replace(/^#\//, '')); }); From af4101f0604d914aa7aa552f2f2adcb5d4ac7c8a Mon Sep 17 00:00:00 2001 From: eraiza0816 <10572974+eraiza0816@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:15:44 +0900 Subject: [PATCH 10/12] Fix language select, stat port index, devWalk polling, and duplicate BW command --- html/index.html | 4 ++-- html/main.js | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/html/index.html b/html/index.html index de315ca9..28d6d345 100644 --- a/html/index.html +++ b/html/index.html @@ -250,7 +250,7 @@
- @@ -260,7 +260,7 @@
-
diff --git a/html/main.js b/html/main.js index e020bd3f..c3fec057 100644 --- a/html/main.js +++ b/html/main.js @@ -947,6 +947,10 @@ function clearSectionIntervals() { clearTimeout(l2Timer); l2Timer = null; } + if (devTimer) { + clearTimeout(devTimer); + devTimer = null; + } } function showSection(name) { @@ -1495,11 +1499,14 @@ function devRender(entries) { } } +var devTimer = null; function devWalk() { walkL2(function(entries, ok) { if (ok) devRender(entries); - setTimeout(devWalk, 15000); + var sec = document.getElementById('page-ports'); + if (sec && sec.style.display === 'block') + devTimer = setTimeout(devWalk, 15000); }); } @@ -1717,7 +1724,7 @@ function fillStats() { td = tr.insertCell();td.appendChild(document.createTextNode(`${txB[i]}` + t('common_pkts'))); td = tr.insertCell();td.appendChild(document.createTextNode(`${rxG[i]}` + t('common_pkts'))); td = tr.insertCell();td.appendChild(document.createTextNode(`${rxB[i]}` + t('common_pkts'))); - var button = ''; + var button = ''; td = tr.insertCell(); td.innerHTML = button; } } @@ -2496,9 +2503,10 @@ async function applyBandwidth(i) { cmd = 'bw in ' + (i-1) + ' ' + parseInt(document.getElementById("ibw_" + i).value).toString(16).padStart(4, "0");; doCMD(cmd); if (document.getElementById("ilimit_port_" + i).checked) { - if (!document.getElementById("fc_port_" + i).checked) + if (!document.getElementById("fc_port_" + i).checked) { cmd = "bw in " + (i-1) + " drop"; - doCMD(cmd); + doCMD(cmd); + } } var cmd = "bw out " + (i-1) + " off"; if (document.getElementById("elimit_port_" + i).checked) From 7df3c87892e38f1d2c04d70b8c73a05fe63b3504 Mon Sep 17 00:00:00 2001 From: eraiza0816 <10572974+eraiza0816@users.noreply.github.com> Date: Wed, 9 Sep 2026 10:58:16 +0900 Subject: [PATCH 11/12] Fix counters rejection of logical port 0 --- CHANGELOG.md | 5 +++++ httpd/page_impl.c | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 168bd34f..a9e5a074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ ## Fixed +- Web UI + - `/counters.json` rejects physical port 1 on 9-port devices. + A mapped logical port of `0` was treated as invalid, + but it is a valid port there. + ## Breaking changes - Config diff --git a/httpd/page_impl.c b/httpd/page_impl.c index b99f0b9b..e0377203 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -302,7 +302,7 @@ bool send_counters(uint8_t phys_port) if (phys_port_idx > 8) goto err; uint8_t log_port = machine.phys_to_log_port[phys_port_idx]; - if (log_port == 0) + if (log_port < machine.min_port || log_port > machine.max_port) goto err; dbg_string("send_counters called: "); dbg_byte(phys_port_idx); dbg_char('\n'); From 98bda62181b3e9deba7e9ddd85e1fd41ea98de60 Mon Sep 17 00:00:00 2001 From: eraiza0816 <10572974+eraiza0816@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:51:51 +0900 Subject: [PATCH 12/12] Fix send_counters validation and changelog --- CHANGELOG.md | 8 +++----- httpd/page_impl.c | 4 +--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9e5a074..704f1058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,13 @@ ## Added +- Web UI + - Compress the embedded assets (minify + gzip) and switch to a single-page layout. #315 + ## Changed ## Fixed -- Web UI - - `/counters.json` rejects physical port 1 on 9-port devices. - A mapped logical port of `0` was treated as invalid, - but it is a valid port there. - ## Breaking changes - Config diff --git a/httpd/page_impl.c b/httpd/page_impl.c index e0377203..1d725626 100644 --- a/httpd/page_impl.c +++ b/httpd/page_impl.c @@ -299,11 +299,9 @@ void send_vlan(uint16_t vlan) bool send_counters(uint8_t phys_port) { uint8_t phys_port_idx = phys_port - 1; - if (phys_port_idx > 8) + if (phys_port_idx > (machine.max_port - machine.min_port)) goto err; uint8_t log_port = machine.phys_to_log_port[phys_port_idx]; - if (log_port < machine.min_port || log_port > machine.max_port) - goto err; dbg_string("send_counters called: "); dbg_byte(phys_port_idx); dbg_char('\n'); slen = strtox(outbuf, HTTP_RESPONCE_JSON);