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
+2446 -3529

No files matched your search

+3
View File
@@ -6,6 +6,9 @@
- Web UI
- 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
+66 -94
View File
@@ -1,135 +1,107 @@
# Supporting Multiple Languages in the Web UI
The firmware uses a client-side i18n approach
all translations are stored in a single JavaScript dictionary embedded in the firmware.
No server-side changes are needed.
The firmware uses a client-side i18n approach: all translations are stored
in one JavaScript dictionary embedded in the firmware. No server-side
changes are needed.
## 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`, ...)
- Language auto-detection (browser language → `localStorage` override)
- `t(key)` — look up a translated string
- `setLang(lang)` — switch language and update the page
- `applyTranslation(el)` — apply translation to one DOM element
- A `LANG` object with one sub-object per language (`en`, `ja`, `zh`)
- Language auto-detection (browser language, overridden by the
`rtl_lang` key in `localStorage`, which the language selector in the
System section writes)
- `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
### 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
var LANG = {
en: {
nav_overview: 'Overview',
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
},
var LANG={
en:{
nav_dash:"Dashboard",
...
},
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:
```js
+ "<option value='en'>English</option><option value='ja'>日本語</option>"
```html
<select class="ctl" id="langSel" data-i18n-t="sy_lang">
<option value="en">English</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
+ "<option value='en'>English</option><option value='ja'>日本語</option><option value='LANGCODE'>Native Name</option>"
```
The inline table there holds, per language: page title, subtitle,
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:
```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.
The detection normalises `navigator.language` to its first two
characters and selects that language if `LANG` has it, otherwise English.
## Two Translation Mechanisms
### (A) `data-i18n` attribute (declarative — for HTML)
Add `data-i18n="key_name"` to any HTML element. The English text goes in the element content as a fallback:
### (A) `data-i18n` attributes (declarative, for HTML)
```html
<h1 data-i18n="port_heading">Port Configuration</h1>
<input type="button" data-i18n="port_apply" value="Apply">
<option data-i18n="port_auto">Auto</option>
<title data-i18n="port_title">Port Configuration</title>
<h2 data-i18n="pt_title">Port configuration</h2>
<button class="ctl" data-i18n="c_apply" data-i18n-t="sy_dhcp_t">Apply</button>
<input class="in" data-i18n-p="l2_filter">
```
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">`
- `el.textContent` for `<option>`, `<title>`
- `el.innerHTML` for everything else
The English text stays in the markup as the fallback and as the source
of truth for what the key means.
### (B) `t('key')` call (imperative — for JavaScript strings)
When generating HTML or text in JavaScript, wrap translatable strings with `t()`:
### (B) `t("key")` calls (imperative, for JavaScript)
```js
td.appendChild(document.createTextNode(t('common_port') + i));
td.innerHTML = t('port_auto');
iHTML += "<tr><td>" + t('port_vendor') + "</td></tr>";
tr.insertCell().textContent=t("c_port")+" "+p;
toast(t("v_loaded",{n:vid}),"ok");
```
### Special Case: Link Speed Display
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.
Strings that are identical in every language (`10M`, `2.5G`, `MAC`,
`VLAN`, `CPU`, `RSTP`) are literals, not dictionary entries.
## Size Considerations
- `html/i18n.js` is embedded in the firmware filesystem (~14 KB for two languages)
- Each new language adds roughly the same number of bytes as the English dictionary (~3–4 KB)
- The firmware binary is padded to 512 KiB, so a few extra KB do not change the flash footprint
- 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"`)
- Each language adds roughly 3 KB to `app.js` before compression; the
build gzips the file, so the flash cost of a language is closer to 1 KB
- The largest embedded file must stay below 64 KB after compression
(`uint16_t` length in the file table); `app.js` with three languages
is about 29 KB compressed
## Build
No special flags are needed. The `html/` directory is embedded by `fileadder` during the build.
## 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>`).
No special flags are needed. `html/` is minified and embedded by the
normal build (see `webui-compression.md`).
+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
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
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.
the authentication gate and is self-contained (its own styles and
strings), so nothing else is served before login. Only these two
pages plus `app.js` and the favicon are embedded; the stylesheet and
the theme bootstrap are inline in `index.html`.
- All JavaScript (translation table, shared helpers, navigation and the
per-section code) lives in one `html/app.js` that the page loads as
its only script, so the browser fetches it once and gzip can compress
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,
`Z_BEST_COMPRESSION`) when it generates the file table
(`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
- 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
content (previously at `data_read + 1`), so the `strlen()`-based size
computation no longer depends on uninitialised buffer content.
+1844
View File
File diff suppressed because it is too large. Load diff
+421 -282
View File
@@ -1,310 +1,449 @@
<!DOCTYPE html>
<html>
<script src="/main.js"></script>
<link rel="stylesheet" href="style.css">
<title data-i18n="index_title">FreeSwitchOS Main Page</title>
<style>
.popup {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.popup-content {
background: #fff;
padding: 20px;
border-radius: 5px;
text-align: center;
height: 80%
}
.popup .popup-content {
overflow-y: scroll;
}
.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>
</head>
<body>
<nav id="sidebar"></nav>
<div id="ports" style="margin-left:16%;padding:1px 16px"></div>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Switch</title>
<style>
:root{
--bg:#f4f6f8;--panel:#ffffff;--panel2:#eaeef2;--bd:#d7dde4;--fg:#1a222c;--fg2:#5c6773;
--ac:#0969da;--acfg:#ffffff;--ok:#158744;--bad:#cf222e;--warn:#9a6700;--cy:#1b7c83;
--sel:rgba(9,105,218,.10);--shadow:0 2px 10px rgba(20,30,40,.07);
--s10:#b58900;--s100:#b58900;--s1000:#159744;--s2g5:#0d9488;--s5g:#0d9488;--s10g:#2563eb;
}
:root[data-theme="dark"]{
--bg:#0e1116;--panel:#161b22;--panel2:#1f262e;--bd:#2c333c;--fg:#e6edf3;--fg2:#8d96a0;
--ac:#4c8ef7;--acfg:#ffffff;--ok:#3fb950;--bad:#f85149;--warn:#d29922;--cy:#39c5cf;
--sel:rgba(76,142,247,.16);--shadow:0 2px 10px rgba(0,0,0,.35);
--s10:#d29922;--s100:#d29922;--s1000:#3fb950;--s2g5:#2dd4bf;--s5g:#2dd4bf;--s10g:#58a6ff;
}
:root[data-theme="sel-dark"]{
--bg:#103c48;--panel:#184956;--panel2:#2d5b69;--bd:#2d5b69;--fg:#adbcbc;--fg2:#72898f;
--ac:#4695f7;--acfg:#ffffff;--ok:#75b938;--bad:#fa5750;--warn:#dbb32d;--cy:#41c7b9;
--sel:rgba(70,149,247,.18);--shadow:0 2px 10px rgba(0,20,26,.35);
--s10:#dbb32d;--s100:#dbb32d;--s1000:#75b938;--s2g5:#41c7b9;--s5g:#41c7b9;--s10g:#4695f7;
}
:root[data-theme="sel-light"]{
--bg:#fbf3db;--panel:#ece3cc;--panel2:#d5cdb6;--bd:#d5cdb6;--fg:#53676d;--fg2:#909995;
--ac:#0072d4;--acfg:#ffffff;--ok:#489100;--bad:#d2212d;--warn:#ad8900;--cy:#009c8f;
--sel:rgba(0,114,212,.12);--shadow:0 2px 10px rgba(80,70,40,.12);
--s10:#ad8900;--s100:#ad8900;--s1000:#489100;--s2g5:#009c8f;--s5g:#009c8f;--s10g:#0072d4;
}
*{box-sizing:border-box;margin:0}
html,body{height:100%}
body{
font:14px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
background:var(--bg);color:var(--fg);display:flex;
}
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}
}
</style>
<script>
(function(){
var pref;
try{pref=localStorage.getItem("theme")||"auto";}catch(e){pref="auto";}
var dark=window.matchMedia("(prefers-color-scheme: dark)").matches;
var t=pref;
if(pref==="auto")t=dark?"dark":"light";
if(pref==="auto-sel")t=dark?"sel-dark":"sel-light";
document.documentElement.dataset.theme=t;
})();
</script>
</head>
<body>
<div class="page" id="page-overview" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="index_heading">Switch Configuration</h1>
<table id="infoTable">
<tr>
<th colspan="2" data-i18n="index_settings">Settings</th>
</tr>
<tbody>
</tbody>
</table>
</div>
<nav id="nav">
<div id="brand">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke-width="2">
<rect x="2" y="7" width="20" height="10" rx="2"/><path d="M6 11v2M10 11v2M14 11v2M18 11v2"/>
</svg>
<span id="brandname">Switch</span>
</div>
<ul id="navlist"></ul>
<div id="navfoot"><span id="fver"></span></div>
</nav>
<div class="page" id="page-ports" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="port_heading">Port Configuration</h1>
<table id="speedtable">
<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>
</table>
<h2 style="margin-top:3em" data-i18n="port_mtu_heading">Configure Maximum Frame Size (MTU) forwarded at Port</h2>
<table id="mtutable" style="margin-top:1em">
</table>
</div>
<div id="wrap">
<header id="top">
<button id="burger" title="Menu">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--fg)" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
</button>
<span id="ttitle"></span>
<span id="dirty" data-i18n="hdr_dirty" data-i18n-t="hdr_dirty_t">unsaved changes</span>
<button class="ctl pri" id="saveBtn" data-i18n="hdr_save" data-i18n-t="hdr_save_t">Save to flash</button>
</header>
<div class="page" id="page-stat" style="margin-left:16%;padding:1px 16px;height:1000px;">
<div id="popup" class="popup">
<div class="popup-content">
<h2 data-i18n="stat_detailed">Detailed Port Statistics</h2>
<div id="popup_text"></div>
<button id="closePopup" class="action" data-i18n="stat_close">Close</button>
<main>
<section class="tab act" id="tab-dash">
<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>
<div id="strip"></div>
</div>
<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>
<h1 data-i18n="stat_heading">Port Statistics</h1>
<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>
</section>
<div class="page" id="page-vlan" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="vlan_heading">VLAN Configuration</h1>
<form id="vform">
<div>
<label for="vlanSelect" data-i18n="vlan_select">VLAN Select:</label>
<select id="vlanSelect" style="margin: 0 0 0 8px">
<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 class="page" id="page-l2" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="l2_heading">L2 Configuration</h1>
<p><span data-i18n="l2_shown">Shown:</span> <span id="l2count">-</span></p>
<table id="l2table">
<tr>
<th><span class="l2sort" onclick="l2SortBy('port')"><span data-i18n="l2_col_port">Port</span><span id="l2a_port" class="l2arrow"></span></span><br>
<input id="l2f_port" class="l2filter" oninput="l2FilterChanged()" size="5"></th>
<th><span class="l2sort" onclick="l2SortBy('mac')">MAC<span id="l2a_mac" class="l2arrow"></span></span><br>
<input id="l2f_mac" class="l2filter" oninput="l2FilterChanged()" size="14"></th>
<th><span class="l2sort" onclick="l2SortBy('vlan')">VLAN<span id="l2a_vlan" class="l2arrow"></span></span><br>
<input id="l2f_vlan" class="l2filter" oninput="l2FilterChanged()" size="5"></th>
<th><span class="l2sort" onclick="l2SortBy('type')"><span data-i18n="l2_col_type">Type</span><span id="l2a_type" class="l2arrow"></span></span><br>
<input id="l2f_type" class="l2filter" oninput="l2FilterChanged()" size="8"></th>
<th data-i18n="l2_col_remove">Remove Entry</th></tr>
</table>
</div>
<div class="page" id="page-stp" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1>Spanning Tree (RSTP)</h1>
<h2>Mode: <select id="stpMode"><option value="off">Disabled</option><option value="on">Enabled</option></select>
<input style="width:15%;margin-left: 3em;" class="action" id="stp_sub" onclick="stpSub();" type="button" value="Apply"></h2>
<div id="stpStat" style="font-family:monospace;"></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 class="page" id="page-mirror" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="mirror_heading">Mirror Configuration</h1>
<label class="tswitch"><span data-i18n="mirror_enabled">Enabled:</span> <input id="me" type="checkbox"></label><br/>
<label for="mp"><span data-i18n="mirror_port">Mirroring Port:</span></label> <input type="number" id="mp" name="mp" min="1" max="9"/>
<h2 data-i18n="mirror_tx">Mirrored Ports (TX)</h2>
<div id="mPortsTX"></div>
<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 class="page" id="page-lag" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="lag_heading">Link Aggregation Groups Configuration</h1>
<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>
<div id="mLAG0"></div>
<br />
<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>
<div id="mLAG1"></div>
<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 class="page" id="page-eee" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="eee_heading">EEE Status</h1>
<table id="eeetable">
<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>
<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>
</table>
<div>
<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">
<section class="tab" id="tab-ports">
<div class="card"><h2 data-i18n="pt_title">Port configuration</h2>
<div class="scrollx"><table class="t" id="ptable">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="c_name">Name</th><th data-i18n="c_link">Link</th><th data-i18n="pt_conn">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>
<tbody></tbody></table></div>
<p class="small mut" style="margin-top:8px" data-i18n="pt_sfp_note">SFP speed is set per slot and is not saved in the startup config.</p>
</div>
</div>
</section>
<div class="page" id="page-bandwidth" style="margin-left:16%;padding:1px 16px;height:1000px;">
<h1 data-i18n="bw_heading">Ingress and Egress Bandwidth</h1>
<table id="bwtable">
<tr> <th> </th> <th colspan="3" data-i18n="bw_ingress"> Ingress </th> <th colspan="2" data-i18n="bw_egress">Egress</th> <th></th></tr>
<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;">
<div class="tab-bar">
<button class="tab-btn active" onclick="openTab(event, 'system-tab')" data-i18n="sys_tab_system">System</button>
<button class="tab-btn" onclick="openTab(event, 'advanced-tab')" data-i18n="sys_tab_advanced">Advanced</button>
<button class="tab-btn" onclick="openTab(event, 'console-tab')" data-i18n="sys_tab_console">Console</button>
<section class="tab" id="tab-stp">
<div class="card"><h2><span data-i18n="stp_title">Spanning tree</span> <span class="hint" data-i18n="stp_h">RSTP, 802.1w</span></h2>
<label class="row"><span class="lb" data-i18n="c_enabled">Enabled</span>
<span class="switch"><input type="checkbox" id="stpen"><i></i></span></label>
<p class="small mono" id="stpids">-</p>
<div id="stpbridge" style="display:flex;gap:14px;flex-wrap:wrap;align-items:flex-end;margin-top:10px">
<label><span class="small mut" data-i18n="stp_version">Version</span><br><select class="in" id="stpver"><option value="rstp">RSTP</option><option value="stp" data-i18n="stp_v_stp">STP compatible</option></select></label>
<label><span class="small mut" data-i18n="stp_prio">Bridge priority</span><br><select class="in" id="stpprio"></select></label>
<label><span class="small mut" data-i18n="stp_hello">Hello [s]</span><br><input class="in sm" id="stphello" type="number" min="1" max="10"></label>
<label><span class="small mut" data-i18n="stp_maxage">Max age [s]</span><br><input class="in sm" id="stpmaxage" type="number" min="6" max="40"></label>
<label><span class="small mut" data-i18n="stp_fwd">Forward delay [s]</span><br><input class="in sm" id="stpfwd" type="number" min="4" max="30"></label>
<label><span class="small mut" data-i18n="stp_txhold">TX hold</span><br><input class="in sm" id="stptxhold" type="number" min="1" max="10"></label>
</div>
<div style="margin-top:10px"><button class="ctl pri" id="stpbapply" data-i18n="stp_bridge_apply">Apply bridge settings</button></div>
<p class="small mut" style="margin-top:8px" data-i18n="stp_prio_note">Lower priority wins the root election (default 8 = 32768). Settings apply immediately; use Save to flash to keep them.</p>
</div>
<div id="system-tab" class="tab-content active">
<h1 data-i18n="sys_heading">System Settings</h1>
<div class="row">
<div class="lcol"> <label for="hostname" data-i18n="sys_hostname">Hostname:</label></div>
<div class="rcol"> <input id="hostname" type="text" maxlength="23" size="20"/>
<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>
<section class="tab" id="tab-stats">
<div class="card"><h2><span data-i18n="st_title">Port statistics</span> <span class="hint" data-i18n="st_h">totals since boot</span></h2>
<div class="scrollx"><table class="t" id="stable">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="c_name">Name</th><th data-i18n="c_link">Link</th><th class="num" data-i18n="st_txg">TX good</th><th class="num" data-i18n="st_txb">TX bad</th><th class="num" data-i18n="st_rxg">RX good</th><th class="num" data-i18n="st_rxb">RX bad</th><th></th></tr></thead>
<tbody></tbody></table></div>
</div>
</section>
<section class="tab" id="tab-vlan">
<div class="card"><h2><span data-i18n="v_title">VLANs</span> <span class="hint"><span data-i18n="v_mgmt">Management VLAN</span>: <span id="vmgmtcur">-</span></span></h2>
<div class="scrollx"><table class="t" id="vtable">
<thead><tr><th data-i18n="v_vid">VID</th><th data-i18n="c_name">Name</th><th data-i18n="v_members">Members</th><th data-i18n="v_tagged">Tagged</th><th data-i18n="v_untagged">Untagged</th><th data-i18n="v_pvid_on">PVID on</th><th></th></tr></thead>
<tbody></tbody></table></div>
<p class="small mut" id="vempty" style="display:none;margin-top:6px" data-i18n="v_empty">No VLANs configured.</p>
</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>
<section class="tab" id="tab-l2">
<div class="card"><h2><span data-i18n="l2_title">MAC address table</span>
<span class="hint"><input class="in" id="l2filter" data-i18n-p="l2_filter" size="14"></span></h2>
<div style="display:flex;gap:10px;margin-bottom:10px">
<button class="ctl" id="l2refresh" data-i18n="c_refresh">Refresh</button>
<button class="ctl" id="l2flush" data-i18n="l2_flush">Flush learned entries</button>
<span class="small mut" id="l2count" style="align-self:center"></span>
</div>
<div class="scrollx"><table class="t" id="l2table">
<thead><tr><th data-sort="pport"><span data-i18n="c_port">Port</span><span class="arrow"></span></th><th data-sort="mac">MAC<span class="arrow"></span></th><th data-sort="vlan">VLAN<span class="arrow"></span></th><th data-sort="stat"><span data-i18n="c_type">Type</span><span class="arrow"></span></th><th></th></tr></thead>
<tbody></tbody></table></div>
</div>
</section>
<section class="tab" id="tab-mirror">
<div class="card"><h2 data-i18n="m_title">Port mirroring</h2>
<div style="display:flex;gap:12px;align-items:center;margin-bottom:10px;flex-wrap:wrap">
<span class="badge" id="mstate">off</span>
<label><span data-i18n="m_monitor">Monitor port</span> <select class="in" id="mport"></select></label>
<button class="ctl pri" id="mapply" data-i18n="c_apply">Apply</button>
<button class="ctl" id="moff" data-i18n="c_disable">Disable</button>
</div>
<div class="scrollx"><table class="t" id="mtable"><tbody></tbody></table></div>
<p class="small mut" style="margin-top:6px" data-i18n="m_note">Both = mirror RX and TX of the port to the monitor port.</p>
</div>
</section>
<section class="tab" id="tab-lag">
<div id="lagwrap" class="grid2"></div>
<p class="small mut" style="margin-top:10px" data-i18n="lag_note">A LAG needs at least one member to be saved in the startup config; applying an empty group clears it.</p>
</section>
<section class="tab" id="tab-eee">
<div class="card"><h2 data-i18n="e_title">Energy Efficient Ethernet</h2>
<div class="scrollx"><table class="t" id="etable">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="e_adv">Advertised</th><th data-i18n="e_lp">Link partner</th><th data-i18n="e_active">Active</th><th data-i18n="e_enable">Enable</th></tr></thead>
<tbody></tbody></table></div>
<p class="small mut" style="margin-top:8px" data-i18n="e_note">Advertised and link-partner flags per speed: 100M, 1G, 2.5G. SFP ports do not support EEE.</p>
</div>
</section>
<section class="tab" id="tab-bw">
<div class="card"><h2><span data-i18n="bw_title">Bandwidth limits</span> <span class="hint" data-i18n="bw_h">Mbit/s, 0.016-10000</span></h2>
<div class="scrollx"><table class="t" id="btable">
<thead><tr><th data-i18n="c_port">Port</th><th data-i18n="bw_in">Ingress limit</th><th>Mbit/s</th><th data-i18n="bw_exceed">When exceeded</th><th data-i18n="bw_out">Egress limit</th><th>Mbit/s</th><th></th></tr></thead>
<tbody></tbody></table></div>
</div>
</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 class="row">
<div class="lcol"> <label for="ip" data-i18n="sys_ip">IP address:</label></div>
<div class="rcol"> <input id="ip" type="text" size="15"/>
</div>
<div class="card"><h2 data-i18n="sy_services">Services</h2>
<label class="row" style="justify-content:space-between"><span data-i18n="sy_igmp">IGMP snooping</span>
<span class="switch"><input type="checkbox" id="sy-igmp"><i></i></span></label>
<label class="row" style="justify-content:space-between"><span data-i18n="i_syslog">Syslog</span>
<span class="switch"><input type="checkbox" id="sy-syslog"><i></i></span></label>
<div class="row" style="display:flex;gap:8px;align-items:center;margin:-2px 0 10px">
<span class="small mut" data-i18n="sy_server">Server</span>
<input class="in" id="sy-sysip" style="flex:1;min-width:0;width:0" data-i18n-p="sy_sysip">
<span class="small mut" data-i18n="sy_port">Port</span>
<input class="in" id="sy-sysport" style="width:64px" type="number" min="1" max="65535"></div>
<p class="small mut" data-i18n="sy_services_note">Service state reflects the startup config; runtime state is not readable.</p>
</div>
<div class="row">
<div class="lcol"> <label for="netmask" data-i18n="sys_netmask">Netmask:</label></div>
<div class="rcol"> <input id="netmask" type="text" size="15"/>
</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 class="row">
<div class="lcol"> <label for="gw" data-i18n="sys_gateway">Gateway:</label></div>
<div class="rcol"> <input id="gw" type="text" size="15"/>
<div class="card"><h2 data-i18n="sy_console">Console</h2>
<div style="display:flex;gap:8px">
<input class="in" id="sy-cmd" style="flex:1" data-i18n-p="sy_cmd" spellcheck="false">
<button class="ctl pri" id="sy-send" data-i18n="sy_send">Send</button>
</div>
<pre class="cfg" id="sy-cout" style="margin-top:10px;min-height:70px;max-height:180px;overflow:auto"></pre>
</div>
<div class="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()">
<div class="card"><h2 data-i18n="sy_display">Display</h2>
<label class="row"><span class="lb" data-i18n="sy_lang">Language</span>
<select class="in" id="langSel">
<option value="en">English</option>
<option value="ja">日本語</option>
<option value="zh">中文</option>
</select>
</div>
</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 class="row">
<div class="lcol"> <label for="mgmtvlan" data-i18n="sys_mgmt_vlan">Management VLAN:</label></div>
<div class="rcol">
<select id="mgmtvlan" onchange="mgmtVlanChanged()">
</select>
</div>
</div>
<div class="card" style="margin-top:16px"><h2><span data-i18n="sy_startup">Startup configuration</span>
<span class="hint"><span id="cfgbytes"></span>, <span data-i18n="sy_replayed">replayed on every boot</span></span></h2>
<textarea class="cfg" id="cfgedit" spellcheck="false"></textarea>
<div style="display:flex;gap:10px;margin-top:10px;flex-wrap:wrap">
<button class="ctl" id="cfgreload" data-i18n="sy_reload">Reload from flash</button>
<button class="ctl pri" id="cfgwrite" data-i18n="sy_write">Write to flash</button>
<span class="small mut" style="align-self:center" data-i18n="sy_unknown_note">Unknown lines are highlighted before writing.</span>
</div>
<br/>
<span data-i18n="sys_ip_note">When updating the above settings, remember to point your browser to the new IP afterwards:</span><br/>
<input style="width:40%;" class="action" id="ip_sub" onclick="ipSub();" type="button" data-i18n="sys_update" value="Update Settings"><br/>
<br/>
<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">
</div>
<div id="advanced-tab" class="tab-content">
<h1 data-i18n="sys_advanced">Advanced Settings</h1>
<div class="lcol"> <label for="config_display" data-i18n="sys_startup_config">Startup configuration:</label></div>
<textarea id="config_display" rows="8" cols="60"></textarea>
<br/><br/>
<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 class="card dangerzone" style="margin-top:16px"><h2 data-i18n="sy_maint">Maintenance</h2>
<button class="ctl danger" id="sy-reboot" data-i18n="sy_reboot">Reboot switch</button>
</div>
</section>
<div id="console-tab" class="tab-content">
<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>
<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>
</div>
</section>
</main>
</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>
<div id="toasts"></div>
<div id="mback"><div id="modal">
<div id="mhead"><span id="mtitle"></span><button id="mx">&#x2715;</button></div>
<div id="mbody"></div><div id="mfoot"></div>
</div></div>
<script src="/app.js"></script>
</body>
</html>
+94 -23
View File
@@ -1,30 +1,101 @@
<!DOCTYPE html>
<html>
<title data-i18n="login_title">RTL Switch Login</title>
<link rel="stylesheet" href="style.css">
<script src="/main.js"></script>
<script>
function removeNote() {
document.getElementById("incorrect").innerHTML = "";
}
window.addEventListener("load", function() {
if (document.referrer.endsWith("login.html"))
document.getElementById("incorrect").innerHTML = t('login_wrong');
});
</script>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Switch Login</title>
<style>
:root{
--bg:#f4f6f8;--panel:#ffffff;--bd:#d7dde4;--fg:#1a222c;--fg2:#5c6773;
--ac:#0969da;--acfg:#ffffff;--bad:#cf222e;
}
:root[data-theme="dark"]{
--bg:#0e1116;--panel:#161b22;--bd:#2c333c;--fg:#e6edf3;--fg2:#8d96a0;
--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>
</head>
<body class="login_page">
<div class = "center">
<h1 data-i18n="login_heading"> RTL Switch Login</h1>
<form method="post" action="login">
<div class="txt_field">
<input name="pwd" type="password" autocomplete="current-password" onclick="removeNote()" required />
<span></span>
<label data-i18n="login_password">Password</label>
<body>
<div class="card">
<div class="logo">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="var(--ac)" stroke-width="2">
<rect x="2" y="7" width="20" height="10" rx="2"/>
<path d="M6 11v2M10 11v2M14 11v2M18 11v2"/>
</svg>
<h1 id="hn">Switch Login</h1>
</div>
<input type="submit" data-i18n="login_login" value="Login"/>
<h3 id="incorrect" style="margin-top: 5em;"></h3>
<p class="sub" id="sub">RTLPlayground management interface</p>
<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>
</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>
</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

+5 -13
View File
@@ -76,7 +76,6 @@ __xdata uint8_t fw_reset_pending;
__xdata char session_id[SESSION_ID_LENGTH + 1];
__xdata uint8_t authenticated;
__xdata uint32_t now;
__xdata uint8_t * __xdata timeptr;
__xdata uint32_t last_session_use;
#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) {
dbg_string("Session expired\n");
} else {
if (is_word_x(session, session_id))
if (is_word_x(session, session_id)) {
authenticated = 1;
else
last_session_use = now;
} else
dbg_string("Invalid session cookie!\n");
}
}
@@ -1010,19 +1010,11 @@ void httpd_appcall(void)
}
} else {
dbg_string("Have entry, authenticated: "); dbg_byte(authenticated); dbg_char('\n');
if (!authenticated && !(f_data[entry].start == FDATA_START_login_html
|| f_data[entry].start == FDATA_START_port_svg
|| f_data[entry].start == FDATA_START_sfp_svg
|| f_data[entry].start == FDATA_START_main_js
|| f_data[entry].start == FDATA_START_style_css)) {
if (!authenticated && !(f_data[entry].start == FDATA_START_login_html
|| f_data[entry].start == FDATA_START_favicon_ico)) {
send_to_login();
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 + slen, mime_strings[f_data[entry].mime]);
/* '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);
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);
goto done;
}