Merge pull request #429 from bloqaudio/feat/webui-v2

web: replace the multi-page UI with a themed single-page app
This commit is contained in:
René van Dorst authored and GitHub committed 2026-09-14 17:17:39 +00:00
commit abf1cd1344
13 files changed
+2419 -3502

No files matched your search

+3
View File
@@ -6,6 +6,9 @@
- Web UI - Web UI
- Compress the embedded assets (minify + gzip) and switch to a single-page layout. #315 - Compress the embedded assets (minify + gzip) and switch to a single-page layout. #315
- Replace the multi-page UI with a themed single-page app: light, dark and Selenized themes following the browser by default,
English, Japanese and Chinese, save to flash merges the command log into the startup config and verifies the write,
firmware images are checked in the browser before upload. #429
## Changed ## Changed
+63 -91
View File
@@ -1,135 +1,107 @@
# Supporting Multiple Languages in the Web UI # Supporting Multiple Languages in the Web UI
The firmware uses a client-side i18n approach The firmware uses a client-side i18n approach: all translations are stored
all translations are stored in a single JavaScript dictionary embedded in the firmware. in one JavaScript dictionary embedded in the firmware. No server-side
No server-side changes are needed. changes are needed.
## Architecture ## Architecture
All translation logic lives in `html/i18n.js`. The file contains: All translation logic lives at the top of `html/app.js`:
- A `LANG` object with one sub-object per language (`en`, `ja`, ...) - A `LANG` object with one sub-object per language (`en`, `ja`, `zh`)
- Language auto-detection (browser language → `localStorage` override) - Language auto-detection (browser language, overridden by the
- `t(key)` — look up a translated string `rtl_lang` key in `localStorage`, which the language selector in the
- `setLang(lang)` — switch language and update the page System section writes)
- `applyTranslation(el)` — apply translation to one DOM element - `t(key, vars)` looks up a translated string; `{name}` placeholders in
the string are replaced from `vars`, e.g. `t("v_del_q", {n: vid})`
- `i18nApply()` applies the dictionary to the static markup once at load
Translation keys are **flat strings** (no nesting). The English keys in `LANG.en` also serve as the fallback when a key is missing in another language. Translation keys are flat strings. The English entry is the fallback when
a key is missing in another language, and the key itself is the fallback
when it is missing everywhere.
Changing the language stores the choice and reloads the page, so every
dynamically built table is rebuilt in the new language.
`login.html` is served before authentication and does not load `app.js`;
it carries its own five strings per language in an inline table.
## How to Add a New Language ## How to Add a New Language
### 1. Add a dictionary entry in `html/i18n.js` ### 1. Add a dictionary entry in `html/app.js`
Append a new sub-object to the `LANG` object. Every key from `LANG.en` must be present: Append a sub-object to `LANG`. Every key from `LANG.en` should be present:
```js ```js
var LANG={ var LANG={
en:{ en:{
nav_overview: 'Overview', nav_dash:"Dashboard",
nav_port_config: 'Port Configuration', ...
// ... all keys for English
},
ja: {
nav_overview: '概要',
nav_port_config: 'ポート設定',
// ... all keys for Japanese
},
LANGCODE: { // ← add your language here
nav_overview: '...',
nav_port_config: '...',
// ... translate every key
}, },
LANGCODE:{ // add your language here
nav_dash:"...",
...
}
}; };
``` ```
### 2. Add the language to the navigation sidebar ### 2. Add the language to the selector in `html/index.html`
In `html/navigation.js`, add an `<option>` to the language selector: ```html
<select class="ctl" id="langSel" data-i18n-t="sy_lang">
```js <option value="en">English</option>
+ "<option value='en'>English</option><option value='ja'>日本語</option>" <option value="ja">日本語</option>
<option value="zh">中文</option>
<option value="LANGCODE">Native name</option>
</select>
``` ```
Replace with: ### 3. Add the login strings in `html/login.html`
```js The inline table there holds, per language: page title, subtitle,
+ "<option value='en'>English</option><option value='ja'>日本語</option><option value='LANGCODE'>Native Name</option>" password label, button label, wrong-password message.
```
### 3. Verify auto-detection ### 4. Verify auto-detection
The language detection code in `i18n.js` reads `navigator.language` and normalises it to the first two characters: The detection normalises `navigator.language` to its first two
characters and selects that language if `LANG` has it, otherwise English.
```js
var browser = (navigator.language || navigator.userLanguage || 'en').substring(0, 2);
return LANG[browser] ? browser : 'en';
```
If the two-letter code matches a key in `LANG`, it will be auto-selected. No changes needed here.
## Two Translation Mechanisms ## Two Translation Mechanisms
### (A) `data-i18n` attribute (declarative — for HTML) ### (A) `data-i18n` attributes (declarative, for HTML)
Add `data-i18n="key_name"` to any HTML element. The English text goes in the element content as a fallback:
```html ```html
<h1 data-i18n="port_heading">Port Configuration</h1> <h2 data-i18n="pt_title">Port configuration</h2>
<input type="button" data-i18n="port_apply" value="Apply"> <button class="ctl" data-i18n="c_apply" data-i18n-t="sy_dhcp_t">Apply</button>
<option data-i18n="port_auto">Auto</option> <input class="in" data-i18n-p="l2_filter">
<title data-i18n="port_title">Port Configuration</title>
``` ```
On page load, `applyTranslation()` sets: - `data-i18n` sets the element's text content
- `data-i18n-t` sets the `title` attribute (tooltip)
- `data-i18n-p` sets the `placeholder` attribute
- `el.value` for `<input type="submit|button">` The English text stays in the markup as the fallback and as the source
- `el.textContent` for `<option>`, `<title>` of truth for what the key means.
- `el.innerHTML` for everything else
### (B) `t('key')` call (imperative — for JavaScript strings) ### (B) `t("key")` calls (imperative, for JavaScript)
When generating HTML or text in JavaScript, wrap translatable strings with `t()`:
```js ```js
td.appendChild(document.createTextNode(t('common_port') + i)); tr.insertCell().textContent=t("c_port")+" "+p;
td.innerHTML = t('port_auto'); toast(t("v_loaded",{n:vid}),"ok");
iHTML += "<tr><td>" + t('port_vendor') + "</td></tr>";
``` ```
### Special Case: Link Speed Display Strings that are identical in every language (`10M`, `2.5G`, `MAC`,
`VLAN`, `CPU`, `RSTP`) are literals, not dictionary entries.
The `linkS` array in `html/main.js` maps numeric link states to display strings. The first two entries (`speed_disabled`, `speed_down`) use `t()` for translation; the remaining entries are static literals (they are the same in all languages):
```js
const linkS = [
function(){return t('speed_disabled')},
function(){return t('speed_down')},
"10M", "100M", "1000M", "500M", "10G", "2.5G", "5G"
];
function linkText(idx) { var v = linkS[idx]; return typeof v === 'function' ? v() : v; }
```
Always use `linkText(idx)` (not `linkS[idx]`) to read these values.
## Size Considerations ## Size Considerations
- `html/i18n.js` is embedded in the firmware filesystem (~14 KB for two languages) - Each language adds roughly 3 KB to `app.js` before compression; the
- Each new language adds roughly the same number of bytes as the English dictionary (~3–4 KB) build gzips the file, so the flash cost of a language is closer to 1 KB
- The firmware binary is padded to 512 KiB, so a few extra KB do not change the flash footprint - The largest embedded file must stay below 64 KB after compression
- Values that are identical in all languages should be inlined as literals rather than added to the dictionary (e.g., `"10M"`, `"2.5G"`, `"MAC"`, `"VLAN"`, `"CPU"`) (`uint16_t` length in the file table); `app.js` with three languages
is about 29 KB compressed
## Build ## Build
No special flags are needed. The `html/` directory is embedded by `fileadder` during the build. No special flags are needed. `html/` is minified and embedded by the
normal build (see `webui-compression.md`).
## Script Load Order
`i18n.js` must be loaded after `main.js` (which defines `t()`'s dependencies like `LANG`) but before any page-specific JS that calls `t()`:
```html
<script src="/main.js"></script>
<script src="/i18n.js"></script>
<script src="/eee.js"></script> <!-- uses t() -->
```
The `navigation.js` script is loaded last (bottom of `<body>`).
+12 -10
View File
@@ -19,16 +19,17 @@ html/ --(minify.py)--> output/html_min/ --(fileadder -z)--> flash image
served bytes, so minification alone already reduces the transfer size served bytes, so minification alone already reduces the transfer size
from 109,814 to 97,730 bytes, independent of gzip. from 109,814 to 97,730 bytes, independent of gzip.
- The Web UI is a single-page application: `index.html` holds all pages - The Web UI is a single-page application: `index.html` holds all pages
as sections (`#/ports`, `#/vlan`, ...) that the sidebar switches as sections (`#ports`, `#vlan`, ...) that the sidebar switches
between via the URL hash. `login.html` stays separate because it is between via the URL hash. `login.html` stays separate because it is
the authentication gate. Only these two pages plus `main.js`, the authentication gate and is self-contained (its own styles and
`style.css`, the three SVG port images and the favicon are embedded. strings), so nothing else is served before login. Only these two
- All JavaScript (i18n, shared helpers, navigation and the per-section pages plus `app.js` and the favicon are embedded; the stylesheet and
scripts) lives in one `html/main.js` bundle that the pages load as the theme bootstrap are inline in `index.html`.
their only script, so the browser fetches it once and gzip can - All JavaScript (translation table, shared helpers, navigation and the
compress across all of it. A section's initialisation runs when the per-section code) lives in one `html/app.js` that the page loads as
section is shown, its polling intervals run only while it is visible, its only script, so the browser fetches it once and gzip can compress
and re-entering a section refetches the data. across all of it. A section starts its pollers when it is shown and
stops them when it is left; re-entering a section refetches the data.
- `fileadder -z` gzip-compresses every file (zlib, gzip format, - `fileadder -z` gzip-compresses every file (zlib, gzip format,
`Z_BEST_COMPRESSION`) when it generates the file table `Z_BEST_COMPRESSION`) when it generates the file table
(`html_data.c`/`html_data.h`) and when it embeds the files into the (`html_data.c`/`html_data.h`) and when it embeds the files into the
@@ -71,7 +72,8 @@ JSON polling of the visible section).
## Notes ## Notes
- The files stay well below the `uint16_t` size limit of the file table - The files stay well below the `uint16_t` size limit of the file table
(largest gzip output: 17.2 KB for the merged `main.js`). (largest gzip output: about 29 KB for `app.js`, three languages
included).
- `fileadder` terminates the embedded files with a NUL directly after the - `fileadder` terminates the embedded files with a NUL directly after the
content (previously at `data_read + 1`), so the `strlen()`-based size content (previously at `data_read + 1`), so the `strlen()`-based size
computation no longer depends on uninitialised buffer content. computation no longer depends on uninitialised buffer content.
+1844
View File
File diff suppressed because it is too large. Load diff
+400 -261
View File
@@ -1,310 +1,449 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<script src="/main.js"></script> <head>
<link rel="stylesheet" href="style.css"> <meta charset="utf-8">
<title data-i18n="index_title">FreeSwitchOS Main Page</title> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Switch</title>
<style> <style>
.popup { :root{
display: none; --bg:#f4f6f8;--panel:#ffffff;--panel2:#eaeef2;--bd:#d7dde4;--fg:#1a222c;--fg2:#5c6773;
position: fixed; --ac:#0969da;--acfg:#ffffff;--ok:#158744;--bad:#cf222e;--warn:#9a6700;--cy:#1b7c83;
top: 0; left: 0; --sel:rgba(9,105,218,.10);--shadow:0 2px 10px rgba(20,30,40,.07);
width: 100%; height: 100%; --s10:#b58900;--s100:#b58900;--s1000:#159744;--s2g5:#0d9488;--s5g:#0d9488;--s10g:#2563eb;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
} }
.popup-content { :root[data-theme="dark"]{
background: #fff; --bg:#0e1116;--panel:#161b22;--panel2:#1f262e;--bd:#2c333c;--fg:#e6edf3;--fg2:#8d96a0;
padding: 20px; --ac:#4c8ef7;--acfg:#ffffff;--ok:#3fb950;--bad:#f85149;--warn:#d29922;--cy:#39c5cf;
border-radius: 5px; --sel:rgba(76,142,247,.16);--shadow:0 2px 10px rgba(0,0,0,.35);
text-align: center; --s10:#d29922;--s100:#d29922;--s1000:#3fb950;--s2g5:#2dd4bf;--s5g:#2dd4bf;--s10g:#58a6ff;
height: 80%
} }
.popup .popup-content { :root[data-theme="sel-dark"]{
overflow-y: scroll; --bg:#103c48;--panel:#184956;--panel2:#2d5b69;--bd:#2d5b69;--fg:#adbcbc;--fg2:#72898f;
--ac:#4695f7;--acfg:#ffffff;--ok:#75b938;--bad:#fa5750;--warn:#dbb32d;--cy:#41c7b9;
--sel:rgba(70,149,247,.18);--shadow:0 2px 10px rgba(0,20,26,.35);
--s10:#dbb32d;--s100:#dbb32d;--s1000:#75b938;--s2g5:#41c7b9;--s5g:#41c7b9;--s10g:#4695f7;
}
:root[data-theme="sel-light"]{
--bg:#fbf3db;--panel:#ece3cc;--panel2:#d5cdb6;--bd:#d5cdb6;--fg:#53676d;--fg2:#909995;
--ac:#0072d4;--acfg:#ffffff;--ok:#489100;--bad:#d2212d;--warn:#ad8900;--cy:#009c8f;
--sel:rgba(0,114,212,.12);--shadow:0 2px 10px rgba(80,70,40,.12);
--s10:#ad8900;--s100:#ad8900;--s1000:#489100;--s2g5:#009c8f;--s5g:#009c8f;--s10g:#0072d4;
}
*{box-sizing:border-box;margin:0}
html,body{height:100%}
body{
font:14px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
background:var(--bg);color:var(--fg);display:flex;
}
button,input,select,textarea{font:inherit;color:var(--fg)}
a{color:var(--ac);text-decoration:none}
#nav{
width:200px;flex:none;background:var(--panel);border-right:1px solid var(--bd);
display:flex;flex-direction:column;position:sticky;top:0;height:100vh;
}
#brand{display:flex;align-items:center;gap:9px;padding:16px 16px 12px;font-weight:650;font-size:15px}
#brand svg{flex:none;stroke:var(--ac)}
#brand span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#navlist{list-style:none;padding:4px 8px;overflow-y:auto;flex:1}
#navlist li{
display:flex;align-items:center;gap:9px;padding:7px 10px;margin:1px 0;
border-radius:7px;cursor:pointer;color:var(--fg2);font-weight:500;
}
#navlist li svg{flex:none;stroke:currentColor}
#navlist li:hover{background:var(--panel2);color:var(--fg)}
#navlist li.act{background:var(--sel);color:var(--ac)}
#navfoot{padding:10px 16px;border-top:1px solid var(--bd);font-size:12px;color:var(--fg2)}
#wrap{flex:1;min-width:0;display:flex;flex-direction:column}
#top{
display:flex;align-items:center;gap:10px;padding:10px 20px;
background:var(--panel);border-bottom:1px solid var(--bd);position:sticky;top:0;z-index:5;
}
#burger{display:none;background:none;border:0;cursor:pointer;padding:4px}
#ttitle{font-size:16px;font-weight:650;flex:1}
#dirty{
display:none;font-size:12px;font-weight:600;color:var(--warn);
border:1px solid var(--warn);border-radius:20px;padding:2px 10px;
}
#dirty.show{display:inline-block}
select.ctl,button.ctl{
background:var(--panel2);border:1px solid var(--bd);border-radius:7px;
padding:5px 9px;cursor:pointer;font-size:13px;white-space:nowrap;
}
button.pri{background:var(--ac);color:var(--acfg);border-color:transparent;font-weight:600}
button.pri:hover,button.ctl:hover{filter:brightness(1.07)}
button:disabled{opacity:.45;cursor:default;filter:none}
main{padding:18px 20px 40px;max-width:1400px;width:100%}
section.tab{display:none}
section.tab.act{display:block}
.card{
background:var(--panel);border:1px solid var(--bd);border-radius:10px;
box-shadow:var(--shadow);padding:16px 18px;margin-bottom:16px;
}
.card h2{font-size:14px;font-weight:650;margin-bottom:12px;color:var(--fg)}
.card h2 .hint{font-weight:400;font-size:12px;color:var(--fg2);margin-left:8px}
.grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:16px;align-items:start}
.grid2 .card{margin-bottom:0}
table.t{width:100%;border-collapse:collapse;font-size:13px}
table.t th{
text-align:left;font-weight:600;color:var(--fg2);font-size:12px;
padding:6px 8px;border-bottom:1px solid var(--bd);white-space:nowrap;
}
table.t th[data-sort]{cursor:pointer;user-select:none}
table.t td{padding:6px 8px;border-bottom:1px solid var(--bd);vertical-align:middle}
table.t tr:last-child td{border-bottom:0}
table.t tr:hover td{background:var(--sel)}
.num{font-variant-numeric:tabular-nums;text-align:right}
.scrollx{overflow-x:auto}
input.in,select.in,textarea.in{
background:var(--bg);border:1px solid var(--bd);border-radius:7px;padding:5px 8px;outline:none;
}
input.in:focus,select.in:focus,textarea.in:focus{border-color:var(--ac)}
input.in.sm{width:7em}
label.row{display:flex;align-items:center;gap:10px;margin:8px 0}
label.row span.lb{width:110px;flex:none;color:var(--fg2);font-size:13px}
.switch{position:relative;display:inline-block;width:34px;height:19px;flex:none}
.switch input{opacity:0;width:0;height:0}
.switch i{
position:absolute;inset:0;background:var(--bd);border-radius:19px;transition:.15s;cursor:pointer;
}
.switch i:before{
content:"";position:absolute;width:15px;height:15px;border-radius:50%;
left:2px;top:2px;background:var(--panel);transition:.15s;
}
.switch input:checked+i{background:var(--ok)}
.switch input:checked+i:before{transform:translateX(15px)}
#strip{display:flex;gap:8px;flex-wrap:wrap;padding:4px 0 2px}
.port{
width:46px;border:2px solid var(--bd);border-radius:7px;padding:5px 0 3px;
text-align:center;cursor:pointer;background:var(--panel2);position:relative;
}
.port.sfp{width:56px}
.port .pn{font-weight:650;font-size:13px}
.port .ps{font-size:10px;color:var(--fg2);display:block;line-height:1.2}
.port .jack{
display:block;width:18px;height:12px;margin:3px auto 2px;border-radius:2px;
border:2px solid var(--fg2);border-top-width:5px;opacity:.55;
}
.port.sfp .jack{width:28px;height:10px;border-top-width:3px}
.port.up{border-color:var(--pc);background:color-mix(in srgb,var(--pc) 10%,var(--panel2))}
.port.up .jack{border-color:var(--pc);opacity:1}
.port.up .ps{color:var(--pc);font-weight:600}
.port.dis{opacity:.45;border-style:dashed}
.badge{font-size:11px;font-weight:600;border-radius:10px;padding:1px 8px;background:var(--panel2);color:var(--fg2)}
.badge.ok{color:var(--ok)}.badge.bad{color:var(--bad)}
.seg{display:inline-flex;border:1px solid var(--bd);border-radius:7px;overflow:hidden}
.seg button{
background:var(--panel);border:0;padding:3px 8px;cursor:pointer;font-size:12px;color:var(--fg2);
border-right:1px solid var(--bd);
}
.seg button:last-child{border-right:0}
.seg button.on{background:var(--ac);color:var(--acfg)}
#toasts{position:fixed;right:16px;bottom:16px;z-index:50;display:flex;flex-direction:column;gap:8px}
.toast{
background:var(--panel);border:1px solid var(--bd);border-left:4px solid var(--ac);
border-radius:8px;padding:9px 14px;box-shadow:var(--shadow);font-size:13px;max-width:340px;
animation:tin .18s ease-out;
}
.toast.ok{border-left-color:var(--ok)}.toast.err{border-left-color:var(--bad)}
@keyframes tin{from{transform:translateY(8px);opacity:0}to{transform:none;opacity:1}}
#mback{
position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:40;display:none;
align-items:flex-start;justify-content:center;padding:6vh 16px;
}
#mback.show{display:flex}
#modal{
background:var(--panel);border:1px solid var(--bd);border-radius:12px;box-shadow:var(--shadow);
width:min(760px,96vw);max-height:86vh;display:flex;flex-direction:column;
}
#mhead{display:flex;align-items:center;padding:13px 18px;border-bottom:1px solid var(--bd)}
#mtitle{font-weight:650;font-size:15px;flex:1}
#mx{background:none;border:0;font-size:19px;cursor:pointer;color:var(--fg2);padding:0 2px}
#mbody{padding:14px 18px;overflow:auto}
#mfoot{padding:12px 18px;border-top:1px solid var(--bd);display:flex;gap:10px;justify-content:flex-end}
#mfoot:empty{display:none}
pre.cfg{
background:var(--bg);border:1px solid var(--bd);border-radius:8px;padding:10px 12px;
font:12px/1.5 ui-monospace,Consolas,monospace;white-space:pre-wrap;word-break:break-all;
}
textarea.cfg{
width:100%;min-height:200px;background:var(--bg);border:1px solid var(--bd);border-radius:8px;
padding:10px 12px;font:12px/1.5 ui-monospace,Consolas,monospace;resize:vertical;outline:none;
}
.mono{font-family:ui-monospace,Consolas,monospace;font-size:12px}
.mut{color:var(--fg2)}
.small{font-size:12px}
.dangerzone{border-color:color-mix(in srgb,var(--bad) 40%,var(--bd))}
button.danger{background:var(--bad);color:#fff;border-color:transparent}
@media (max-width:820px){
#nav{position:fixed;left:-210px;transition:left .18s;z-index:30;box-shadow:var(--shadow)}
#nav.open{left:0}
#burger{display:block}
main{padding:14px 12px 40px}
#top{padding:8px 10px;gap:7px}
#ttitle{font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#dirty{padding:2px 7px}
} }
.tab-bar { display: flex; border-bottom: 2px solid #226; margin-bottom: 0; padding: 1px 16px; padding-bottom: 0; }
.tab-btn { padding: 10px 20px; background-color: #ddf; border: none; cursor: pointer; font-size: 1em; border-radius: 8px 8px 0 0; margin-right: 4px; }
.tab-btn:hover { background-color: #aaf; }
.tab-btn.active { background-color: #aaf; font-weight: bold; border-bottom: 2px solid #aaf; margin-bottom: -2px; }
.tab-content { display: none; }
.tab-content.active { display: block; }
</style> </style>
<script>
(function(){
var pref;
try{pref=localStorage.getItem("theme")||"auto";}catch(e){pref="auto";}
var dark=window.matchMedia("(prefers-color-scheme: dark)").matches;
var t=pref;
if(pref==="auto")t=dark?"dark":"light";
if(pref==="auto-sel")t=dark?"sel-dark":"sel-light";
document.documentElement.dataset.theme=t;
})();
</script>
</head> </head>
<body> <body>
<nav id="sidebar"></nav>
<div id="ports" style="margin-left:16%;padding:1px 16px"></div>
<div class="page" id="page-overview" style="margin-left:16%;padding:1px 16px;height:1000px;"> <nav id="nav">
<h1 data-i18n="index_heading">Switch Configuration</h1> <div id="brand">
<table id="infoTable"> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="2">
<tr> <rect x="2" y="7" width="20" height="10" rx="2"/><path d="M6 11v2M10 11v2M14 11v2M18 11v2"/>
<th colspan="2" data-i18n="index_settings">Settings</th> </svg>
</tr> <span id="brandname">Switch</span>
<tbody>
</tbody>
</table>
</div> </div>
<ul id="navlist"></ul>
<div id="navfoot"><span id="fver"></span></div>
</nav>
<div class="page" id="page-ports" style="margin-left:16%;padding:1px 16px;height:1000px;"> <div id="wrap">
<h1 data-i18n="port_heading">Port Configuration</h1> <header id="top">
<table id="speedtable"> <button id="burger" title="Menu">
<tr> <th data-i18n="port_col_port">Port</th> <th data-i18n="port_col_name">Name</th> <th data-i18n="port_col_speed">Current Link Speed</th><th data-i18n="port_col_devices">Connected devices</th><th data-i18n="port_col_set_speed">Set Speed</th><th data-i18n="port_col_disabled">Disabled</th><th data-i18n="port_col_apply">Apply</th></tr> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--fg)" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
</table> </button>
<h2 style="margin-top:3em" data-i18n="port_mtu_heading">Configure Maximum Frame Size (MTU) forwarded at Port</h2> <span id="ttitle"></span>
<table id="mtutable" style="margin-top:1em"> <span id="dirty" data-i18n="hdr_dirty" data-i18n-t="hdr_dirty_t">unsaved changes</span>
</table> <button class="ctl pri" id="saveBtn" data-i18n="hdr_save" data-i18n-t="hdr_save_t">Save to flash</button>
</div> </header>
<div class="page" id="page-stat" style="margin-left:16%;padding:1px 16px;height:1000px;"> <main>
<div id="popup" class="popup"> <section class="tab act" id="tab-dash">
<div class="popup-content"> <div class="card"><h2><span data-i18n="d_ports">Ports</span> <span class="hint" data-i18n="d_ports_h">click a port for details</span></h2>
<h2 data-i18n="stat_detailed">Detailed Port Statistics</h2> <div id="strip"></div>
<div id="popup_text"></div> </div>
<button id="closePopup" class="action" data-i18n="stat_close">Close</button> <div class="grid2">
<div class="card"><h2 data-i18n="d_system">System</h2><table class="t" id="sysinfo"></table></div>
<div class="card"><h2><span data-i18n="d_traffic">Traffic</span> <span class="hint" data-i18n="d_traffic_h">packets/s, live</span></h2>
<div class="scrollx"><table class="t" id="traffic">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="c_link">Link</th><th class="num" data-i18n="d_txpps">TX pps</th><th class="num" data-i18n="d_rxpps">RX pps</th><th class="num" data-i18n="d_txbad">TX bad</th><th class="num" data-i18n="d_rxbad">RX bad</th></tr></thead>
<tbody></tbody></table></div>
</div> </div>
</div> </div>
<h1 data-i18n="stat_heading">Port Statistics</h1> </section>
<table id="statstable">
<tr> <th data-i18n="stat_col_port">Port</th> <th data-i18n="stat_col_name">Name</th> <th data-i18n="stat_col_link">link</th> <th data-i18n="stat_col_tx_good">TX Good</th> <th data-i18n="stat_col_tx_bad">TX Bad</th> <th data-i18n="stat_col_rx_good">RX Good</th> <th data-i18n="stat_col_rx_bad">RX Bad</th> <th data-i18n="stat_col_all"> All Counters </th></tr>
</table>
</div>
<div class="page" id="page-vlan" style="margin-left:16%;padding:1px 16px;height:1000px;"> <section class="tab" id="tab-ports">
<h1 data-i18n="vlan_heading">VLAN Configuration</h1> <div class="card"><h2 data-i18n="pt_title">Port configuration</h2>
<form id="vform"> <div class="scrollx"><table class="t" id="ptable">
<div> <thead><tr><th data-i18n="c_port">Port</th><th data-i18n="c_name">Name</th><th data-i18n="c_link">Link</th><th data-i18n="pt_conn">Connected</th><th data-i18n="pt_speed">Speed</th><th data-i18n="c_enabled">Enabled</th><th data-i18n="pt_mtu">MTU</th><th></th></tr></thead>
<label for="vlanSelect" data-i18n="vlan_select">VLAN Select:</label> <tbody></tbody></table></div>
<select id="vlanSelect" style="margin: 0 0 0 8px"> <p class="small mut" style="margin-top:8px" data-i18n="pt_sfp_note">SFP speed is set per slot and is not saved in the startup config.</p>
<option value="" disabled selected data-i18n="vlan_choose">— VLAN Choose —</option>
</select>
</div>
<br/>
<div>
<label for="vid" data-i18n="vlan_id">VLAN ID:</label>
<input type="number" min="1" max="4094" id="vid" name="vid">
<button type="button" style="margin: 0 0 0 24px" onclick="fetchVLAN();" data-i18n="vlan_get_config">Get Configuration</button>
</div>
<br/><br/>
<label for="vname" data-i18n="vlan_name">VLAN Name:</label>
<input type="text" id="vname" name="vname"><br><br>
<br/>
<h2 data-i18n="vlan_tagged">Tagged Ports</h2>
<div id="tPorts"><button type="button" style="transform: translateY(-100%);margin: 0 50px 0 0" onclick="utClicked(true);" data-i18n="vlan_select_all">Select all</button></div>
<h2 data-i18n="vlan_untagged">Untagged Ports</h2>
<div id="uPorts"><button type="button" style="transform: translateY(-100%); margin: 0 50px 0 0" onclick="utClicked(false);" data-i18n="vlan_select_all">Select all</button> </div>
<h2 data-i18n="vlan_pvid">Use as default VLAN for incoming traffic (PVID)</h2>
<div id="pPorts"><button type="button" style="transform: translateY(-100%); margin: 0 50px 0 0" onclick="pvClicked(true);" data-i18n="vlan_select_all">Select all</button> </div>
<br/> <input style="width:40%;" class="action" id="vlan_sub" onclick="vlanSub();" type="button" data-i18n="vlan_update" value="Update / Create">
</form>
<h2 data-i18n="vlan_configured">Configured VLANs</h2>
<table id="vlanTable" style="width:90%">
<thead>
<tr>
<th>VLAN</th>
<th data-i18n="vlan_col_name">Name</th>
<th data-i18n="vlan_col_member">Member Ports</th>
<th data-i18n="vlan_col_tagged">Tagged Ports</th>
<th data-i18n="vlan_col_untagged">Untagged Ports</th>
<th data-i18n="vlan_col_pvid">PVID Ports</th>
<th data-i18n="vlan_col_delete">Delete</th>
</tr>
</thead>
<tbody id="vlanTableBody">
</tbody>
</table>
</div> </div>
</section>
<div class="page" id="page-l2" style="margin-left:16%;padding:1px 16px;height:1000px;"> <section class="tab" id="tab-stp">
<h1 data-i18n="l2_heading">L2 Configuration</h1> <div class="card"><h2><span data-i18n="stp_title">Spanning tree</span> <span class="hint" data-i18n="stp_h">RSTP, 802.1w</span></h2>
<p><span data-i18n="l2_shown">Shown:</span> <span id="l2count">-</span></p> <label class="row"><span class="lb" data-i18n="c_enabled">Enabled</span>
<table id="l2table"> <span class="switch"><input type="checkbox" id="stpen"><i></i></span></label>
<tr> <p class="small mono" id="stpids">-</p>
<th><span class="l2sort" onclick="l2SortBy('port')"><span data-i18n="l2_col_port">Port</span><span id="l2a_port" class="l2arrow"></span></span><br> <div id="stpbridge" style="display:flex;gap:14px;flex-wrap:wrap;align-items:flex-end;margin-top:10px">
<input id="l2f_port" class="l2filter" oninput="l2FilterChanged()" size="5"></th> <label><span class="small mut" data-i18n="stp_version">Version</span><br><select class="in" id="stpver"><option value="rstp">RSTP</option><option value="stp" data-i18n="stp_v_stp">STP compatible</option></select></label>
<th><span class="l2sort" onclick="l2SortBy('mac')">MAC<span id="l2a_mac" class="l2arrow"></span></span><br> <label><span class="small mut" data-i18n="stp_prio">Bridge priority</span><br><select class="in" id="stpprio"></select></label>
<input id="l2f_mac" class="l2filter" oninput="l2FilterChanged()" size="14"></th> <label><span class="small mut" data-i18n="stp_hello">Hello [s]</span><br><input class="in sm" id="stphello" type="number" min="1" max="10"></label>
<th><span class="l2sort" onclick="l2SortBy('vlan')">VLAN<span id="l2a_vlan" class="l2arrow"></span></span><br> <label><span class="small mut" data-i18n="stp_maxage">Max age [s]</span><br><input class="in sm" id="stpmaxage" type="number" min="6" max="40"></label>
<input id="l2f_vlan" class="l2filter" oninput="l2FilterChanged()" size="5"></th> <label><span class="small mut" data-i18n="stp_fwd">Forward delay [s]</span><br><input class="in sm" id="stpfwd" type="number" min="4" max="30"></label>
<th><span class="l2sort" onclick="l2SortBy('type')"><span data-i18n="l2_col_type">Type</span><span id="l2a_type" class="l2arrow"></span></span><br> <label><span class="small mut" data-i18n="stp_txhold">TX hold</span><br><input class="in sm" id="stptxhold" type="number" min="1" max="10"></label>
<input id="l2f_type" class="l2filter" oninput="l2FilterChanged()" size="8"></th>
<th data-i18n="l2_col_remove">Remove Entry</th></tr>
</table>
</div> </div>
<div style="margin-top:10px"><button class="ctl pri" id="stpbapply" data-i18n="stp_bridge_apply">Apply bridge settings</button></div>
<p class="small mut" style="margin-top:8px" data-i18n="stp_prio_note">Lower priority wins the root election (default 8 = 32768). Settings apply immediately; use Save to flash to keep them.</p>
</div>
<div class="card"><h2 data-i18n="stp_portcfg">Port configuration</h2>
<div class="scrollx"><table class="t" id="stpcfg">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="c_enabled">Enabled</th><th data-i18n="stp_edge">Edge</th><th data-i18n="stp_pcost">Path cost</th><th data-i18n="stp_pprio">Priority</th><th data-i18n="stp_guard">Guard</th><th data-i18n="stp_filter">BPDU filter</th><th data-i18n="stp_p2p">Point-to-point</th><th></th></tr></thead>
<tbody></tbody></table></div>
<p class="small mut" style="margin-top:8px" data-i18n="stp_port_note">Edge ports forward immediately and cause no topology change; Auto treats a port as edge after 3 s without BPDUs, and any received BPDU revokes it. Path cost 0 = automatic.</p>
</div>
<div class="card"><h2 data-i18n="stp_status">Port status</h2>
<div class="scrollx"><table class="t" id="stpstat">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="stp_role">Role</th><th data-i18n="c_state">State</th><th data-i18n="stp_db">Designated bridge</th><th data-i18n="stp_dp">Designated port</th><th data-i18n="stp_dc">Designated cost</th><th data-i18n="stp_oedge">Oper. edge</th><th data-i18n="stp_op2p">Oper. P2P</th></tr></thead>
<tbody></tbody></table></div>
</div>
</section>
<div class="page" id="page-stp" style="margin-left:16%;padding:1px 16px;height:1000px;"> <section class="tab" id="tab-stats">
<h1>Spanning Tree (RSTP)</h1> <div class="card"><h2><span data-i18n="st_title">Port statistics</span> <span class="hint" data-i18n="st_h">totals since boot</span></h2>
<h2>Mode: <select id="stpMode"><option value="off">Disabled</option><option value="on">Enabled</option></select> <div class="scrollx"><table class="t" id="stable">
<input style="width:15%;margin-left: 3em;" class="action" id="stp_sub" onclick="stpSub();" type="button" value="Apply"></h2> <thead><tr><th data-i18n="c_port">Port</th><th data-i18n="c_name">Name</th><th data-i18n="c_link">Link</th><th class="num" data-i18n="st_txg">TX good</th><th class="num" data-i18n="st_txb">TX bad</th><th class="num" data-i18n="st_rxg">RX good</th><th class="num" data-i18n="st_rxb">RX bad</th><th></th></tr></thead>
<div id="stpStat" style="font-family:monospace;"></div> <tbody></tbody></table></div>
<h2>Bridge settings</h2>
<table id="stpBridge">
<tr>
<th>Priority</th><th>Version</th><th>Hello [s]</th><th>Max age [s]</th><th>Fwd delay [s]</th><th>Tx hold</th>
</tr>
<tr>
<td><select id="bPrio"></select></td>
<td><select id="bVer"><option value="rstp">RSTP</option><option value="stp">STP compat</option></select></td>
<td><input id="bHello" type="number" min="1" max="10" style="width:4em"></td>
<td><input id="bMaxage" type="number" min="6" max="40" style="width:4em"></td>
<td><input id="bFwd" type="number" min="4" max="30" style="width:4em"></td>
<td><input id="bTxhold" type="number" min="1" max="10" style="width:4em"></td>
</tr>
</table>
<p style="font-size:small">Changes apply immediately. Edge ports skip the listen period; guard/filter act on received BPDUs.</p>
<h2>Port configuration</h2>
<table id="stpPortsTbl">
<tr>
<th>Port</th><th>State</th><th>Path Cost<br><span style="font-weight:normal;font-size:small">(0 = Auto)</span></th><th>Priority</th><th>Edge Port</th><th>BPDU Filter</th><th>Guard</th><th>Point-to-Point</th>
</tr>
</table>
<h2>Port status</h2>
<table id="stpStatTbl">
<tr>
<th>Port</th><th>Port State</th><th>Role</th><th>Designated Bridge</th><th>Designated Port ID</th><th>Designated Cost</th><th>Oper. Edge</th><th>Oper. P2P</th>
</tr>
</table>
</div> </div>
</section>
<div class="page" id="page-mirror" style="margin-left:16%;padding:1px 16px;height:1000px;"> <section class="tab" id="tab-vlan">
<h1 data-i18n="mirror_heading">Mirror Configuration</h1> <div class="card"><h2><span data-i18n="v_title">VLANs</span> <span class="hint"><span data-i18n="v_mgmt">Management VLAN</span>: <span id="vmgmtcur">-</span></span></h2>
<label class="tswitch"><span data-i18n="mirror_enabled">Enabled:</span> <input id="me" type="checkbox"></label><br/> <div class="scrollx"><table class="t" id="vtable">
<label for="mp"><span data-i18n="mirror_port">Mirroring Port:</span></label> <input type="number" id="mp" name="mp" min="1" max="9"/> <thead><tr><th data-i18n="v_vid">VID</th><th data-i18n="c_name">Name</th><th data-i18n="v_members">Members</th><th data-i18n="v_tagged">Tagged</th><th data-i18n="v_untagged">Untagged</th><th data-i18n="v_pvid_on">PVID on</th><th></th></tr></thead>
<h2 data-i18n="mirror_tx">Mirrored Ports (TX)</h2> <tbody></tbody></table></div>
<div id="mPortsTX"></div> <p class="small mut" id="vempty" style="display:none;margin-top:6px" data-i18n="v_empty">No VLANs configured.</p>
<br />
<h2 data-i18n="mirror_rx">Mirrored Ports (RX)</h2>
<div id="mPortsRX"></div>
<br/> <input style="width:15%;" class="action" id="mirror_sub" onclick="mirrorSub();" type="button" data-i18n="mirror_update" value="Update / Create">
<input style="width:15%;" class="action" id="mirror_del" onclick="mirrorDel();" type="button" data-i18n="mirror_disable" value="Disable Mirroring">
</div> </div>
<div class="card"><h2 data-i18n="v_editor">VLAN editor</h2>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:10px">
<label><span data-i18n="v_vid">VID</span> <input class="in sm" id="vvid" type="number" min="1" max="4094"></label>
<label><span data-i18n="c_name">Name</span> <input class="in" id="vname" size="12" maxlength="15" data-i18n-p="v_optional"></label>
<button class="ctl" id="vload" data-i18n="c_load">Load</button>
<button class="ctl pri" id="vapply" data-i18n="c_apply">Apply</button>
<button class="ctl" id="vmgmt" data-i18n="v_setmgmt" data-i18n-t="v_setmgmt_t">Set as mgmt VLAN</button>
</div>
<div class="scrollx"><table class="t" id="vedit"><tbody></tbody></table></div>
<p class="small mut" style="margin-top:6px" data-i18n="v_legend">U = untagged member, T = tagged member, - = not a member. PVID assigns this VLAN to untagged ingress traffic.</p>
</div>
<div class="card"><h2><span data-i18n="v_ingress">Ingress filtering</span> <span class="hint" data-i18n="v_ingress_h">write-only: the state is not readable from the switch</span></h2>
<div class="scrollx"><table class="t" id="ingress"><tbody></tbody></table></div>
<div style="margin-top:10px"><button class="ctl pri" id="ingapply" data-i18n="v_ing_apply">Apply ingress modes</button></div>
</div>
</section>
<div class="page" id="page-lag" style="margin-left:16%;padding:1px 16px;height:1000px;"> <section class="tab" id="tab-l2">
<h1 data-i18n="lag_heading">Link Aggregation Groups Configuration</h1> <div class="card"><h2><span data-i18n="l2_title">MAC address table</span>
<h2>LAG 1 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub0" onclick="lagSub(0);" type="button" data-i18n="lag_update" value="Update / Create"></h2> <span class="hint"><input class="in" id="l2filter" data-i18n-p="l2_filter" size="14"></span></h2>
<div id="mLAG0"></div> <div style="display:flex;gap:10px;margin-bottom:10px">
<br /> <button class="ctl" id="l2refresh" data-i18n="c_refresh">Refresh</button>
<h2>LAG 2 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub1" onclick="lagSub(1);" type="button" data-i18n="lag_update" value="Update / Create"></h2> <button class="ctl" id="l2flush" data-i18n="l2_flush">Flush learned entries</button>
<div id="mLAG1"></div> <span class="small mut" id="l2count" style="align-self:center"></span>
<br />
<h2>LAG 3 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub2" onclick="lagSub(2);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG2"></div>
<br />
<h2>LAG 4 <input style="width:15%;margin-left: 3em;" class="action" id="l_sub3" onclick="lagSub(3);" type="button" data-i18n="lag_update" value="Update / Create"></h2>
<div id="mLAG3"></div>
</div> </div>
<div class="scrollx"><table class="t" id="l2table">
<thead><tr><th data-sort="pport"><span data-i18n="c_port">Port</span><span class="arrow"></span></th><th data-sort="mac">MAC<span class="arrow"></span></th><th data-sort="vlan">VLAN<span class="arrow"></span></th><th data-sort="stat"><span data-i18n="c_type">Type</span><span class="arrow"></span></th><th></th></tr></thead>
<tbody></tbody></table></div>
</div>
</section>
<div class="page" id="page-eee" style="margin-left:16%;padding:1px 16px;height:1000px;"> <section class="tab" id="tab-mirror">
<h1 data-i18n="eee_heading">EEE Status</h1> <div class="card"><h2 data-i18n="m_title">Port mirroring</h2>
<table id="eeetable"> <div style="display:flex;gap:12px;align-items:center;margin-bottom:10px;flex-wrap:wrap">
<tr> <th> </th> <th colspan="3" data-i18n="eee_advertising"> Advertising </th> <th colspan="3" data-i18n="eee_partner">Link-Partner advertises</th> <th></th></tr> <span class="badge" id="mstate">off</span>
<tr> <th data-i18n="eee_port">Port</th> <th>2.5G</th> <th>1G</th> <th>100M</th> <th>2.5G</th> <th>1G</th> <th>100M</th> <th data-i18n="eee_active">Active?</th></tr> <label><span data-i18n="m_monitor">Monitor port</span> <select class="in" id="mport"></select></label>
</table> <button class="ctl pri" id="mapply" data-i18n="c_apply">Apply</button>
<div> <button class="ctl" id="moff" data-i18n="c_disable">Disable</button>
<input style="width:20%;" class="action" id="eee_enable" onclick="eeeSub(0, 1);" type="button" data-i18n="eee_enable" value="Enable EEE">
<input style="width:20%;" class="action" id="eee_disable" onclick="eeeSub(0, 0);" type="button" data-i18n="eee_disable" value="Disable EEE">
</div> </div>
<div class="scrollx"><table class="t" id="mtable"><tbody></tbody></table></div>
<p class="small mut" style="margin-top:6px" data-i18n="m_note">Both = mirror RX and TX of the port to the monitor port.</p>
</div> </div>
</section>
<div class="page" id="page-bandwidth" style="margin-left:16%;padding:1px 16px;height:1000px;"> <section class="tab" id="tab-lag">
<h1 data-i18n="bw_heading">Ingress and Egress Bandwidth</h1> <div id="lagwrap" class="grid2"></div>
<table id="bwtable"> <p class="small mut" style="margin-top:10px" data-i18n="lag_note">A LAG needs at least one member to be saved in the startup config; applying an empty group clears it.</p>
<tr> <th> </th> <th colspan="3" data-i18n="bw_ingress"> Ingress </th> <th colspan="2" data-i18n="bw_egress">Egress</th> <th></th></tr> </section>
<tr> <th data-i18n="bw_col_port">Port</th> <th data-i18n="bw_col_limit">Limit</th> <th data-i18n="bw_col_bandwidth">Bandwidth [kBit/s]</th> <th data-i18n="bw_col_flow">Flow Control</th> <th data-i18n="bw_col_limit">Limit</th> <th data-i18n="bw_col_bandwidth">Bandwidth [kBit/s]</th> <th data-i18n="bw_col_apply">Apply</th></tr>
</table>
</div>
<div class="page" id="page-system" style="margin-left:16%;padding:1px 16px;height:1000px;"> <section class="tab" id="tab-eee">
<div class="tab-bar"> <div class="card"><h2 data-i18n="e_title">Energy Efficient Ethernet</h2>
<button class="tab-btn active" onclick="openTab(event, 'system-tab')" data-i18n="sys_tab_system">System</button> <div class="scrollx"><table class="t" id="etable">
<button class="tab-btn" onclick="openTab(event, 'advanced-tab')" data-i18n="sys_tab_advanced">Advanced</button> <thead><tr><th data-i18n="c_port">Port</th><th data-i18n="e_adv">Advertised</th><th data-i18n="e_lp">Link partner</th><th data-i18n="e_active">Active</th><th data-i18n="e_enable">Enable</th></tr></thead>
<button class="tab-btn" onclick="openTab(event, 'console-tab')" data-i18n="sys_tab_console">Console</button> <tbody></tbody></table></div>
<p class="small mut" style="margin-top:8px" data-i18n="e_note">Advertised and link-partner flags per speed: 100M, 1G, 2.5G. SFP ports do not support EEE.</p>
</div> </div>
<div id="system-tab" class="tab-content active"> </section>
<h1 data-i18n="sys_heading">System Settings</h1>
<div class="row"> <section class="tab" id="tab-bw">
<div class="lcol"> <label for="hostname" data-i18n="sys_hostname">Hostname:</label></div> <div class="card"><h2><span data-i18n="bw_title">Bandwidth limits</span> <span class="hint" data-i18n="bw_h">Mbit/s, 0.016-10000</span></h2>
<div class="rcol"> <input id="hostname" type="text" maxlength="23" size="20"/> <div class="scrollx"><table class="t" id="btable">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="bw_in">Ingress limit</th><th>Mbit/s</th><th data-i18n="bw_exceed">When exceeded</th><th data-i18n="bw_out">Egress limit</th><th>Mbit/s</th><th></th></tr></thead>
<tbody></tbody></table></div>
</div>
</section>
<section class="tab" id="tab-system">
<div class="grid2">
<div class="card"><h2 data-i18n="sy_network">Network</h2>
<label class="row"><span class="lb" data-i18n="i_host">Hostname</span><input class="in" id="sy-host" maxlength="23"></label>
<label class="row"><span class="lb" data-i18n="i_ip">IP address</span><input class="in" id="sy-ip" size="15"></label>
<label class="row"><span class="lb" data-i18n="i_mask">Netmask</span><input class="in" id="sy-mask" size="15"></label>
<label class="row"><span class="lb" data-i18n="i_gw">Gateway</span><input class="in" id="sy-gw" size="15"></label>
<div style="display:flex;gap:10px;margin-top:12px">
<button class="ctl pri" id="sy-apply" data-i18n="c_apply">Apply</button>
<button class="ctl" id="sy-dhcp" data-i18n="sy_dhcp" data-i18n-t="sy_dhcp_t">Use DHCP</button>
</div> </div>
</div> </div>
<div class="row"> <div class="card"><h2 data-i18n="sy_services">Services</h2>
<div class="lcol"> <label for="ip" data-i18n="sys_ip">IP address:</label></div> <label class="row" style="justify-content:space-between"><span data-i18n="sy_igmp">IGMP snooping</span>
<div class="rcol"> <input id="ip" type="text" size="15"/> <span class="switch"><input type="checkbox" id="sy-igmp"><i></i></span></label>
<label class="row" style="justify-content:space-between"><span data-i18n="i_syslog">Syslog</span>
<span class="switch"><input type="checkbox" id="sy-syslog"><i></i></span></label>
<div class="row" style="display:flex;gap:8px;align-items:center;margin:-2px 0 10px">
<span class="small mut" data-i18n="sy_server">Server</span>
<input class="in" id="sy-sysip" style="flex:1;min-width:0;width:0" data-i18n-p="sy_sysip">
<span class="small mut" data-i18n="sy_port">Port</span>
<input class="in" id="sy-sysport" style="width:64px" type="number" min="1" max="65535"></div>
<p class="small mut" data-i18n="sy_services_note">Service state reflects the startup config; runtime state is not readable.</p>
</div> </div>
<div class="card"><h2 data-i18n="sy_password">Admin password</h2>
<label class="row"><span class="lb" data-i18n="sy_newpw">New password</span><input class="in" id="sy-pw1" type="password" maxlength="20"></label>
<label class="row"><span class="lb" data-i18n="sy_repeat">Repeat</span><input class="in" id="sy-pw2" type="password" maxlength="20"></label>
<button class="ctl pri" id="sy-pwapply" style="margin-top:8px" data-i18n="sy_pwapply">Change password</button>
<p class="small mut" style="margin-top:8px" data-i18n="sy_pw_note">Takes effect immediately; save to flash to keep it after reboot.</p>
</div> </div>
<div class="row"> <div class="card"><h2 data-i18n="sy_console">Console</h2>
<div class="lcol"> <label for="netmask" data-i18n="sys_netmask">Netmask:</label></div> <div style="display:flex;gap:8px">
<div class="rcol"> <input id="netmask" type="text" size="15"/> <input class="in" id="sy-cmd" style="flex:1" data-i18n-p="sy_cmd" spellcheck="false">
<button class="ctl pri" id="sy-send" data-i18n="sy_send">Send</button>
</div> </div>
<pre class="cfg" id="sy-cout" style="margin-top:10px;min-height:70px;max-height:180px;overflow:auto"></pre>
</div> </div>
<div class="row"> <div class="card"><h2 data-i18n="sy_display">Display</h2>
<div class="lcol"> <label for="gw" data-i18n="sys_gateway">Gateway:</label></div> <label class="row"><span class="lb" data-i18n="sy_lang">Language</span>
<div class="rcol"> <input id="gw" type="text" size="15"/> <select class="in" id="langSel">
</div>
</div>
<div class="row">
<div class="lcol"> <label for="model" data-i18n="sys_model">Model:</label></div>
<div class="rcol"> <span id="model">-</span>
</div>
</div>
<div class="row">
<div class="lcol"> <label for="lang-select" data-i18n="sys_language">Language:</label></div>
<div class="rcol">
<select id="lang-select" onchange="changeLang()">
<option value="en">English</option> <option value="en">English</option>
<option value="ja">日本語</option> <option value="ja">日本語</option>
<option value="zh">中文</option> <option value="zh">中文</option>
</select> </select></label>
<label class="row"><span class="lb" data-i18n="sy_theme">Theme</span>
<select class="in" id="themeSel">
<option value="auto" data-i18n="th_auto">System</option>
<option value="auto-sel" data-i18n="th_auto_sel">System (Selenized)</option>
<option value="light" data-i18n="th_light">Light</option>
<option value="dark" data-i18n="th_dark">Dark</option>
<option value="sel-light" data-i18n="th_sel_light">Selenized Light</option>
<option value="sel-dark" data-i18n="th_sel_dark">Selenized Dark</option>
</select></label>
<p class="small mut" data-i18n="sy_display_note">Stored in this browser only.</p>
</div> </div>
</div> </div>
<div class="row"> <div class="card" style="margin-top:16px"><h2><span data-i18n="sy_startup">Startup configuration</span>
<div class="lcol"> <label for="mgmtvlan" data-i18n="sys_mgmt_vlan">Management VLAN:</label></div> <span class="hint"><span id="cfgbytes"></span>, <span data-i18n="sy_replayed">replayed on every boot</span></span></h2>
<div class="rcol"> <textarea class="cfg" id="cfgedit" spellcheck="false"></textarea>
<select id="mgmtvlan" onchange="mgmtVlanChanged()"> <div style="display:flex;gap:10px;margin-top:10px;flex-wrap:wrap">
</select> <button class="ctl" id="cfgreload" data-i18n="sy_reload">Reload from flash</button>
<button class="ctl pri" id="cfgwrite" data-i18n="sy_write">Write to flash</button>
<span class="small mut" style="align-self:center" data-i18n="sy_unknown_note">Unknown lines are highlighted before writing.</span>
</div> </div>
</div> </div>
<br/> <div class="card dangerzone" style="margin-top:16px"><h2 data-i18n="sy_maint">Maintenance</h2>
<span data-i18n="sys_ip_note">When updating the above settings, remember to point your browser to the new IP afterwards:</span><br/> <button class="ctl danger" id="sy-reboot" data-i18n="sy_reboot">Reboot switch</button>
<input style="width:40%;" class="action" id="ip_sub" onclick="ipSub();" type="button" data-i18n="sys_update" value="Update Settings"><br/> </div>
<br/> </section>
<span data-i18n="sys_save_label">Save all current settings to Flash:</span><br/>
<input style="width:40%;" class="action" id="flash_sub" onclick="flashSave();" type="button" data-i18n="sys_save" value="Save Settings to Flash"> <section class="tab" id="tab-fw">
<div class="card"><h2 data-i18n="fw_title">Firmware update</h2>
<p class="small mut" style="margin-bottom:12px" data-i18n="fw_intro">Upload an RTLPlayground image (512 KiB .bin). The image is staged to flash and CRC-checked; on success the switch reboots itself, re-verifies the image and applies it. The startup configuration is preserved.</p>
<input type="file" id="fwfile" accept=".bin">
<div id="fwinfo" class="small" style="margin:10px 0"></div>
<div style="display:flex;gap:10px;align-items:center">
<button class="ctl pri" id="fwup" disabled data-i18n="fw_upload">Upload</button>
<progress id="fwprog" max="100" value="0" style="display:none;width:220px"></progress>
<span class="small" id="fwstat"></span>
</div>
</div>
</section>
</main>
</div> </div>
<div id="advanced-tab" class="tab-content"> <div id="toasts"></div>
<h1 data-i18n="sys_advanced">Advanced Settings</h1> <div id="mback"><div id="modal">
<div class="lcol"> <label for="config_display" data-i18n="sys_startup_config">Startup configuration:</label></div> <div id="mhead"><span id="mtitle"></span><button id="mx">&#x2715;</button></div>
<textarea id="config_display" rows="8" cols="60"></textarea> <div id="mbody"></div><div id="mfoot"></div>
<br/><br/> </div></div>
<span data-i18n="sys_startup_warn">Be careful when saving the directly edited startup configuration, you can lock yourself out:</span><br/>
<input style="width:40%;" class="action" id="clear_config" onclick="clearConfig();" type="button" data-i18n="sys_clear_config" value="Clear Startup Config">
<br/>
<input style="width:40%;" class="action" id="flash_startup_sub" onclick="flashStartupSave();" type="button" data-i18n="sys_save_startup" value="Save Startup Settings to Flash">
<br/>
<input style="width:40%;" class="action" id="switch_reset" onclick="resetSwitch();" type="button" data-i18n="sys_reset" value="Reset Switch">
</div>
<div id="console-tab" class="tab-content"> <script src="/app.js"></script>
<h1 data-i18n="sys_console">Console Command</h1>
<label for="console_command" data-i18n="sys_enter_cmd">Enter command:</label>
<input type="text" id="console_cmd" name="console_cmd" style="width:40%;">
<input style="width:20%;" class="action" id="cmd_sub" onclick="cmdSub();" type="button" data-i18n="sys_send_cmd" value="Send Command"><br/>
<br/><br/>
<span data-i18n="sys_console_warn">Be careful when entering console commands, you can lock yourself out!</span><br/>
<br/>
<pre id="console_out" style="height:22em;overflow:auto;white-space:pre-wrap;border:1px solid;padding:0.5em;"></pre>
</div>
</div>
<div class="page" id="page-update" style="margin-left:16%;padding:1px 16px;height:1000px;width:40%;">
<h1 data-i18n="update_heading">Firmware Update</h1>
<form enctype="multipart/form-data" action="/upload" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="1000000" />
<span data-i18n="update_instruction">Choose a firmware update file to upload:</span> <br/> <br/>
<input name="uploadedfile" type="file" accept=".bin" /><br />
<input style="margin-top:3em" type="submit" data-i18n="update_upload" value="Upload File" />
</form>
</div>
</body> </body>
</html> </html>
+92 -21
View File
@@ -1,30 +1,101 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<title data-i18n="login_title">RTL Switch Login</title> <head>
<link rel="stylesheet" href="style.css"> <meta charset="utf-8">
<script src="/main.js"></script> <meta name="viewport" content="width=device-width,initial-scale=1">
<script> <title>Switch Login</title>
function removeNote() { <style>
document.getElementById("incorrect").innerHTML = ""; :root{
--bg:#f4f6f8;--panel:#ffffff;--bd:#d7dde4;--fg:#1a222c;--fg2:#5c6773;
--ac:#0969da;--acfg:#ffffff;--bad:#cf222e;
} }
window.addEventListener("load", function() { :root[data-theme="dark"]{
if (document.referrer.endsWith("login.html")) --bg:#0e1116;--panel:#161b22;--bd:#2c333c;--fg:#e6edf3;--fg2:#8d96a0;
document.getElementById("incorrect").innerHTML = t('login_wrong'); --ac:#4c8ef7;--acfg:#ffffff;--bad:#f85149;
}); }
:root[data-theme="sel-dark"]{
--bg:#103c48;--panel:#184956;--bd:#2d5b69;--fg:#adbcbc;--fg2:#72898f;
--ac:#4695f7;--acfg:#ffffff;--bad:#fa5750;
}
:root[data-theme="sel-light"]{
--bg:#fbf3db;--panel:#ece3cc;--bd:#d5cdb6;--fg:#53676d;--fg2:#909995;
--ac:#0072d4;--acfg:#ffffff;--bad:#d2212d;
}
*{box-sizing:border-box;margin:0}
body{
font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
background:var(--bg);color:var(--fg);
min-height:100vh;display:flex;align-items:center;justify-content:center;
}
.card{
background:var(--panel);border:1px solid var(--bd);border-radius:12px;
padding:36px 32px;width:min(360px,92vw);
box-shadow:0 8px 30px rgba(0,0,0,.12);
}
.logo{display:flex;align-items:center;gap:10px;margin-bottom:6px}
h1{font-size:19px;font-weight:650}
p.sub{color:var(--fg2);font-size:13px;margin-bottom:24px}
label{display:block;font-size:13px;color:var(--fg2);margin-bottom:6px}
input[type=password]{
width:100%;padding:10px 12px;font-size:15px;color:var(--fg);
background:var(--bg);border:1px solid var(--bd);border-radius:8px;outline:none;
}
input[type=password]:focus{border-color:var(--ac)}
button{
width:100%;margin-top:18px;padding:10px;font-size:15px;font-weight:600;
color:var(--acfg);background:var(--ac);border:0;border-radius:8px;cursor:pointer;
}
button:hover{filter:brightness(1.1)}
#err{color:var(--bad);font-size:13px;min-height:20px;margin-top:14px;text-align:center}
</style>
<script>
(function(){
var pref;
try{pref=localStorage.getItem("theme")||"auto";}catch(e){pref="auto";}
var dark=window.matchMedia("(prefers-color-scheme: dark)").matches;
var t=pref;
if(pref==="auto")t=dark?"dark":"light";
if(pref==="auto-sel")t=dark?"sel-dark":"sel-light";
document.documentElement.dataset.theme=t;
})();
</script> </script>
</head> </head>
<body>
<body class="login_page"> <div class="card">
<div class = "center"> <div class="logo">
<h1 data-i18n="login_heading"> RTL Switch Login</h1> <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="var(--ac)" stroke-width="2">
<form method="post" action="login"> <rect x="2" y="7" width="20" height="10" rx="2"/>
<div class="txt_field"> <path d="M6 11v2M10 11v2M14 11v2M18 11v2"/>
<input name="pwd" type="password" autocomplete="current-password" onclick="removeNote()" required /> </svg>
<span></span> <h1 id="hn">Switch Login</h1>
<label data-i18n="login_password">Password</label>
</div> </div>
<input type="submit" data-i18n="login_login" value="Login"/> <p class="sub" id="sub">RTLPlayground management interface</p>
<h3 id="incorrect" style="margin-top: 5em;"></h3> <form method="post" action="login">
<label for="pwd" id="lp">Password</label>
<input id="pwd" name="pwd" type="password" autocomplete="current-password" autofocus required
oninput="document.getElementById('err').textContent=''">
<button type="submit" id="lb">Sign in</button>
<div id="err"></div>
</form> </form>
</div>
<script>
(function(){
var L={
en:["Switch Login","RTLPlayground management interface","Password","Sign in","Wrong password, try again."],
ja:["スイッチ ログイン","RTLPlayground 管理インターフェース","パスワード","ログイン","パスワードが違います。もう一度お試しください。"],
zh:["交换机登录","RTLPlayground 管理界面","密码","登录","密码错误,请重试。"]
};
var l=null;
try{l=localStorage.getItem("rtl_lang");}catch(e){}
if(!L[l])l=(navigator.language||"en").slice(0,2);
var s=L[l]||L.en;
document.title=s[0];
document.getElementById("hn").textContent=s[0];
document.getElementById("sub").textContent=s[1];
document.getElementById("lp").textContent=s[2];
document.getElementById("lb").textContent=s[3];
if(document.referrer.endsWith("login.html"))document.getElementById("err").textContent=s[4];
})();
</script>
</body> </body>
</html> </html>
-2862
View File
File diff suppressed because it is too large. Load diff
-26
View File
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
<g id="layer1" transform="translate(-314.07 -485.06)">
<g id="g1" transform="translate(256.16 417.12)">
<rect id="r1" style="stroke:#0a0a0a;stroke-linecap:square;stroke-width:4;fill:#ffffff" rx="4.514" ry="4.514" height="90.625" width="117.88" y="69.93" x="59.896"/>
<g id="g2"> <g id="g3">
<path class="bg" id="rect3787"
d="m73.958 75.66h89.41c2.3083 0 4.1667 1.8583 4.1667 4.1667l0.00028 52.653h-21.742v9.414h-8.694l0.00004 12.586h-36.871l-0.00004-12.586h-8.694v-9.414h-21.742l-0.000336-52.653c-0.000011-2.3083 1.8583-4.1667 4.1667-4.1667z"
style="stroke:#0a0a0a;stroke-linecap:square;stroke-width:2.5;fill:#dcdcdc"/>
<g id="g4" transform="translate(-0.368)" style="stroke:#191919;stroke-linecap:square;fill:#666666">
<rect id="r2" height="22.786" width="3.9062" y="75.747" x="80.556"/>
<rect id="r3" height="22.786" width="3.9062" y="75.747" x="153.6" />
<rect id="r4" height="22.786" width="3.9062" y="75.747" x="132.73"/>
<rect id="r5" height="22.786" width="3.9062" y="75.747" x="122.3"/>
<rect id="r6" height="22.786" width="3.9062" y="75.747" x="111.86"/>
<rect id="r7" height="22.786" width="3.9062" y="75.747" x="101.43"/>
<rect id="r8" height="22.786" width="3.9062" y="75.747" x="90.99"/>
<rect id="r9" height="22.786" width="3.9062" y="75.747" x="143.17"/>
</g>
</g>
<rect class="led" id="r10" style="stroke:#0a0a0a;stroke-linecap:square;stroke-width:2;fill:#1dfe0a" height="13.89" width="17.1" y="137.03" x="69.542"/>
<rect class="led" id="r11" style="stroke:#0a0a0a;stroke-linecap:square;stroke-width:2;fill:#ffd10a" height="13.89" width="17.1" y="137.03" x="150.68"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-24
View File
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<g id="layer1" transform="translate(-41.68,-55.95)">
<g id="g1" transform="translate(-177.45,-12.07)">
<g id="g2" transform="matrix(1.46,0,0,1.44,51.31,-317.4)">
<path id="p1"
d="m 117.94442,272.67478 h 7.64646 c 0.21062,0 0.38018,0.16649 0.38018,0.3733 v 4.69239 c 0,0.2068 -0.16956,0.3733 -0.38018,0.3733 h -2.1027 v 0.31956 h -3.44117 v -0.31956 h -2.1027 c -0.21062,0 -0.38018,-0.16649 -0.38018,-0.3733 v -4.69239 c 0,-0.20681 0.16956,-0.3733 0.38018,-0.3733 z"
style="fill:#ffffff;stroke:#000000;stroke-width:0.4"/>
<path class="bg" id="p2"
d="m 118.36775,273.15632 h 6.79794 c 0.18725,0 0.338,0.14695 0.338,0.32946 v 4.14153 c 0,0.18252 -0.15074,0.32946 -0.338,0.32946 h -6.79794 c -0.18724,0 -0.338,-0.14695 -0.338,-0.32946 v -4.14153 c 0,-0.18252 0.15074,-0.32946 0.338,-0.32946 z"
style="fill:#000000;stroke-width:0.26" />
<path d="m 118.65,273.53 h 6.24 v 4.05 h -6.24 z" style="fill:#1a1a1a;stroke-width:0.26" id="p3" />
<path d="m 118.86,276.61 h 5.82" style="fill:#1a1a1a;stroke:#000000;stroke-width:0.26" id="p4" />
<path d="m 124.05,274.25 h 1.39 v 0.72 h -1.39 z" style="fill:#1a1a1a;stroke-width:0.26" id="p5" />
<path d="m 124.05,276 h 1.39 v 0.72 h -1.39 z" style="fill:#1a1a1a;stroke-width:0.26" id="p6" />
<path transform="matrix(0.26,0,0,0.26,116.9,272.11)" d="M 7.3648613,7.0152205 H 29.369861" style="opacity:0.61257;fill:#4d4d4d;stroke:#4d4d4d;filter:url(#filter17794-0)" id="p7" />
</g>
<g id="g3" style="opacity:1;fill:#37d733;" transform="matrix(0.93,0,0,0.92,62.18,-43.11)">
<circle class="led" id="c1" cx="176.86" cy="125.7" r="0.93"/>
<circle class="led" id="c2" r="0.93" cy="125.7" cx="180.84"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

-172
View File
@@ -1,172 +0,0 @@
h1, h2, h3 {
color: #226;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
width: 12%;
height: calc(100% - 50px);
position: fixed;
top: 50px;
overflow: auto;
}
li a {
background-color: #aaf;
display: block;
color: #000;
padding: 8px 16px;
text-decoration: none;
}
/* Change the link color on hover */
li a:hover {
background-color: #226;
color: white;
}
table, th, td {
border: 1px solid #226;
padding: 8px;
}
table {
border-collapse: collapse;
width: 60%;
}
td {
text-align: right;
}
input[type=submit] { padding: 8px 16px;background-color:#aaf;color:#000; margin-bottom: 2em;border-radius: 15px;width: 100%;}
input[type=submit]:hover { background-color: #226; color: white;}
input[type=file] { padding: 8px 16px;background-color:#aaf;color:#000; margin-bottom: 2em;border-radius: 15px;width: 60%;}
input[type=file]:hover { background-color: #226; color: white;}
b
button {padding: 8px; background-color:#99f; color:#000;border-radius: 15px;}
button:hover { background-color: #226; color: white;}
.action{padding: 8px 16px;margin-top: 2em;margin-right: 3em; background-color: #aaf;color: #000;border-radius: 15px; width: 100%;}
.action:hover { background-color: #226; color: white;}
/* Port selection inputs */
.psel {
position: absolute;
opacity: 0;
width: 0;
height: 0;
margin-top: 1em;
}
.psel + img {
cursor: pointer;
opacity: 0.4;
padding: 8px;
}
.psel:checked + img {
/* outline: 2px solid #f00;*/
opacity: 1.0;
}
object, img {
margin: 0.5em 0.5em;
}
.cbgroup {
display: inline-block;
text-align: center;
}
.cbgroup label {
display: block;
}
.disabled{ opacity: .4; background-color: #f88; color: #000}
.isSFP{ opacity: .4; background-color: #660;}
.isNOK{ color: #900;}
.isOK{ color: #090;}
.ip{padding:8px 16px;margin-bottom: 1em;margin-left: 1em}
.rotext{display:inline-block;padding:8px 16px;margin-bottom: 1em;margin-left: 1em}
.row {display: flex;}
.rcol {flex: 90%;}
.lcol {flex: 10%;}
/* Login page */
.login_page {margin: 0; padding: 0; background: #aaf; height: 100vh; overflow: hidden;}
.center{
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -40% );
width: 500px; height: 400px;
background: white;
border-radius: 2px;
}
.center h1 {
text-align: center;
border-bottom: 1px solid silver;
}
.center form {
padding: 0 60px;
box-sizing: border-box;
}
form .txt_field{
position: relative;
border-bottom: 2px solid #adadad;
margin: 30px 0;
}
.txt_field input { width: 100%; padding: 0 5px; height: 40px; font-size: 16px; border: none;
background: none;
outline: none;}
.txt_field label { position: absolute; top: 50%; left: 5px; color:#adadad; transform: translateY(-50%);
font-size: 16px; pointer-events: none;transition: .5s; }
.txt_field span::before{ content:''; position: absolute; top: 40px; left:0; width: 100%; height: 2px; background: #aaf;}
.txt_field input:focus ~ label,
.txt_field input:valid ~ label { top: -5px; color: #aaf; }
.tooltip {
position: relative; display: inline-block; cursor: pointer;
}
.tooltiptext {
visibility: hidden;
width: 260px;
background-color: #226;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px 0;
position: absolute;
z-index: 1;
top: 80%;
left: 50%;
margin-left: -130px;
}
.tooltiptext::after {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent black transparent;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
.tt_table {
width: 100%; font-size:90%;
}
.tt_table td {
text-align: left;
}
select { text-align-last: right; font-family: monospace}
option { direction: rtl; font-family: sans-serif}
#vlanTable td { text-align: left; }
.l2sort{cursor:pointer;user-select:none}
.l2sort:hover{text-decoration:underline}
.l2arrow{opacity:0.55;font-size:0.85em}
.l2filter{width:100%;box-sizing:border-box;font-weight:normal;font-size:0.9em}
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" height="100%" width="100%" version="1.1" viewBox="0 0 577 434.38">
<defs>
<radialGradient id="rG39" gradientUnits="userSpaceOnUse" cy="676.79" cx="508.65" gradientTransform="matrix(1,0,0,0.5704,-2,274.75)" r="86.402">
<stop stop-color="#000000" offset="0"/>
<stop stop-color="#000000" stop-opacity="0" offset="1"/>
</radialGradient>
<filter id="f39" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="2.8"/>
</filter>
</defs>
<g transform="translate(-101.235,-272.06)">
<path d="m420.75,672.71v-18.688l82.055-42.01,89.748,35.306v18.414l-93.012,43.848-78.792-36.871z" transform="matrix(3.115,0,0,3.115,-1188.5,-1524.8)" filter="url(#f39)" fill="url(#rG39)"/>
<path style="color:#000000;" fill="#3a68b0" d="M143.37,387.98,384.43,501.62,630.32,385.7,389.26,272.06z"/>
<path style="color:#000000;" fill="#5d9ce1" d="M384.43,501.62v141.21l245.89-115.92v-141.21z"/>
<path style="color:#000000;" fill="#1b3a88" d="M143.37,387.98,384.43,501.62v141.21l-241.06-113.64z"/>
<path fill-opacity="0.7" fill="#f2fdff" d="m422.47,315.78-38.514,5.6218-38.514,5.6218,16.367,5.3307-46.555,22.107,30.52,9.9405,46.555-22.107,16.367,5.3308,6.8691-15.929,6.9054-15.917z"/>
<path fill-opacity="0.7" fill="#f2fdff" d="m269.31,421.78,6.8691-15.929,6.8691-15.929,16.367,5.3308,46.555-22.107,30.52,9.9405-46.555,22.107,16.367,5.3307-38.514,5.6218-38.477,5.6336z"/>
<path fill-opacity="0.7" fill="#f2fdff" d="m507.05,352.03-38.514,5.6218-38.514,5.6218,16.367,5.3307-46.555,22.107,30.52,9.9405,46.555-22.107,16.367,5.3308,6.8691-15.929,6.9054-15.917z"/>
<path fill-opacity="0.7" fill="#f2fdff" d="m353.9,458.03,6.8691-15.929,6.8691-15.929,16.367,5.3308,46.555-22.107,30.52,9.9405-46.555,22.107,16.367,5.3307-38.514,5.6218-38.477,5.6336z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

+4 -12
View File
@@ -76,7 +76,6 @@ __xdata uint8_t fw_reset_pending;
__xdata char session_id[SESSION_ID_LENGTH + 1]; __xdata char session_id[SESSION_ID_LENGTH + 1];
__xdata uint8_t authenticated; __xdata uint8_t authenticated;
__xdata uint32_t now; __xdata uint32_t now;
__xdata uint8_t * __xdata timeptr;
__xdata uint32_t last_session_use; __xdata uint32_t last_session_use;
#define TSTATE_NONE 0 #define TSTATE_NONE 0
@@ -345,9 +344,10 @@ __xdata uint8_t *scan_header(__xdata uint8_t * __xdata p)
if (now - last_session_use > SESSION_TIMEOUT) { if (now - last_session_use > SESSION_TIMEOUT) {
dbg_string("Session expired\n"); dbg_string("Session expired\n");
} else { } else {
if (is_word_x(session, session_id)) if (is_word_x(session, session_id)) {
authenticated = 1; authenticated = 1;
else last_session_use = now;
} else
dbg_string("Invalid session cookie!\n"); dbg_string("Invalid session cookie!\n");
} }
} }
@@ -1011,18 +1011,10 @@ void httpd_appcall(void)
} else { } else {
dbg_string("Have entry, authenticated: "); dbg_byte(authenticated); dbg_char('\n'); dbg_string("Have entry, authenticated: "); dbg_byte(authenticated); dbg_char('\n');
if (!authenticated && !(f_data[entry].start == FDATA_START_login_html if (!authenticated && !(f_data[entry].start == FDATA_START_login_html
|| f_data[entry].start == FDATA_START_port_svg || f_data[entry].start == FDATA_START_favicon_ico)) {
|| f_data[entry].start == FDATA_START_sfp_svg
|| f_data[entry].start == FDATA_START_main_js
|| f_data[entry].start == FDATA_START_style_css)) {
send_to_login(); send_to_login();
goto do_send; goto do_send;
} }
// A web-page is actively accessed, we can reset session time-out
reg_read_m(RTL837X_REG_SEC_COUNTER);
timeptr = (uint8_t*)&last_session_use; // last_session_use is Little endian
timeptr[0] = sfr_data[3]; timeptr[1] = sfr_data[2]; timeptr[2] = sfr_data[1]; timeptr[3] = sfr_data[0];
slen = strtox(outbuf, "HTTP/1.1 200 OK\r\nContent-Type: "); slen = strtox(outbuf, "HTTP/1.1 200 OK\r\nContent-Type: ");
slen += strtox(outbuf + slen, mime_strings[f_data[entry].mime]); slen += strtox(outbuf + slen, mime_strings[f_data[entry].mime]);
/* 'unsafe-inline' is needed for the inline onclick handlers /* 'unsafe-inline' is needed for the inline onclick handlers
+1 -1
View File
@@ -724,7 +724,7 @@ void launch(struct Server *server)
send_counters(new_socket, port); send_counters(new_socket, port);
goto done; goto done;
} }
if (!authenticated && !(!strncmp(&buffer[4], "/login.html", 11) || !strncmp(&buffer[4], "/style.css", 10))) { if (!authenticated && !(!strncmp(&buffer[4], "/login.html", 11) || !strncmp(&buffer[4], "/favicon.ico", 12))) {
send_to_login(new_socket); send_to_login(new_socket);
goto done; goto done;
} }