From 1854a1c960b8e6feeaff3477aeed36d97d0062ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Sep 2026 00:13:15 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20Sync=202026-09-28=2000:13:15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-app-fwlive/Makefile | 2 +- luci-app-fwlive/README.md | 3 +- .../luci-static/resources/fwlive/css.js | 2 +- .../luci-static/resources/fwlive/fwlive.css | 3 + .../luci-static/resources/fwlive/logging.js | 18 +- .../resources/view/status/fwlive.js | 49 +- luci-app-fwlive/po/de/luci-app-fwlive.po | 453 ++-- luci-app-fwlive/po/ru/luci-app-fwlive.po | 453 ++-- .../po/templates/luci-app-fwlive.pot | 390 +-- luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po | 450 ++-- .../root/lib/upgrade/keep.d/luci-app-fwlive | 3 + .../root/usr/libexec/fwlive-logging.sh | 11 +- luci-app-qosify/Makefile | 26 + .../luci-static/resources/view/qosify/main.js | 2354 +++++++++++++++++ .../resources/view/qosify/qosify.css | 398 +++ luci-app-qosify/po/cs/qosify.po | 768 ++++++ luci-app-qosify/po/de/qosify.po | 741 ++++++ luci-app-qosify/po/es/qosify.po | 791 ++++++ luci-app-qosify/po/ga/qosify.po | 787 ++++++ luci-app-qosify/po/ko/qosify.po | 732 +++++ luci-app-qosify/po/lo/qosify.po | 756 ++++++ luci-app-qosify/po/lt/qosify.po | 793 ++++++ luci-app-qosify/po/pl/qosify.po | 781 ++++++ luci-app-qosify/po/pt/qosify.po | 729 +++++ luci-app-qosify/po/pt_BR/qosify.po | 732 +++++ luci-app-qosify/po/ru/qosify.po | 785 ++++++ luci-app-qosify/po/sv/qosify.po | 772 ++++++ luci-app-qosify/po/ta/qosify.po | 732 +++++ luci-app-qosify/po/templates/qosify.pot | 1365 ++++++++++ luci-app-qosify/po/tr/qosify.po | 732 +++++ luci-app-qosify/po/uk/qosify.po | 733 +++++ luci-app-qosify/po/zh-cn | 1 + luci-app-qosify/po/zh_Hans/qosify.po | 750 ++++++ luci-app-qosify/po/zh_Hant/qosify.po | 733 +++++ .../share/luci/menu.d/luci-app-qosify.json | 14 + .../usr/share/qosify-luci/00-defaults.conf | 17 + .../root/usr/share/qosify-luci/cleanup | 137 + .../root/usr/share/qosify-luci/qosify | 48 + .../usr/share/rpcd/acl.d/luci-app-qosify.json | 37 + luci-app-sfp-info/Makefile | 17 + luci-app-sfp-info/README.md | 26 + .../resources/view/sfp-info/main.js | 206 ++ luci-app-sfp-info/po/cs/sfp-info.po | 125 + luci-app-sfp-info/po/de/sfp-info.po | 119 + luci-app-sfp-info/po/es/sfp-info.po | 125 + luci-app-sfp-info/po/ga/sfp-info.po | 126 + luci-app-sfp-info/po/ko/sfp-info.po | 119 + luci-app-sfp-info/po/lo/sfp-info.po | 125 + luci-app-sfp-info/po/lt/sfp-info.po | 120 + luci-app-sfp-info/po/pl/sfp-info.po | 126 + luci-app-sfp-info/po/pt/sfp-info.po | 116 + luci-app-sfp-info/po/pt_BR/sfp-info.po | 119 + luci-app-sfp-info/po/ru/sfp-info.po | 127 + luci-app-sfp-info/po/sv/sfp-info.po | 119 + luci-app-sfp-info/po/ta/sfp-info.po | 119 + luci-app-sfp-info/po/templates/sfp-info.pot | 110 + luci-app-sfp-info/po/tr/sfp-info.po | 119 + luci-app-sfp-info/po/uk/sfp-info.po | 120 + luci-app-sfp-info/po/zh-cn | 1 + luci-app-sfp-info/po/zh_Hans/sfp-info.po | 128 + luci-app-sfp-info/po/zh_Hant/sfp-info.po | 128 + .../share/luci/menu.d/luci-app-sfp-info.json | 13 + .../share/rpcd/acl.d/luci-app-sfp-info.json | 10 + .../root/usr/share/rpcd/ucode/sfp-info.uc | 286 ++ luci-app-shunt/Makefile | 16 + .../resources/view/shunt/logread.js | 4 + .../resources/view/shunt/logtemplate.js | 68 + .../resources/view/shunt/overview.js | 480 ++++ .../resources/view/shunt/policies.js | 223 ++ .../resources/view/shunt/setreport.js | 263 ++ luci-app-shunt/po/templates/shunt.pot | 601 +++++ .../95-luci-app-shunt-housekeeping | 4 + .../usr/share/luci/menu.d/luci-app-shunt.json | 54 + .../usr/share/rpcd/acl.d/luci-app-shunt.json | 47 + luci-theme-footstrap/Makefile | 241 +- luci-theme-footstrap/README.md | 67 - luci-theme-footstrap/build-apk.sh | 120 - luci-theme-footstrap/build-css.sh | 274 -- luci-theme-footstrap/dev-sync.sh | 146 - .../htdocs/luci-static/footstrap/cascade.css | 1068 ++++++++ .../luci-static/resources/fs-appearance.js | 137 +- .../htdocs/luci-static/resources/fs-assets.js | 78 +- .../htdocs/luci-static/resources/fs-axes.js | 238 +- .../htdocs/luci-static/resources/fs-chrome.js | 90 +- .../htdocs/luci-static/resources/fs-fit.js | 71 +- .../luci-static/resources/fs-menutree.js | 6 +- .../luci-static/resources/fs-overview.js | 63 +- .../htdocs/luci-static/resources/fs-prefs.js | 88 +- .../htdocs/luci-static/resources/fs-router.js | 201 +- .../htdocs/luci-static/resources/fs-search.js | 82 +- .../htdocs/luci-static/resources/fs-select.js | 53 +- .../htdocs/luci-static/resources/fs-sheets.js | 14 +- .../luci-static/resources/fs-version.js | 2 +- .../luci-static/resources/fs-widgets.js | 70 +- .../resources/menu-footstrap-common.js | 85 +- .../luci-static/resources/menu-footstrap.js | 58 +- luci-theme-footstrap/luci-upstream.pin | 81 - luci-theme-footstrap/mangle-tokens.sh | 205 -- luci-theme-footstrap/po/es/footstrap.po | 428 ++- luci-theme-footstrap/po/ru/footstrap.po | 425 ++- .../po/templates/footstrap.pot | 338 +-- .../etc/uci-defaults/30_luci-theme-footstrap | 13 +- luci-theme-footstrap/strip-assets.sh | 101 - luci-theme-footstrap/strip-probes.sh | 53 - luci-theme-footstrap/strip-shell.sh | 69 - luci-theme-footstrap/strip-templates.sh | 120 - luci-theme-footstrap/styles/00-header.css | 16 - luci-theme-footstrap/styles/02-tokens.css | 233 -- luci-theme-footstrap/styles/03-palettes.css | 282 -- luci-theme-footstrap/styles/04-nocolormix.css | 49 - luci-theme-footstrap/styles/base/10-reset.css | 97 - .../styles/base/20-typography.css | 153 -- luci-theme-footstrap/styles/base/30-forms.css | 220 -- .../styles/base/40-tables.css | 61 - .../styles/base/50-chrome.css | 126 - luci-theme-footstrap/styles/base/60-modal.css | 62 - .../styles/base/70-buttons.css | 72 - .../styles/base/90-widgets.css | 119 - luci-theme-footstrap/styles/base/95-luci.css | 630 ----- .../styles/pages/10-login.css | 66 - .../styles/pages/20-overview.css | 269 -- .../styles/pages/30-software.css | 39 - .../styles/pages/40-sshkeys.css | 14 - .../styles/pages/50-leases.css | 18 - .../styles/pages/70-syslog.css | 40 - .../styles/pages/80-appearance.css | 117 - .../styles/theme/10-chrome.css | 118 - .../styles/theme/15-wallpaper.css | 41 - .../styles/theme/16-login-bg.css | 14 - .../styles/theme/20-shell.css | 598 ----- .../styles/theme/25-progressbar.css | 89 - .../styles/theme/30-tables.css | 430 --- .../styles/theme/35-alerts.css | 130 - luci-theme-footstrap/styles/theme/40-tabs.css | 127 - luci-theme-footstrap/styles/theme/45-misc.css | 123 - .../styles/theme/50-toplayout.css | 48 - .../styles/theme/55-buttons.css | 247 -- .../styles/theme/60-inputs.css | 520 ---- .../styles/theme/65-dropdown.css | 377 --- .../styles/theme/70-modal.css | 42 - .../styles/theme/75-search.css | 80 - .../styles/theme/90-responsive.css | 89 - .../styles/theme/95-a11y-media.css | 74 - .../styles/theme/97-print.css | 30 - .../ucode/template/themes/footstrap/footer.ut | 36 +- .../ucode/template/themes/footstrap/header.ut | 54 +- .../themes/footstrap/partials/footer.ut | 35 + .../themes/footstrap/partials/head.ut | 61 +- .../template/themes/footstrap/sysauth.ut | 7 +- luci-theme-footstrap/update-po.sh | 116 - quickfile/Makefile | 4 +- 151 files changed, 25199 insertions(+), 9649 deletions(-) create mode 100644 luci-app-fwlive/root/lib/upgrade/keep.d/luci-app-fwlive create mode 100644 luci-app-qosify/Makefile create mode 100644 luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js create mode 100644 luci-app-qosify/htdocs/luci-static/resources/view/qosify/qosify.css create mode 100644 luci-app-qosify/po/cs/qosify.po create mode 100644 luci-app-qosify/po/de/qosify.po create mode 100644 luci-app-qosify/po/es/qosify.po create mode 100644 luci-app-qosify/po/ga/qosify.po create mode 100644 luci-app-qosify/po/ko/qosify.po create mode 100644 luci-app-qosify/po/lo/qosify.po create mode 100644 luci-app-qosify/po/lt/qosify.po create mode 100644 luci-app-qosify/po/pl/qosify.po create mode 100644 luci-app-qosify/po/pt/qosify.po create mode 100644 luci-app-qosify/po/pt_BR/qosify.po create mode 100644 luci-app-qosify/po/ru/qosify.po create mode 100644 luci-app-qosify/po/sv/qosify.po create mode 100644 luci-app-qosify/po/ta/qosify.po create mode 100644 luci-app-qosify/po/templates/qosify.pot create mode 100644 luci-app-qosify/po/tr/qosify.po create mode 100644 luci-app-qosify/po/uk/qosify.po create mode 120000 luci-app-qosify/po/zh-cn create mode 100644 luci-app-qosify/po/zh_Hans/qosify.po create mode 100644 luci-app-qosify/po/zh_Hant/qosify.po create mode 100644 luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json create mode 100644 luci-app-qosify/root/usr/share/qosify-luci/00-defaults.conf create mode 100755 luci-app-qosify/root/usr/share/qosify-luci/cleanup create mode 100644 luci-app-qosify/root/usr/share/qosify-luci/qosify create mode 100644 luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json create mode 100644 luci-app-sfp-info/Makefile create mode 100644 luci-app-sfp-info/README.md create mode 100644 luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js create mode 100644 luci-app-sfp-info/po/cs/sfp-info.po create mode 100644 luci-app-sfp-info/po/de/sfp-info.po create mode 100644 luci-app-sfp-info/po/es/sfp-info.po create mode 100644 luci-app-sfp-info/po/ga/sfp-info.po create mode 100644 luci-app-sfp-info/po/ko/sfp-info.po create mode 100644 luci-app-sfp-info/po/lo/sfp-info.po create mode 100644 luci-app-sfp-info/po/lt/sfp-info.po create mode 100644 luci-app-sfp-info/po/pl/sfp-info.po create mode 100644 luci-app-sfp-info/po/pt/sfp-info.po create mode 100644 luci-app-sfp-info/po/pt_BR/sfp-info.po create mode 100644 luci-app-sfp-info/po/ru/sfp-info.po create mode 100644 luci-app-sfp-info/po/sv/sfp-info.po create mode 100644 luci-app-sfp-info/po/ta/sfp-info.po create mode 100644 luci-app-sfp-info/po/templates/sfp-info.pot create mode 100644 luci-app-sfp-info/po/tr/sfp-info.po create mode 100644 luci-app-sfp-info/po/uk/sfp-info.po create mode 120000 luci-app-sfp-info/po/zh-cn create mode 100644 luci-app-sfp-info/po/zh_Hans/sfp-info.po create mode 100644 luci-app-sfp-info/po/zh_Hant/sfp-info.po create mode 100644 luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json create mode 100644 luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json create mode 100755 luci-app-sfp-info/root/usr/share/rpcd/ucode/sfp-info.uc create mode 100644 luci-app-shunt/Makefile create mode 100644 luci-app-shunt/htdocs/luci-static/resources/view/shunt/logread.js create mode 100644 luci-app-shunt/htdocs/luci-static/resources/view/shunt/logtemplate.js create mode 100644 luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js create mode 100644 luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js create mode 100644 luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js create mode 100644 luci-app-shunt/po/templates/shunt.pot create mode 100644 luci-app-shunt/root/etc/uci-defaults/95-luci-app-shunt-housekeeping create mode 100644 luci-app-shunt/root/usr/share/luci/menu.d/luci-app-shunt.json create mode 100644 luci-app-shunt/root/usr/share/rpcd/acl.d/luci-app-shunt.json delete mode 100644 luci-theme-footstrap/README.md delete mode 100755 luci-theme-footstrap/build-apk.sh delete mode 100755 luci-theme-footstrap/build-css.sh delete mode 100755 luci-theme-footstrap/dev-sync.sh create mode 100644 luci-theme-footstrap/htdocs/luci-static/footstrap/cascade.css delete mode 100644 luci-theme-footstrap/luci-upstream.pin delete mode 100755 luci-theme-footstrap/mangle-tokens.sh delete mode 100755 luci-theme-footstrap/strip-assets.sh delete mode 100755 luci-theme-footstrap/strip-probes.sh delete mode 100755 luci-theme-footstrap/strip-shell.sh delete mode 100755 luci-theme-footstrap/strip-templates.sh delete mode 100644 luci-theme-footstrap/styles/00-header.css delete mode 100644 luci-theme-footstrap/styles/02-tokens.css delete mode 100644 luci-theme-footstrap/styles/03-palettes.css delete mode 100644 luci-theme-footstrap/styles/04-nocolormix.css delete mode 100644 luci-theme-footstrap/styles/base/10-reset.css delete mode 100644 luci-theme-footstrap/styles/base/20-typography.css delete mode 100644 luci-theme-footstrap/styles/base/30-forms.css delete mode 100644 luci-theme-footstrap/styles/base/40-tables.css delete mode 100644 luci-theme-footstrap/styles/base/50-chrome.css delete mode 100644 luci-theme-footstrap/styles/base/60-modal.css delete mode 100644 luci-theme-footstrap/styles/base/70-buttons.css delete mode 100644 luci-theme-footstrap/styles/base/90-widgets.css delete mode 100644 luci-theme-footstrap/styles/base/95-luci.css delete mode 100644 luci-theme-footstrap/styles/pages/10-login.css delete mode 100644 luci-theme-footstrap/styles/pages/20-overview.css delete mode 100644 luci-theme-footstrap/styles/pages/30-software.css delete mode 100644 luci-theme-footstrap/styles/pages/40-sshkeys.css delete mode 100644 luci-theme-footstrap/styles/pages/50-leases.css delete mode 100644 luci-theme-footstrap/styles/pages/70-syslog.css delete mode 100644 luci-theme-footstrap/styles/pages/80-appearance.css delete mode 100644 luci-theme-footstrap/styles/theme/10-chrome.css delete mode 100644 luci-theme-footstrap/styles/theme/15-wallpaper.css delete mode 100644 luci-theme-footstrap/styles/theme/16-login-bg.css delete mode 100644 luci-theme-footstrap/styles/theme/20-shell.css delete mode 100644 luci-theme-footstrap/styles/theme/25-progressbar.css delete mode 100644 luci-theme-footstrap/styles/theme/30-tables.css delete mode 100644 luci-theme-footstrap/styles/theme/35-alerts.css delete mode 100644 luci-theme-footstrap/styles/theme/40-tabs.css delete mode 100644 luci-theme-footstrap/styles/theme/45-misc.css delete mode 100644 luci-theme-footstrap/styles/theme/50-toplayout.css delete mode 100644 luci-theme-footstrap/styles/theme/55-buttons.css delete mode 100644 luci-theme-footstrap/styles/theme/60-inputs.css delete mode 100644 luci-theme-footstrap/styles/theme/65-dropdown.css delete mode 100644 luci-theme-footstrap/styles/theme/70-modal.css delete mode 100644 luci-theme-footstrap/styles/theme/75-search.css delete mode 100644 luci-theme-footstrap/styles/theme/90-responsive.css delete mode 100644 luci-theme-footstrap/styles/theme/95-a11y-media.css delete mode 100644 luci-theme-footstrap/styles/theme/97-print.css create mode 100644 luci-theme-footstrap/ucode/template/themes/footstrap/partials/footer.ut delete mode 100755 luci-theme-footstrap/update-po.sh diff --git a/luci-app-fwlive/Makefile b/luci-app-fwlive/Makefile index eb01f21c6..972f926db 100644 --- a/luci-app-fwlive/Makefile +++ b/luci-app-fwlive/Makefile @@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter +coreutils-timeout LUCI_PKGARCH:=all PKG_VERSION:=0.1.46 -PKG_RELEASE:=43 +PKG_RELEASE:=44 # Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI). ifdef SOURCE_DATE_EPOCH diff --git a/luci-app-fwlive/README.md b/luci-app-fwlive/README.md index 63113f9b9..1b96206da 100644 --- a/luci-app-fwlive/README.md +++ b/luci-app-fwlive/README.md @@ -27,7 +27,8 @@ LuCI **Firewall Live View** — client-side JS view polling `ubus fwlive poll` ( | `root/usr/libexec/rpcd/fwlive` | rpcd plugin (`rules`, `poll`, `resolve`, `logging_status`, `enable_wan_logging`, `disable_wan_logging`) | | `root/usr/libexec/fwlive-adaptive-cap.sh` | Layer 1 adaptive poll cap (sourced by `rpcd/fwlive`) | | `root/usr/libexec/fwlive-logging.sh` | WAN zone logging helpers | -| `/etc/fwlive/wan-log-baseline` | Written on first **Enable logging**; restored on uninstall (`prerm`). Disable of a pre-existing/foreign log bit is not snapshotted, so uninstall will not restore that bit. | +| `root/lib/upgrade/keep.d/luci-app-fwlive` | Keep `/etc/fwlive/` across sysupgrade | +| `/etc/fwlive/wan-log-baseline` | Written on first **Enable logging** (and best-effort on already-on enable); restored on uninstall (`prerm`). Disable of a pre-existing/foreign log bit is not snapshotted, so uninstall will not restore that bit. | | `root/usr/libexec/fwlive-log-filter.sh` | Server-side firewall-only filter (`isFirewallEvent` parity) | | `root/usr/libexec/fwlive-is-firewall-event.sh` | Shared filter logic (sourced by filter + tests) | | `root/usr/libexec/fwlive-is-firewall-event.awk` | **Generated** standalone classifier from `CLASSIFY_SPEC`; loaded by `fwlive-is-firewall-event.sh` | diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/css.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/css.js index 279bf5119..b1be84bdc 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/css.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/css.js @@ -11,5 +11,5 @@ * GENERATED — do not edit. Edit fwlive.css and run: node scripts/embed-fwlive-css.js */ return baseclass.extend({ - styleText: "\n/* SPDX-License-Identifier: Apache-2.0 */\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-display-bar-label {\n\tflex: 0 0 auto;\n\tfont-weight: 600;\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-high);\n\twhite-space: nowrap;\n}\n.fwlive-display-controls {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmin-width: 0;\n\tflex: 1 1 auto;\n}\n.fwlive-display-ctl {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tmargin: 0;\n\tfont-size: 0.92em;\n\twhite-space: nowrap;\n}\n.fwlive-display-ctl input[type=\"checkbox\"] {\n\tmargin: 0;\n\tflex-shrink: 0;\n}\n.fwlive-display-ctl select {\n\tmargin-left: 0;\n}\n.fwlive-display-ctl.fwlive-hidden {\n\tdisplay: none;\n}\n/* Draw our own disclosure arrow — themes that restyle summary drop the native marker. */\n.fwlive-more-filters > summary,\n.fwlive-help > summary {\n\tcursor: pointer;\n\tlist-style: none;\n}\n.fwlive-more-filters > summary::-webkit-details-marker,\n.fwlive-help > summary::-webkit-details-marker { display: none; }\n.fwlive-more-filters > summary::before,\n.fwlive-help > summary::before {\n\tcontent: \"▸ \";\n\tcolor: var(--text-color-medium);\n}\n.fwlive-more-filters[open] > summary::before,\n.fwlive-help[open] > summary::before { content: \"▾ \"; }\n.fwlive-ctl {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.92em;\n\tflex-wrap: wrap;\n}\n.fwlive-ctl select { margin-left: 4px; }\n#fwlive-limit { width: auto; min-width: 4.5em; }\n.fwlive-grid { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)); gap: 8px; margin-bottom: 12px; }\n.fwlive-status { margin: 0; color: var(--text-color-medium); font-size: 0.9em; }\n.fwlive-status-paused { color: var(--warn-color-high); font-weight: 600; }\n.fwlive-status-error { color: var(--warn-color-high); font-weight: 600; }\n.fwlive-hint-line {\n\tmargin: 0 0 10px;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-medium);\n}\n.fwlive-empty-title {\n\tfont-weight: 600;\n\tmargin: 0 0 6px;\n\tcolor: var(--text-color-high);\n}\n.fwlive-find-row .fwlive-grid-core { margin-bottom: 0; }\n.fwlive-proto-pair {\n\tdisplay: grid;\n\tgrid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);\n\tgap: 6px;\n\talign-items: stretch;\n\tmin-width: 0;\n}\n.fwlive-proto-pair select,\n.fwlive-proto-pair input {\n\twidth: 100%;\n\tmin-width: 0;\n}\n@media (max-width: 520px) {\n\t.fwlive-proto-pair { grid-template-columns: 1fr; }\n}\n.fwlive-scroll {\n\tmax-height: 78vh;\n\tmax-height: 78dvh;\n\toverflow: auto;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n\twidth: 100%;\n\tscrollbar-gutter: stable;\n}\n#fwlive-table { margin: 0; width: 100%; table-layout: auto; border-collapse: collapse; }\n.fwlive-map[data-view=\"simple\"] #fwlive-table { table-layout: fixed; width: 100%; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-action { width: 5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-time { width: 5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-iface { width: 5.5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-proto { width: 4rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-rule { width: 12rem; }\n.fwlive-map[data-view=\"detailed\"] .fwlive-scroll #fwlive-table {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-time { width: 11rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-action { width: 4.5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-rule { width: 8rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-iface-in,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-iface-out { width: 3.5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dir { width: 3rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-proto { width: 3.25rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-src,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dst { width: 9rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-sport,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dport { width: 3.75rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-flags { width: 5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-len { width: 3.25rem; }\n.fwlive-scroll.fwlive-msg-oneline #fwlive-table {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n.fwlive-scroll.fwlive-msg-oneline tbody td { vertical-align: middle; }\n#fwlive-table thead th {\n\tposition: sticky;\n\ttop: 0;\n\tz-index: 2;\n\tbackground: var(--background-color-low);\n\tborder-bottom: 2px solid var(--border-color-high);\n\twhite-space: nowrap;\n\tpadding: 5px 8px;\n\tfont-size: 0.9em;\n\tfont-weight: 600;\n\tvertical-align: top;\n\ttext-align: left;\n}\n#fwlive-table thead th.fwlive-port,\n#fwlive-table thead th.fwlive-len {\n\ttext-align: right;\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-table thead th,\n.fwlive-map[data-view=\"simple\"] #fwlive-table tbody td {\n\tvertical-align: top;\n}\n#fwlive-table thead th.fwlive-action {\n\tfont-weight: 700;\n\ttext-transform: lowercase;\n}\n#fwlive-table tbody td {\n\tpadding: 5px 8px;\n\tborder-bottom: 1px solid var(--border-color-low);\n\tvertical-align: top;\n\tfont-size: 0.92em;\n}\n.fwlive-row-alt td {\n\tbackground: #f9f9f9;\n\tbackground: var(--fwlive-bg-medium);\n}\n.fwlive-time, .fwlive-addr, .fwlive-port, .fwlive-len {\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\twhite-space: nowrap;\n}\n.fwlive-port, .fwlive-len { text-align: right; }\n.fwlive-rule, .fwlive-iface, .fwlive-proto, .fwlive-dir, .fwlive-flags { white-space: nowrap; }\n.fwlive-rule { font-size: 0.88em; }\n.fwlive-rule-link {\n\tcolor: var(--primary-color-high);\n\ttext-decoration: none;\n\tfont-weight: 500;\n}\n.fwlive-rule-link:hover { text-decoration: underline; }\n.fwlive-iface-badge {\n\tdisplay: inline-block;\n\tpadding: 1px 6px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tfont-size: 0.85em;\n\ttext-decoration: none;\n}\n.fwlive-iface-badge:hover {\n\tbackground: var(--background-color-medium);\n\tborder-color: var(--primary-color-medium);\n}\n.fwlive-action {\n\tfont-weight: 700;\n\ttext-transform: lowercase;\n\twhite-space: nowrap;\n}\n.fwlive-deny { color: var(--fwlive-deny-color); }\n.fwlive-pass { color: var(--fwlive-pass-color); }\n#fwlive-table td.fwlive-action.fwlive-pass,\n#fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {\n\tcolor: var(--fwlive-pass-color);\n}\n#fwlive-table td.fwlive-action.fwlive-deny,\n#fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(70, 165, 70, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(70, 165, 70, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(202, 60, 60, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(202, 60, 60, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(70, 165, 70, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(202, 60, 60, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(70, 165, 70, 0.12);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(70, 165, 70, 0.18);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(202, 60, 60, 0.12);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(202, 60, 60, 0.18);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(70, 165, 70, 0.20);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(202, 60, 60, 0.20);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(13, 148, 136, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(13, 148, 136, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(194, 65, 12, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(194, 65, 12, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(13, 148, 136, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(194, 65, 12, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(13, 148, 136, 0.12);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(13, 148, 136, 0.18);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(194, 65, 12, 0.12);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(194, 65, 12, 0.18);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(13, 148, 136, 0.20);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(194, 65, 12, 0.20);\n}\n.fwlive-unknown { color: var(--text-color-medium); font-weight: 500; }\n.fwlive-message {\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-high);\n}\n.fwlive-scroll.fwlive-msg-wrap .fwlive-message-wrap {\n\tdisplay: block;\n\tmin-width: 16em;\n\tmax-width: 42em;\n\twhite-space: normal;\n\tword-break: break-word;\n\tword-wrap: break-word;\n}\n.fwlive-scroll.fwlive-msg-oneline .fwlive-message {\n\twhite-space: nowrap;\n\tmax-width: none;\n\tmin-width: 32em;\n}\n#fwlive-table th.fwlive-th-message,\n.fwlive-scroll.fwlive-msg-oneline .fwlive-message {\n\twidth: 99%;\n}\n.fwlive-empty {\n\tmargin: 12px 0;\n\tpadding: 10px;\n\tbackground: var(--background-color-medium);\n\tborder: 1px dashed var(--border-color-high);\n}\n.fwlive-empty-muted {\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmargin: 0 0 8px;\n}\n.fwlive-consent {\n\tmargin: 0 0 12px;\n\tpadding: 10px 12px;\n\tbackground: var(--background-color-high);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n}\n.fwlive-consent-list {\n\tmargin: 0 0 10px;\n\tpadding-left: 1.25em;\n}\n.fwlive-consent-list li {\n\tmargin: 0 0 4px;\n}\n.fwlive-consent-check {\n\tmargin: 0 0 10px;\n\tfont-size: 0.92em;\n}\n.fwlive-consent-actions {\n\tmargin: 0;\n}\n.fwlive-logging-bar {\n\tdisplay: contents;\n}\n.fwlive-logging-status {\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmargin-right: 4px;\n}\n.fwlive-logging-notice {\n\tcolor: var(--fwlive-pass-color);\n}\n.fwlive-filter-link {\n\tcolor: inherit;\n\ttext-decoration: underline;\n\ttext-decoration-style: dotted;\n\tcursor: pointer;\n}\n.fwlive-filter-link:hover { color: var(--primary-color-high); }\n.fwlive-chips {\n\tdisplay: none;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 6px;\n\tmargin: 0 0 10px;\n}\n.fwlive-chip-label {\n\tline-height: 1.3;\n}\n.fwlive-chip-not {\n\tfont-weight: 700;\n}\n.fwlive-chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 4px;\n\tpadding: 2px 8px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tfont-size: 0.88em;\n}\n.fwlive-chip-negated {\n\tborder-style: dashed;\n\tbackground: var(--background-color-medium);\n}\n.fwlive-chip-include {\n}\n.fwlive-chip-polarity {\n\tfont-weight: 700;\n\tfont-size: 0.78em;\n\ttext-transform: uppercase;\n\tletter-spacing: 0.03em;\n\tcolor: var(--text-color-medium);\n}\n.fwlive-chip-strike {\n\ttext-decoration: line-through;\n\ttext-decoration-thickness: 1.5px;\n}\n.fwlive-chip-sym {\n\tfont-weight: 700;\n\tline-height: 1;\n\tmin-width: 0.9em;\n\ttext-align: center;\n}\n.fwlive-chip-sym-light {\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.95em;\n}\n#fwlive-row-tint { width: auto; min-width: 9.5em; }\n/* labels (default) */\n.fwlive-chips-labels .fwlive-chip-include {\n\tborder-left: 3px solid var(--primary-color-medium, var(--primary-color-high, var(--border-color-high)));\n}\n.fwlive-chips-labels .fwlive-chip-negated .fwlive-chip-not {\n\tcolor: var(--text-color-high);\n}\n.fwlive-chip-invert-wrap {\n\tposition: relative;\n\tdisplay: inline-flex;\n}\n.fwlive-chip-invert-wrap::before {\n\tcontent: attr(data-tip);\n\tposition: absolute;\n\tbottom: calc(100% + 6px);\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\twhite-space: nowrap;\n\tpadding: 4px 8px;\n\tfont-size: 0.85em;\n\tfont-weight: normal;\n\tcolor: var(--text-color-high);\n\tbackground: var(--background-color-high);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbox-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);\n\topacity: 0;\n\tvisibility: hidden;\n\tpointer-events: none;\n\tz-index: 20;\n\ttransition: opacity 0.15s ease 1.5s, visibility 0s linear 2s;\n}\n.fwlive-chip-invert-wrap:hover::before,\n.fwlive-chip-invert-wrap:focus-within::before {\n\topacity: 1;\n\tvisibility: visible;\n\ttransition: opacity 0.12s ease 0.35s, visibility 0s;\n}\n.fwlive-chip-invert {\n\tcolor: var(--text-color-medium);\n\tbackground: none;\n\tborder: none;\n\tpadding: 0;\n\tfont: inherit;\n\tfont-weight: 700;\n\tline-height: 1;\n\tcursor: pointer;\n}\n.fwlive-chip-invert:hover { color: var(--primary-color-high); }\n.fwlive-chip-remove {\n\tcolor: var(--text-color-medium);\n\ttext-decoration: none;\n\tfont-weight: 700;\n\tline-height: 1;\n}\n.fwlive-chip-remove:hover { color: var(--fwlive-deny-color); }\n.fwlive-chip-clear {\n\tfont-size: 0.88em;\n\tmargin-left: 4px;\n}\n.fwlive-flood {\n\tdisplay: none;\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--warn-color-high);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n\tfont-size: 0.92em;\n}\n.fwlive-adaptive {\n\tdisplay: none;\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n\tfont-size: 0.92em;\n}\n.fwlive-summary {\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n}\n.fwlive-summary-head {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 10px;\n}\n.fwlive-summary-body {\n\tmargin: 8px 0 0;\n\tfont: inherit;\n\twhite-space: pre-wrap;\n}\n.fwlive-tint-warn {\n\tdisplay: none;\n\tcolor: var(--warn-color-high);\n\tfont-size: 0.78em;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n\tpadding: 2px 8px;\n\tborder: 1px solid var(--warn-color-high);\n\tborder-radius: 999px;\n\tbackground: color-mix(in srgb, var(--warn-color-high) 10%, transparent);\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-msg-group { display: none; }\n@media (max-width: 850px) {\n\t.fwlive-title-status {\n\t\tflex-basis: 100%;\n\t\tmargin-left: 0;\n\t}\n\t.fwlive-watch-group {\n\t\tborder-left: 0;\n\t\tpadding-left: 0;\n\t\tpadding-right: 10px;\n\t}\n\t.fwlive-display-bar {\n\t\talign-items: flex-start;\n\t}\n}\n.fwlive-help-row {\n\tdisplay: flex;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin: 10px 0 0;\n}\n.fwlive-help {\n\tmargin: 0;\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmin-width: 0;\n\tflex: 1;\n}\n.fwlive-help ul { margin: 6px 0 0 1.2em; padding: 0; }\n.fwlive-help li { margin: 4px 0; }\n.fwlive-build {\n\tflex-shrink: 0;\n\tfont-size: 0.78em;\n\tcolor: var(--text-color-low, var(--text-color-medium));\n\tfont-variant-numeric: tabular-nums;\n\tuser-select: all;\n\twhite-space: nowrap;\n}\n.fwlive-backend {\n\tfont-size: 0.65em;\n\tfont-weight: normal;\n\tcolor: var(--text-color-medium);\n\tmargin-left: 8px;\n\tvertical-align: middle;\n}\n.fwlive-backend.fwlive-backend-warn {\n\tcolor: var(--warn-color-high);\n\tfont-weight: 600;\n}\n.fwlive-filter-panel { margin-bottom: 12px; }\n.fwlive-map[data-view=\"simple\"] .fwlive-grid-core {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(3, minmax(140px, 1fr));\n\tgap: 8px;\n}\n.fwlive-map[data-view=\"simple\"] .fwlive-more-filters { margin-top: 8px; }\n.fwlive-map[data-view=\"simple\"] .fwlive-grid-extra {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(4, minmax(140px, 1fr));\n\tgap: 8px;\n\tmargin-top: 8px;\n}\n.fwlive-map[data-view=\"detailed\"] .fwlive-more-filters > summary { display: none; }\n.fwlive-map[data-view=\"detailed\"] .fwlive-filter-panel {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(4, minmax(140px, 1fr));\n\tgap: 8px;\n}\n.fwlive-map[data-view=\"detailed\"] .fwlive-grid-core,\n.fwlive-map[data-view=\"detailed\"] .fwlive-grid-extra,\n.fwlive-map[data-view=\"detailed\"] .fwlive-more-filters { display: contents; }\n.fwlive-row-clickable { cursor: pointer; }\n.fwlive-row-expanded td { border-bottom-color: transparent; }\n.fwlive-msg-expand td {\n\tbackground: var(--background-color-low);\n\tpadding: 8px 12px 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n}\n.fwlive-msg-expand-label {\n\tfont-size: 0.82em;\n\tcolor: var(--text-color-medium);\n\tmargin-bottom: 4px;\n\tfont-weight: 600;\n}\n.fwlive-msg-expand-body {\n\tmargin: 0;\n\twhite-space: pre-wrap;\n\tword-break: break-word;\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-high);\n}\n.fwlive-flow-arrow {\n\tcolor: var(--text-color-low);\n\tfont-weight: 700;\n}\n.fwlive-flow-cell { white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.9em; }\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-pass,\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {\n\tcolor: var(--fwlive-pass-color);\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-deny,\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-iface-badge {\n\tborder-radius: 10px;\n\tpadding: 2px 8px;\n}\n" + styleText: "\n/* SPDX-License-Identifier: Apache-2.0 */\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-display-bar-label {\n\tflex: 0 0 auto;\n\tfont-weight: 600;\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-high);\n\twhite-space: nowrap;\n}\n.fwlive-display-controls {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmin-width: 0;\n\tflex: 1 1 auto;\n}\n.fwlive-display-ctl {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tmargin: 0;\n\tfont-size: 0.92em;\n\twhite-space: nowrap;\n}\n.fwlive-display-ctl input[type=\"checkbox\"] {\n\tmargin: 0;\n\tflex-shrink: 0;\n}\n.fwlive-display-ctl select {\n\tmargin-left: 0;\n}\n.fwlive-display-ctl.fwlive-hidden {\n\tdisplay: none;\n}\n/* Draw our own disclosure arrow — themes that restyle summary drop the native marker. */\n.fwlive-more-filters > summary,\n.fwlive-help > summary {\n\tcursor: pointer;\n\tlist-style: none;\n}\n.fwlive-more-filters > summary::-webkit-details-marker,\n.fwlive-help > summary::-webkit-details-marker { display: none; }\n.fwlive-more-filters > summary::before,\n.fwlive-help > summary::before {\n\tcontent: \"▸ \";\n\tcolor: var(--text-color-medium);\n}\n.fwlive-more-filters[open] > summary::before,\n.fwlive-help[open] > summary::before { content: \"▾ \"; }\n.fwlive-ctl {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.92em;\n\tflex-wrap: wrap;\n}\n.fwlive-ctl select { margin-left: 4px; }\n#fwlive-limit { width: auto; min-width: 4.5em; }\n.fwlive-grid { display: grid; grid-template-columns: repeat(4, minmax(140px, 1fr)); gap: 8px; margin-bottom: 12px; }\n.fwlive-status { margin: 0; color: var(--text-color-medium); font-size: 0.9em; }\n.fwlive-status-paused { color: var(--warn-color-high); font-weight: 600; }\n.fwlive-status-error { color: var(--warn-color-high); font-weight: 600; }\n.fwlive-hint-line {\n\tmargin: 0 0 10px;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-medium);\n}\n.fwlive-empty-title {\n\tfont-weight: 600;\n\tmargin: 0 0 6px;\n\tcolor: var(--text-color-high);\n}\n.fwlive-find-row .fwlive-grid-core { margin-bottom: 0; }\n.fwlive-proto-pair {\n\tdisplay: grid;\n\tgrid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);\n\tgap: 6px;\n\talign-items: stretch;\n\tmin-width: 0;\n}\n.fwlive-proto-pair select,\n.fwlive-proto-pair input {\n\twidth: 100%;\n\tmin-width: 0;\n}\n@media (max-width: 520px) {\n\t.fwlive-proto-pair { grid-template-columns: 1fr; }\n}\n.fwlive-scroll {\n\tmax-height: 78vh;\n\tmax-height: 78dvh;\n\toverflow: auto;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n\twidth: 100%;\n\tscrollbar-gutter: stable;\n}\n#fwlive-table { margin: 0; width: 100%; table-layout: auto; border-collapse: collapse; }\n.fwlive-map[data-view=\"simple\"] #fwlive-table { table-layout: fixed; width: 100%; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-action { width: 5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-time { width: 5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-iface { width: 5.5rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-proto { width: 4rem; }\n.fwlive-map[data-view=\"simple\"] col.fwlive-col-rule { width: 12rem; }\n.fwlive-map[data-view=\"detailed\"] .fwlive-scroll #fwlive-table {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-time { width: 11rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-action { width: 4.5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-rule { width: 8rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-iface-in,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-iface-out { width: 3.5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dir { width: 3rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-proto { width: 3.25rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-src,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dst { width: 9rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-sport,\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-dport { width: 3.75rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-flags { width: 5rem; }\n.fwlive-map[data-view=\"detailed\"] col.fwlive-col-len { width: 3.25rem; }\n.fwlive-scroll.fwlive-msg-oneline #fwlive-table {\n\twidth: max-content;\n\tmin-width: 100%;\n}\n.fwlive-scroll.fwlive-msg-oneline tbody td { vertical-align: middle; }\n#fwlive-table thead th {\n\tposition: sticky;\n\ttop: 0;\n\tz-index: 2;\n\tbackground: var(--background-color-low);\n\tborder-bottom: 2px solid var(--border-color-high);\n\twhite-space: nowrap;\n\tpadding: 5px 8px;\n\tfont-size: 0.9em;\n\tfont-weight: 600;\n\tvertical-align: top;\n\ttext-align: left;\n}\n#fwlive-table thead th.fwlive-port,\n#fwlive-table thead th.fwlive-len {\n\ttext-align: right;\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-table thead th,\n.fwlive-map[data-view=\"simple\"] #fwlive-table tbody td {\n\tvertical-align: top;\n}\n#fwlive-table thead th.fwlive-action {\n\tfont-weight: 700;\n\ttext-transform: lowercase;\n}\n#fwlive-table tbody td {\n\tpadding: 5px 8px;\n\tborder-bottom: 1px solid var(--border-color-low);\n\tvertical-align: top;\n\tfont-size: 0.92em;\n}\n.fwlive-row-alt td {\n\tbackground: #f9f9f9;\n\tbackground: var(--fwlive-bg-medium);\n}\n.fwlive-time, .fwlive-addr, .fwlive-port, .fwlive-len {\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\twhite-space: nowrap;\n}\n.fwlive-port, .fwlive-len { text-align: right; }\n.fwlive-rule, .fwlive-iface, .fwlive-proto, .fwlive-dir, .fwlive-flags { white-space: nowrap; }\n.fwlive-rule { font-size: 0.88em; }\n.fwlive-rule-link {\n\tcolor: var(--primary-color-high);\n\ttext-decoration: none;\n\tfont-weight: 500;\n}\n.fwlive-rule-link:hover { text-decoration: underline; }\n.fwlive-iface-badge {\n\tdisplay: inline-block;\n\tpadding: 1px 6px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tfont-size: 0.85em;\n\ttext-decoration: none;\n}\n.fwlive-iface-badge:hover {\n\tbackground: var(--background-color-medium);\n\tborder-color: var(--primary-color-medium);\n}\n.fwlive-action {\n\tfont-weight: 700;\n\ttext-transform: lowercase;\n\twhite-space: nowrap;\n}\n.fwlive-deny { color: var(--fwlive-deny-color); }\n.fwlive-pass { color: var(--fwlive-pass-color); }\n#fwlive-table td.fwlive-action.fwlive-pass,\n#fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {\n\tcolor: var(--fwlive-pass-color);\n}\n#fwlive-table td.fwlive-action.fwlive-deny,\n#fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(70, 165, 70, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(70, 165, 70, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(202, 60, 60, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(202, 60, 60, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(70, 165, 70, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(202, 60, 60, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(70, 165, 70, 0.12);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(70, 165, 70, 0.18);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(202, 60, 60, 0.12);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(202, 60, 60, 0.18);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(70, 165, 70, 0.20);\n}\n.fwlive-map[data-row-tint=\"classic\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(202, 60, 60, 0.20);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(13, 148, 136, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(13, 148, 136, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(194, 65, 12, 0.12);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(194, 65, 12, 0.18);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 12%, var(--fwlive-bg-medium));\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(13, 148, 136, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-pass-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(194, 65, 12, 0.20);\n\tbackground: color-mix(in srgb, var(--fwlive-deny-color) 20%, transparent);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass td {\n\tbackground: rgba(13, 148, 136, 0.12);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass.fwlive-row-alt td {\n\tbackground: rgba(13, 148, 136, 0.18);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny td {\n\tbackground: rgba(194, 65, 12, 0.12);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny.fwlive-row-alt td {\n\tbackground: rgba(194, 65, 12, 0.18);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-pass:hover td {\n\tbackground: rgba(13, 148, 136, 0.20);\n}\n.fwlive-map[data-row-tint=\"accessible\"][data-tint-fallback=\"1\"] #fwlive-table tbody tr.fwlive-row-deny:hover td {\n\tbackground: rgba(194, 65, 12, 0.20);\n}\n.fwlive-unknown { color: var(--text-color-medium); font-weight: 500; }\n.fwlive-message {\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-high);\n}\n.fwlive-scroll.fwlive-msg-wrap .fwlive-message-wrap {\n\tdisplay: block;\n\tmin-width: 16em;\n\tmax-width: 42em;\n\twhite-space: normal;\n\tword-break: break-word;\n\tword-wrap: break-word;\n}\n.fwlive-scroll.fwlive-msg-oneline .fwlive-message {\n\twhite-space: nowrap;\n\tmax-width: none;\n\tmin-width: 32em;\n}\n#fwlive-table th.fwlive-th-message,\n.fwlive-scroll.fwlive-msg-oneline .fwlive-message {\n\twidth: 99%;\n}\n.fwlive-empty {\n\tmargin: 12px 0;\n\tpadding: 10px;\n\tbackground: var(--background-color-medium);\n\tborder: 1px dashed var(--border-color-high);\n}\n.fwlive-empty-muted {\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmargin: 0 0 8px;\n}\n.fwlive-consent {\n\tmargin: 0 0 12px;\n\tpadding: 10px 12px;\n\tbackground: var(--background-color-high);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n}\n.fwlive-consent-list {\n\tmargin: 0 0 10px;\n\tpadding-left: 1.25em;\n}\n.fwlive-consent-list li {\n\tmargin: 0 0 4px;\n}\n.fwlive-consent-check {\n\tmargin: 0 0 10px;\n\tfont-size: 0.92em;\n}\n.fwlive-consent-actions {\n\tmargin: 0;\n}\n.fwlive-logging-bar {\n\tdisplay: contents;\n}\n.fwlive-logging-status {\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmargin-right: 4px;\n}\n.fwlive-logging-notice {\n\tcolor: var(--fwlive-pass-color);\n}\n.fwlive-logging-notice-fail {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-filter-link {\n\tcolor: inherit;\n\ttext-decoration: underline;\n\ttext-decoration-style: dotted;\n\tcursor: pointer;\n}\n.fwlive-filter-link:hover { color: var(--primary-color-high); }\n.fwlive-chips {\n\tdisplay: none;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 6px;\n\tmargin: 0 0 10px;\n}\n.fwlive-chip-label {\n\tline-height: 1.3;\n}\n.fwlive-chip-not {\n\tfont-weight: 700;\n}\n.fwlive-chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 4px;\n\tpadding: 2px 8px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tfont-size: 0.88em;\n}\n.fwlive-chip-negated {\n\tborder-style: dashed;\n\tbackground: var(--background-color-medium);\n}\n.fwlive-chip-include {\n}\n.fwlive-chip-polarity {\n\tfont-weight: 700;\n\tfont-size: 0.78em;\n\ttext-transform: uppercase;\n\tletter-spacing: 0.03em;\n\tcolor: var(--text-color-medium);\n}\n.fwlive-chip-strike {\n\ttext-decoration: line-through;\n\ttext-decoration-thickness: 1.5px;\n}\n.fwlive-chip-sym {\n\tfont-weight: 700;\n\tline-height: 1;\n\tmin-width: 0.9em;\n\ttext-align: center;\n}\n.fwlive-chip-sym-light {\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.95em;\n}\n#fwlive-row-tint { width: auto; min-width: 9.5em; }\n/* labels (default) */\n.fwlive-chips-labels .fwlive-chip-include {\n\tborder-left: 3px solid var(--primary-color-medium, var(--primary-color-high, var(--border-color-high)));\n}\n.fwlive-chips-labels .fwlive-chip-negated .fwlive-chip-not {\n\tcolor: var(--text-color-high);\n}\n.fwlive-chip-invert-wrap {\n\tposition: relative;\n\tdisplay: inline-flex;\n}\n.fwlive-chip-invert-wrap::before {\n\tcontent: attr(data-tip);\n\tposition: absolute;\n\tbottom: calc(100% + 6px);\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\twhite-space: nowrap;\n\tpadding: 4px 8px;\n\tfont-size: 0.85em;\n\tfont-weight: normal;\n\tcolor: var(--text-color-high);\n\tbackground: var(--background-color-high);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbox-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);\n\topacity: 0;\n\tvisibility: hidden;\n\tpointer-events: none;\n\tz-index: 20;\n\ttransition: opacity 0.15s ease 1.5s, visibility 0s linear 2s;\n}\n.fwlive-chip-invert-wrap:hover::before,\n.fwlive-chip-invert-wrap:focus-within::before {\n\topacity: 1;\n\tvisibility: visible;\n\ttransition: opacity 0.12s ease 0.35s, visibility 0s;\n}\n.fwlive-chip-invert {\n\tcolor: var(--text-color-medium);\n\tbackground: none;\n\tborder: none;\n\tpadding: 0;\n\tfont: inherit;\n\tfont-weight: 700;\n\tline-height: 1;\n\tcursor: pointer;\n}\n.fwlive-chip-invert:hover { color: var(--primary-color-high); }\n.fwlive-chip-remove {\n\tcolor: var(--text-color-medium);\n\ttext-decoration: none;\n\tfont-weight: 700;\n\tline-height: 1;\n}\n.fwlive-chip-remove:hover { color: var(--fwlive-deny-color); }\n.fwlive-chip-clear {\n\tfont-size: 0.88em;\n\tmargin-left: 4px;\n}\n.fwlive-flood {\n\tdisplay: none;\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--warn-color-high);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n\tfont-size: 0.92em;\n}\n.fwlive-adaptive {\n\tdisplay: none;\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n\tfont-size: 0.92em;\n}\n.fwlive-summary {\n\tmargin: 0 0 10px;\n\tpadding: 8px 12px;\n\tbackground: var(--background-color-low);\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tcolor: var(--text-color-high);\n}\n.fwlive-summary-head {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 10px;\n}\n.fwlive-summary-body {\n\tmargin: 8px 0 0;\n\tfont: inherit;\n\twhite-space: pre-wrap;\n}\n.fwlive-tint-warn {\n\tdisplay: none;\n\tcolor: var(--warn-color-high);\n\tfont-size: 0.78em;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n\tpadding: 2px 8px;\n\tborder: 1px solid var(--warn-color-high);\n\tborder-radius: 999px;\n\tbackground: color-mix(in srgb, var(--warn-color-high) 10%, transparent);\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-msg-group { display: none; }\n@media (max-width: 850px) {\n\t.fwlive-title-status {\n\t\tflex-basis: 100%;\n\t\tmargin-left: 0;\n\t}\n\t.fwlive-watch-group {\n\t\tborder-left: 0;\n\t\tpadding-left: 0;\n\t\tpadding-right: 10px;\n\t}\n\t.fwlive-display-bar {\n\t\talign-items: flex-start;\n\t}\n}\n.fwlive-help-row {\n\tdisplay: flex;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin: 10px 0 0;\n}\n.fwlive-help {\n\tmargin: 0;\n\tfont-size: 0.92em;\n\tcolor: var(--text-color-medium);\n\tmin-width: 0;\n\tflex: 1;\n}\n.fwlive-help ul { margin: 6px 0 0 1.2em; padding: 0; }\n.fwlive-help li { margin: 4px 0; }\n.fwlive-build {\n\tflex-shrink: 0;\n\tfont-size: 0.78em;\n\tcolor: var(--text-color-low, var(--text-color-medium));\n\tfont-variant-numeric: tabular-nums;\n\tuser-select: all;\n\twhite-space: nowrap;\n}\n.fwlive-backend {\n\tfont-size: 0.65em;\n\tfont-weight: normal;\n\tcolor: var(--text-color-medium);\n\tmargin-left: 8px;\n\tvertical-align: middle;\n}\n.fwlive-backend.fwlive-backend-warn {\n\tcolor: var(--warn-color-high);\n\tfont-weight: 600;\n}\n.fwlive-filter-panel { margin-bottom: 12px; }\n.fwlive-map[data-view=\"simple\"] .fwlive-grid-core {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(3, minmax(140px, 1fr));\n\tgap: 8px;\n}\n.fwlive-map[data-view=\"simple\"] .fwlive-more-filters { margin-top: 8px; }\n.fwlive-map[data-view=\"simple\"] .fwlive-grid-extra {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(4, minmax(140px, 1fr));\n\tgap: 8px;\n\tmargin-top: 8px;\n}\n.fwlive-map[data-view=\"detailed\"] .fwlive-more-filters > summary { display: none; }\n.fwlive-map[data-view=\"detailed\"] .fwlive-filter-panel {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(4, minmax(140px, 1fr));\n\tgap: 8px;\n}\n.fwlive-map[data-view=\"detailed\"] .fwlive-grid-core,\n.fwlive-map[data-view=\"detailed\"] .fwlive-grid-extra,\n.fwlive-map[data-view=\"detailed\"] .fwlive-more-filters { display: contents; }\n.fwlive-row-clickable { cursor: pointer; }\n.fwlive-row-expanded td { border-bottom-color: transparent; }\n.fwlive-msg-expand td {\n\tbackground: var(--background-color-low);\n\tpadding: 8px 12px 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n}\n.fwlive-msg-expand-label {\n\tfont-size: 0.82em;\n\tcolor: var(--text-color-medium);\n\tmargin-bottom: 4px;\n\tfont-weight: 600;\n}\n.fwlive-msg-expand-body {\n\tmargin: 0;\n\twhite-space: pre-wrap;\n\tword-break: break-word;\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n\tfont-size: 0.85em;\n\tcolor: var(--text-color-high);\n}\n.fwlive-flow-arrow {\n\tcolor: var(--text-color-low);\n\tfont-weight: 700;\n}\n.fwlive-flow-cell { white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.9em; }\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-pass,\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-pass a.fwlive-filter-link {\n\tcolor: var(--fwlive-pass-color);\n}\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-deny,\n.fwlive-map[data-view=\"simple\"] #fwlive-table td.fwlive-action.fwlive-deny a.fwlive-filter-link {\n\tcolor: var(--fwlive-deny-color);\n}\n.fwlive-iface-badge {\n\tborder-radius: 10px;\n\tpadding: 2px 8px;\n}\n" }); diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/fwlive.css b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/fwlive.css index bbab162d4..c7880f041 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/fwlive.css +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/fwlive.css @@ -533,6 +533,9 @@ .fwlive-logging-notice { color: var(--fwlive-pass-color); } +.fwlive-logging-notice-fail { + color: var(--fwlive-deny-color); +} .fwlive-filter-link { color: inherit; text-decoration: underline; diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js index 17d5441bd..3f580d721 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js @@ -9,7 +9,7 @@ * * renderToolbar(host, state, callbacks) → void * host - #fwlive-logging-bar strip slot (cleared and rebuilt; element kept) - * state - { loggingStatus, loggingBusy, loggingNotice } + * state - { loggingStatus, loggingBusy, loggingNotice, loggingNoticeFail } * callbacks - { onEnable(), onDisable() } * * G Hybrid chrome: when WAN logging is on, one merged control carries status + @@ -88,9 +88,15 @@ function labelWithZoneCandidates(label, st) { return children; } +function loggingNoticeClass(state) { + return state.loggingNoticeFail + ? 'fwlive-logging-notice fwlive-logging-notice-fail' + : 'fwlive-logging-notice'; +} + function appendLoggingNotice(host, state) { if (!state.loggingNotice) return; - host.appendChild(E('span', { 'class': 'fwlive-logging-notice' }, [state.loggingNotice])); + host.appendChild(E('span', { 'class': loggingNoticeClass(state) }, [state.loggingNotice])); } function appendBlockerStatus(host, blocker, st) { @@ -254,7 +260,7 @@ function buildEmptyStateNodes(state, callbacks) { if (state.loggingNotice) { nodes.push( - E('p', { 'class': 'fwlive-logging-notice' }, [ + E('p', { 'class': loggingNoticeClass(state) }, [ state.loggingNotice, ' ', links.firewallZonesLink() @@ -287,14 +293,12 @@ function buildEmptyStateNodes(state, callbacks) { nodes.push( E('p', {}, [ _( - 'Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall.' + 'Kernel netfilter log modules are missing. Install kmod-nf-log and kmod-nf-log6, then reload the firewall.' ) ]) ); nodes.push( - E('p', {}, [ - E('code', {}, ['opkg update && opkg install kmod-nf-log-ipv4 kmod-nf-log-ipv6']) - ]) + E('p', {}, [E('code', {}, ['opkg update && opkg install kmod-nf-log kmod-nf-log6'])]) ); return nodes; } diff --git a/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js b/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js index e91fd6057..85a9cbf3c 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js @@ -177,6 +177,7 @@ return view.extend({ loggingStatusReadGeneration: 0, loggingBusy: false, loggingNotice: '', + loggingNoticeFail: false, _loggingNoticeFromToggle: false, /* Session-only dismiss of first-run consent (Not now without checkbox). */ consentDismissedSession: false, @@ -704,10 +705,13 @@ return view.extend({ /* Bounds / mktemp failures are reply.error — same idea as poll. */ this.lastRulesError = (res && res.error) || null; if (this.lastRulesError) console.warn('fwlive rules map error:', this.lastRulesError); + this.refreshBufferedRuleLabels(); } catch (_e) { if (!this.isCurrentPollEpoch(epoch)) return; - this.rulesMap = {}; - this.firewallBackend = 'nft'; + if (!(this.rulesMap && Object.keys(this.rulesMap).length)) { + this.rulesMap = {}; + this.firewallBackend = 'nft'; + } this.lastRulesError = 'rules_unavailable'; } this.updateBackendUi(); @@ -762,7 +766,10 @@ return view.extend({ ) return; this.loggingStatus = status; - if (!this._loggingNoticeFromToggle) this.loggingNotice = ''; + if (!this._loggingNoticeFromToggle) { + this.loggingNotice = ''; + this.loggingNoticeFail = false; + } this.weakDevice = !!(this.loggingStatus && this.loggingStatus.weak_device === true); } catch (_e) { if ( @@ -771,10 +778,12 @@ return view.extend({ ) return; /* Keep last-known toolbar; unknown until the first successful fetch. */ - if (!this.loggingNotice) + if (!this.loggingNotice) { this.loggingNotice = this.loggingStatus ? _('Could not refresh logging status; showing the last known state.') : _('Could not load logging status.'); + this.loggingNoticeFail = true; + } } this.updateBackendUi(); this.updateLoggingToolbarUi(); @@ -791,6 +800,7 @@ return view.extend({ this.loggingStatusReadGeneration++; this.loggingBusy = true; this.loggingNotice = ''; + this.loggingNoticeFail = false; this._loggingNoticeFromToggle = false; opts.initialUi(); @@ -798,12 +808,14 @@ return view.extend({ const res = await opts.call(); if (!res || !res.ok) { this.loggingNotice = opts.failureNotice(res); + this.loggingNoticeFail = true; this._loggingNoticeFromToggle = true; await this.loadLoggingStatus(); return; } this.loggingNotice = opts.successNotice(res); + this.loggingNoticeFail = false; this._loggingNoticeFromToggle = !!this.loggingNotice; if (opts.onSuccess) opts.onSuccess(res); if (this.loggingStatus && typeof opts.wanLog === 'boolean') @@ -813,6 +825,7 @@ return view.extend({ await this.loadLoggingStatus(); } catch (_e) { this.loggingNotice = opts.catchNotice(); + this.loggingNoticeFail = true; this._loggingNoticeFromToggle = true; await this.loadLoggingStatus(); } finally { @@ -847,6 +860,16 @@ return view.extend({ return _('Could not snapshot the current logging state.'); if (res && res.error === 'firewall_reload_failed') return _('The firewall did not reload; saved and live logging may differ.'); + if (res && res.error === 'uci_set_failed') + return _('Could not write the WAN zone log option.'); + if (res && res.error === 'uci_delete_failed') + return _('Could not clear the WAN zone log option.'); + if (res && res.error === 'uci_commit_failed') + return _('Could not save the firewall configuration.'); + if (res && res.error === 'firewall_commit_raced') + return _( + 'Another change overwrote WAN logging after it was saved; check the current state.' + ); return _('Could not enable logging.'); }, successNotice: (res) => @@ -876,6 +899,16 @@ return view.extend({ return _('Could not acquire the logging lock.'); if (res && res.error === 'firewall_reload_failed') return _('The firewall did not reload; saved and live logging may differ.'); + if (res && res.error === 'uci_set_failed') + return _('Could not write the WAN zone log option.'); + if (res && res.error === 'uci_delete_failed') + return _('Could not clear the WAN zone log option.'); + if (res && res.error === 'uci_commit_failed') + return _('Could not save the firewall configuration.'); + if (res && res.error === 'firewall_commit_raced') + return _( + 'Another change overwrote WAN logging after it was saved; check the current state.' + ); return _('Could not disable logging.'); }, successNotice: (res) => (res.changed ? _('WAN drop/reject logging is off.') : ''), @@ -905,6 +938,7 @@ return view.extend({ loggingStatus: this.loggingStatus, loggingBusy: this.loggingBusy, loggingNotice: this.loggingNotice, + loggingNoticeFail: !!this.loggingNoticeFail, showConsent: this.shouldShowLoggingConsent() }; }, @@ -924,6 +958,7 @@ return view.extend({ candidates, this.loggingBusy ? '1' : '0', this.loggingNotice || '', + this.loggingNoticeFail ? 'f1' : 'f0', this.shouldShowLoggingConsent() ? 'c1' : 'c0' ].join('|'); }, @@ -1145,6 +1180,9 @@ return view.extend({ /* Recovery RPCs can outlive the poll epoch; don't repaint stale views. */ if (epoch !== this.currentPollEpoch() || this.viewDisposed) return; this.refreshBufferedRuleLabels(); + } else if (this.lastRulesError === 'rules_unavailable') { + await this.loadRulesMap(epoch); + if (epoch !== this.currentPollEpoch() || this.viewDisposed) return; } }, @@ -1505,12 +1543,11 @@ return view.extend({ }, scheduleResolvePaint() { + this.resolvePaintPending = true; if (this.tablePaused) { - this.resolvePaintPending = true; this.updateStatus(); return; } - this.resolvePaintPending = false; this.scheduleRenderRows(true); }, diff --git a/luci-app-fwlive/po/de/luci-app-fwlive.po b/luci-app-fwlive/po/de/luci-app-fwlive.po index bce025417..65992075f 100644 --- a/luci-app-fwlive/po/de/luci-app-fwlive.po +++ b/luci-app-fwlive/po/de/luci-app-fwlive.po @@ -12,16 +12,16 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1548 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1594 msgid "%d matching · %d/%d stored" msgstr "%d Treffer · %d/%d gespeichert" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1341 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1388 msgid "%s · requested up to %d raw lines · %d firewall messages returned" msgstr "" "%s · bis zu %d Rohzeilen angefordert · %d Firewall-Meldungen zurückgegeben" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1331 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1378 msgid "" "%s · requested up to %d raw lines · server limited fetch to %d · %d firewall " "messages returned" @@ -33,76 +33,85 @@ msgstr "" msgid "%s: %s" msgstr "%s: %s" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1550 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596 msgid "0 matching · %d/%d stored" msgstr "0 Treffer · %d/%d gespeichert" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:440 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:447 msgid "Accessible (teal/orange)" msgstr "Barrierefrei (teal/orange)" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:353 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37 msgid "Action" msgstr "Aktion" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:874 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:915 msgid "Administrator access is required to disable logging." msgstr "" "Zum Deaktivieren der Protokollierung sind Administratorrechte erforderlich." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:851 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:882 msgid "Administrator access is required to enable logging." msgstr "" "Zum Aktivieren der Protokollierung sind Administratorrechte erforderlich." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:208 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:214 msgid "Allow/deny rules, LAN logging, or anything else." msgstr "Allow/Deny-Regeln, LAN-Protokollierung oder irgendetwas anderes." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1742 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1788 msgid "Also seen" msgstr "Weitere" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:863 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:853 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:894 msgid "Another change is staged for the firewall; apply or revert it first." msgstr "" "Eine andere Änderung an der Firewall ist vorgemerkt; zuerst anwenden oder " "verwerfen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2547 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:910 +msgid "" +"Another change overwrote WAN logging after it was saved; check the current " +"state." +msgstr "" +"Eine andere Änderung hat die WAN-Protokollierung nach dem Speichern " +"überschrieben; prüfen Sie den aktuellen Zustand." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2596 msgid "Any action" msgstr "Beliebige Aktion" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1735 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1781 msgid "Any protocol" msgstr "Beliebiges Protokoll" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1382 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1369 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1429 msgid "Auto" msgstr "Auto" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:198 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204 msgid "Before you enable logging" msgstr "Bevor Sie die Protokollierung aktivieren" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:829 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:850 msgid "Cannot enable logging until kernel log modules are installed." msgstr "" "Protokollierung kann erst aktiviert werden, wenn die Kernel-Log-Module " "installiert sind." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:201 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:207 msgid "Changes:" msgstr "Änderungen:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:439 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:446 msgid "Classic (green/red)" msgstr "Klassisch (grün/rot)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2278 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2327 msgid "Classic uses green/red; Accessible uses teal/orange" msgstr "Klassisch verwendet grün/rot; Barrierefrei verwendet teal/orange" @@ -110,7 +119,7 @@ msgstr "Klassisch verwendet grün/rot; Barrierefrei verwendet teal/orange" msgid "Clear all" msgstr "Alle löschen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2619 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2668 msgid "" "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for " "firewall settings · in Simple view, click a row for the full message" @@ -119,7 +128,7 @@ msgstr "" "auf eine Regel für Firewall-Einstellungen · in der Einfachen Ansicht Zeile " "anklicken für die vollständige Meldung" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2346 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2395 msgid "" "Click a row (Time or other non-link cells) to see the full log line (Simple " "view)." @@ -127,11 +136,11 @@ msgstr "" "Klicken Sie auf eine Zeile (Zeit oder andere Nicht-Link-Zellen), um die " "vollständige Log-Zeile zu sehen (Einfache Ansicht)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:156 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:158 msgid "Click a row for the full message" msgstr "Zeile anklicken für die vollständige Meldung" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2351 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2400 msgid "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." @@ -139,66 +148,81 @@ msgstr "" "Klicken Sie auf eine IP, Aktion oder ein Protokoll, um zu filtern; nutzen " "Sie das Protokoll-Menü (oder ≠ auf einem Chip) zum Ausschließen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1736 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1782 msgid "Common" msgstr "Häufig" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1567 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1613 msgid "Connection lost — retrying…" msgstr "Verbindung verloren — neuer Versuch…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:837 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:868 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:858 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:899 msgid "Could not acquire the logging lock." msgstr "Die Protokollierungssperre konnte nicht erworben werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:866 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:905 +msgid "Could not clear the WAN zone log option." +msgstr "Die WAN-Zonen-Log-Option konnte nicht gelöscht werden." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:912 msgid "Could not disable logging." msgstr "Protokollierung konnte nicht deaktiviert werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:842 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:873 msgid "Could not enable logging." msgstr "Protokollierung konnte nicht aktiviert werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:769 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:784 msgid "Could not load logging status." msgstr "Protokollstatus konnte nicht geladen werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:768 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:783 msgid "Could not refresh logging status; showing the last known state." msgstr "" "Protokollstatus konnte nicht aktualisiert werden; zeige den zuletzt " "bekannten Zustand." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:839 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:868 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907 +msgid "Could not save the firewall configuration." +msgstr "Die Firewall-Konfiguration konnte nicht gespeichert werden." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:860 msgid "Could not snapshot the current logging state." msgstr "Der aktuelle Protokollierungszustand konnte nicht gesichert werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2574 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:864 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:903 +msgid "Could not write the WAN zone log option." +msgstr "Die WAN-Zonen-Log-Option konnte nicht geschrieben werden." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623 msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set." msgstr "" "Eigenes Protokoll (! zum Ausschließen). Überschreibt das Menü, wenn gesetzt." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:47 msgid "DPort" msgstr "Ziel-Port" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1184 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1231 msgid "Degraded — sampling" msgstr "Eingeschränkt — Stichproben" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1353 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1400 msgid "Degraded — sampling (slow poll RTT; cadence reduced)." msgstr "" "Eingeschränkt — Stichproben (langsame Poll-Roundtrip-Zeit; Frequenz " "reduziert)." #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:357 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45 msgid "Destination" msgstr "Ziel" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2653 msgid "Destination IP contains (! to exclude)" msgstr "Ziel-IP enthält (! zum Ausschließen)" @@ -206,41 +230,41 @@ msgstr "Ziel-IP enthält (! zum Ausschließen)" msgid "Destination port" msgstr "Ziel-Port" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2609 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658 msgid "Destination port (! to exclude)" msgstr "Ziel-Port (! zum Ausschließen)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2462 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2511 msgid "Detail" msgstr "Detail" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42 msgid "Dir" msgstr "Richtung" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:130 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:136 msgid "Disabling…" msgstr "Wird deaktiviert…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1180 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1227 msgid "Display limited to %d rows on this device" msgstr "Anzeige auf %d Zeilen auf diesem Gerät begrenzt" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2219 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2268 msgid "Display options" msgstr "Anzeigeoptionen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2311 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2360 msgid "Display options on the bar set Limit, row tint, palette, and hostnames." msgstr "" "Anzeigeoptionen in der Leiste setzen Limit, Zeilenfärbung, Palette und " "Hostnamen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:212 msgid "Does not change:" msgstr "Ändert nicht:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:223 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:229 msgid "Don’t show this again" msgstr "Nicht mehr anzeigen" @@ -248,17 +272,17 @@ msgstr "Nicht mehr anzeigen" msgid "Enable WAN drop/reject logging" msgstr "WAN-Drop/Reject-Protokollierung aktivieren" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:183 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:189 msgid "Enable WAN zone drop/reject logging (same as Network → Firewall)." msgstr "" "Drop/Reject-Protokollierung der WAN-Zone aktivieren (wie unter Netzwerk → " "Firewall)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:189 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 msgid "Enable logging" msgstr "Protokollierung aktivieren" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2307 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2356 msgid "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." @@ -268,11 +292,11 @@ msgstr "" "und protokolliert keinen normalen LAN-Verkehr." #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:60 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:189 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 msgid "Enabling…" msgstr "Wird aktiviert…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1749 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1795 msgid "Exclude" msgstr "Ausschließen" @@ -280,11 +304,11 @@ msgstr "Ausschließen" msgid "Exclude instead" msgstr "Stattdessen ausschließen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2233 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2282 msgid "Fetch budget" msgstr "Abrufbudget" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2315 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2364 msgid "" "Fetch budget controls raw log lines per poll. Auto derives from Limit; " "Manual selects a bounded maximum." @@ -293,31 +317,31 @@ msgstr "" "Limit ab; Manuell wählt ein begrenztes Maximum." #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:63 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:91 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:156 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:96 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:161 msgid "Filter by %s" msgstr "Filtern nach %s" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:129 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:134 msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings." msgstr "" "Logs nach Regel filtern (Hinweis: %s). Strg+Klick öffnet die Firewall-" "Einstellungen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2389 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2438 #: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 msgid "Firewall Live View" msgstr "Firewall-Live-Ansicht" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:48 msgid "Flags" msgstr "Flags" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:48 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:50 msgid "Flow" msgstr "Fluss" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2325 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2374 msgid "" "For a responsive table on a weak device, keep Limit at 250 rows or below. " "The weak-device cap affects rendered rows; the buffer can still retain more." @@ -326,7 +350,7 @@ msgstr "" "höchstens 250 Zeilen stehen. Die Schwachgerätebegrenzung betrifft die " "angezeigten Zeilen; der Puffer kann weiterhin mehr Zeilen behalten." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:59 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:61 msgid "Forwarded" msgstr "Weitergeleitet" @@ -334,15 +358,15 @@ msgstr "Weitergeleitet" msgid "Grant access to firewall live log view" msgstr "Zugriff auf die Live-Ansicht der Firewall-Protokolle gewähren" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2298 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2347 msgid "Help" msgstr "Hilfe" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1460 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507 msgid "Hide rows" msgstr "Zeilen ausblenden" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1516 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1562 msgid "" "High event rate — table refresh is throttled to protect the browser. The " "buffer still updates; refresh will resume automatically." @@ -351,19 +375,19 @@ msgstr "" "Browser zu schonen. Der Puffer wird weiter aktualisiert; die Anzeige wird " "automatisch fortgesetzt." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1360 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1407 msgid "Hostname resolve paused while the router is under load." msgstr "Hostnamenauflösung pausiert, solange der Router ausgelastet ist." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1740 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1786 msgid "ICMPv6" msgstr "ICMPv6" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 msgid "IN" msgstr "EINGANG" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2362 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2411 msgid "" "If Row tint looks missing, the active LuCI theme may omit success/error or " "info/warn CSS variables; fwlive falls back to local colors (air-gapped, no " @@ -373,7 +397,7 @@ msgstr "" "möglicherweise keine Erfolgs/Fehler-CSS-Variablen; fwlive fällt auf lokale " "Farben zurück (air-gapped, keine Daten verlassen das Gerät)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:324 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328 msgid "" "If the WAN is quiet, wait for probes or use the optional ping check in " "Help / the enabling-logs guide." @@ -381,7 +405,7 @@ msgstr "" "Wenn das WAN ruhig ist, warten Sie auf Probes oder nutzen Sie den optionalen " "Ping-Test in der Hilfe / der Anleitung zum Aktivieren der Protokollierung." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:57 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:59 msgid "Inbound" msgstr "Eingehend" @@ -389,65 +413,64 @@ msgstr "Eingehend" msgid "Include instead" msgstr "Stattdessen einschließen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1566 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1612 msgid "Installation is incomplete. Reinstall luci-app-fwlive." msgstr "Installation unvollständig. Installieren Sie luci-app-fwlive neu." #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:354 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39 msgid "Interface" msgstr "Schnittstelle" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2589 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 msgid "Interface (prefix ! to exclude)" msgstr "Schnittstelle (! voranstellen zum Ausschließen)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:244 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:361 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:250 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:365 msgid "I’ll configure this under Network → Firewall" msgstr "Ich konfiguriere das unter Netzwerk → Firewall" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:286 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:292 msgid "Kernel log modules missing" msgstr "Kernel-Logmodule fehlen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:290 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:296 msgid "" -"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-" -"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall." +"Kernel netfilter log modules are missing. Install kmod-nf-log and kmod-nf-" +"log6, then reload the firewall." msgstr "" -"Kernel-Netfilter-Log-Module fehlen. Installieren Sie kmod-nf-log-ipv4 und " -"kmod-nf-log-ipv6 (oder kmod-nf-log / kmod-nf-log6) und laden Sie dann die " -"Firewall neu." +"Kernel-Netfilter-Log-Module fehlen. Installieren Sie kmod-nf-log und kmod-nf-" +"log6 und laden Sie dann die Firewall neu." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:47 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:49 msgid "Len" msgstr "Länge" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2222 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2271 msgid "Limit" msgstr "Limit" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:734 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:746 msgid "Live view may be incomplete — a legacy iptables table is registered" msgstr "" "Die Live-Ansicht kann unvollständig sein — eine veraltete iptables-Tabelle " "ist registriert" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:336 msgid "Logging is off on this router" msgstr "Protokollierung ist auf diesem Router deaktiviert" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1383 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1369 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1430 msgid "Manual" msgstr "Manuell" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2251 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2300 msgid "Manual still uses server protection and poll cadence" msgstr "Manuell nutzt weiterhin Serverschutz und Abfrageintervall" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2320 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2369 msgid "" "Manual still uses server protection and poll cadence; it changes the fetch " "budget, not the polling interval." @@ -455,22 +478,22 @@ msgstr "" "Manuell nutzt weiterhin Serverschutz und Abfrageintervall; es ändert das " "Abrufbudget, nicht das Abfrageintervall." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:380 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:384 msgid "Manual test (System → Terminal):" msgstr "Manueller Test (System → Terminal):" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2245 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2294 msgid "Maximum raw lines" msgstr "Maximale Rohzeilen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:49 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:330 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2474 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2481 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:51 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:344 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2530 msgid "Message" msgstr "Nachricht" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2584 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633 msgid "More filters" msgstr "Weitere Filter" @@ -478,45 +501,45 @@ msgstr "Weitere Filter" msgid "Network → Firewall" msgstr "Netzwerk → Firewall" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:277 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:283 msgid "" "No WAN firewall zone found in /etc/config/firewall. Configure zones under:" msgstr "" "Keine WAN-Firewallzone in /etc/config/firewall gefunden. Zonen konfigurieren " "unter:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:272 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:278 msgid "No WAN zone found" msgstr "Keine WAN-Zone gefunden" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:835 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:866 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:856 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:897 msgid "No WAN zone found; cannot toggle logging without one." msgstr "" "Keine WAN-Zone gefunden; Protokollierung kann ohne eine solche nicht " "umgeschaltet werden." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:241 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:247 msgid "Not now" msgstr "Nicht jetzt" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:355 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359 msgid "Nothing changes until you click Enable." msgstr "Nichts ändert sich, bis Sie auf Aktivieren klicken." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 msgid "OUT" msgstr "AUSGANG" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2504 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2553 msgid "One line" msgstr "Einzeilig" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332 msgid "Open firewall zone settings" msgstr "Firewall-Zonen-Einstellungen öffnen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:336 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:340 msgid "" "OpenWrt does not write firewall events to the log until you turn logging on. " "Live View only shows what the firewall already logs — it does not add allow/" @@ -526,33 +549,33 @@ msgstr "" "Protokollierung aktivieren. Die Live-Ansicht zeigt nur, was die Firewall " "bereits protokolliert — sie fügt keine Allow/Deny-Regeln hinzu." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:58 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:60 msgid "Outbound" msgstr "Ausgehend" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2272 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2321 msgid "Palette" msgstr "Palette" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1615 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2428 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1661 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2477 msgid "Pause" msgstr "Pause" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1660 msgid "Paused" msgstr "Pausiert" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:355 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43 msgid "Proto" msgstr "Protokoll" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2565 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2614 msgid "Protocol — common values" msgstr "Protokoll — häufige Werte" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2544 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2593 msgid "Quick search" msgstr "Schnellsuche" @@ -560,15 +583,15 @@ msgstr "Schnellsuche" msgid "Remove filter" msgstr "Filter entfernen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1615 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1661 msgid "Resume" msgstr "Fortsetzen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2262 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2311 msgid "Row tint" msgstr "Zeileneinfärbung" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2356 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2405 msgid "" "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/" "red, default) or Accessible (teal/orange). Action text stays colored either " @@ -578,7 +601,7 @@ msgstr "" "Klassisch (grün/rot, Standard) oder Barrierefrei (teal/orange). Aktionstext " "bleibt in beiden Fällen farbig." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2412 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2461 msgid "" "Row tint used a local color fallback because the active LuCI theme did not " "apply pass/deny backgrounds." @@ -586,23 +609,23 @@ msgstr "" "Die Zeileneinfärbung verwendete einen lokalen Farbfallback, da das aktive " "LuCI-Theme keine Erlauben/Verweigern-Hintergründe anwendet." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 msgid "Rule" msgstr "Regel" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:725 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:737 msgid "Rule labels incomplete — map truncated" msgstr "Regel-Labels unvollständig — Zuordnung abgeschnitten" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:728 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:740 msgid "Rule labels unavailable" msgstr "Regel-Labels nicht verfügbar" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:727 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:739 msgid "Rule labels unavailable — temp file failed" msgstr "Regel-Labels nicht verfügbar — temporäre Datei fehlgeschlagen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46 msgid "SPort" msgstr "Quell-Port" @@ -610,49 +633,49 @@ msgstr "Quell-Port" msgid "Search" msgstr "Suche" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1351 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1398 msgid "Server adaptive protection is disabled." msgstr "Adaptiver Serverschutz ist deaktiviert." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1349 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1396 msgid "Server protection state is unknown." msgstr "Serverschutz-Status unbekannt." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1356 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1403 msgid "Server shedding — at most %d log lines per poll." msgstr "Server reduziert — höchstens %d Protokollzeilen pro Abfrage." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1358 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1405 msgid "Server truncated this poll (adaptive cap)." msgstr "Server hat diese Abfrage gekürzt (adaptives Limit)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2289 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2338 msgid "Show hostnames" msgstr "Hostnamen anzeigen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2260 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2309 msgid "Show pass/deny row background colors" msgstr "Pass/Deny-Zeilenhintergrundfarben anzeigen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1460 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2527 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2576 msgid "Show rows" msgstr "Zeilen anzeigen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1417 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1464 msgid "Showing a compact summary of the top of the shown sample." msgstr "Eine kompakte Zusammenfassung des Anfangs der angezeigten Stichprobe." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2451 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2500 msgid "Simple" msgstr "Einfach" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:356 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44 msgid "Source" msgstr "Quelle" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2594 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 msgid "Source IP contains (! to exclude)" msgstr "Quell-IP enthält (! zum Ausschließen)" @@ -660,27 +683,27 @@ msgstr "Quell-IP enthält (! zum Ausschließen)" msgid "Source port" msgstr "Quell-Port" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 msgid "Source port (! to exclude)" msgstr "Quell-Port (! zum Ausschließen)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1419 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1466 msgid "Summary data is unavailable; show rows to inspect the sample." msgstr "" "Zusammenfassungsdaten nicht verfügbar; Zeilen anzeigen, um die Stichprobe zu " "prüfen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2518 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2567 msgid "Summary mode" msgstr "Zusammenfassungsmodus" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1421 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1468 msgid "Summary was bounded before all categories could be included." msgstr "" "Die Zusammenfassung wurde begrenzt, bevor alle Kategorien aufgenommen werden " "konnten." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2330 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2379 msgid "" "Switching tabs pauses polling; returning performs one catch-up poll. " "Hostnames are off by default because lookups add work." @@ -689,14 +712,14 @@ msgstr "" "eine einmalige Aufholabfrage. Hostnamen sind standardmäßig deaktiviert, da " "Auflösungen zusätzliche Arbeit verursachen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:841 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:870 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:862 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:901 msgid "The firewall did not reload; saved and live logging may differ." msgstr "" "Die Firewall wurde nicht neu geladen; gespeicherte und aktive " "Protokollierung können voneinander abweichen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2340 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2389 msgid "" "The rate shown for WAN logging is the firewall zone log_limit. OpenWrt " "defaults to 10/minute when no explicit limit is configured; fwlive does not " @@ -706,7 +729,7 @@ msgstr "" "Standard ist 10/Minute ohne explizites Limit; fwlive setzt diese Grenze " "nicht." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2335 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2384 msgid "" "The router log ring may evict older events before fwlive reads them. fwlive " "cannot recover evicted entries or change forwarding behavior." @@ -715,7 +738,7 @@ msgstr "" "fwlive sie liest. fwlive kann verdrängte Einträge nicht wiederherstellen und " "ändert das Weiterleitungsverhalten nicht." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2302 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2351 msgid "" "The table updates automatically when your firewall logs traffic. Use Pause " "if it moves too fast." @@ -723,44 +746,44 @@ msgstr "" "Die Tabelle aktualisiert sich automatisch, wenn Ihre Firewall Verkehr " "protokolliert. Pause nutzen, wenn es zu schnell wird." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2415 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2464 msgid "Theme tint fallback" msgstr "Theme-Farb-Fallback" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:388 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:392 msgid "Then ping the router." msgstr "Dann den Router anpingen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:306 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:310 msgid "" "This router reported a logging blocker that Live View does not recognize yet." msgstr "" "Dieser Router hat eine Protokollierungsblockade gemeldet, die Live View noch " "nicht kennt." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36 msgid "Time" msgstr "Zeit" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1426 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 msgid "Top drops" msgstr "Häufigste Drops" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1429 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1476 msgid "Top rules" msgstr "Häufigste Regeln" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1423 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1470 msgid "Top talkers" msgstr "Häufigste Absender" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:213 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219 msgid "Turn it back off with the WAN logging control on the watch strip." msgstr "" "Über das WAN-Protokollierungs-Steuerelement in der Beobachtungsleiste wieder " "deaktivieren." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:350 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:354 msgid "" "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → " "Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal " @@ -770,31 +793,31 @@ msgstr "" "Firewall → wan → Protokoll). Ratenbegrenzt durch das log_limit der Zone " "(OpenWrt-Standard: 10/Minute). Normales LAN-Surfen wird nicht protokolliert." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:209 msgid "Turns on logging for the WAN firewall zone and reloads the firewall." msgstr "" "Aktiviert die Protokollierung für die WAN-Firewallzone und lädt die Firewall " "neu." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:217 msgid "Undo:" msgstr "Rückgängig:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2359 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2408 msgid "Use Detail for all columns (flags, length, raw message)." msgstr "" "Verwenden Sie „Detail“ für alle Spalten (Flags, Länge, rohe Nachricht)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2433 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2439 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2482 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2488 msgid "View" msgstr "Ansicht" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:873 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:914 msgid "WAN drop/reject logging is off." msgstr "WAN-Drop/Reject-Protokollierung ist aus." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:317 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321 msgid "" "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "here. Normal LAN browsing will not." @@ -802,7 +825,7 @@ msgstr "" "WAN-Drop/Reject-Protokollierung ist an. Blockierter eingehender WAN-Verkehr " "erscheint hier. Normales LAN-Surfen nicht." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:847 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:878 msgid "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." @@ -810,58 +833,58 @@ msgstr "" "WAN-Drop/Reject-Protokollierung ist an. Blockierter eingehender Verkehr " "sollte hier in Echtzeit erscheinen — nicht normales LAN-Surfen." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:849 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:880 msgid "WAN logging is already enabled." msgstr "WAN-Protokollierung ist bereits aktiviert." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:133 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:139 msgid "WAN logging on" msgstr "WAN-Protokollierung: EIN" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:142 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:148 msgid "WAN logging on (%s). Click to disable." msgstr "WAN-Protokollierung: EIN (%s). Klicken zum Deaktivieren." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:307 msgid "WAN logging unavailable" msgstr "WAN-Protokollierung nicht verfügbar" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:112 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118 msgid "WAN logging unavailable: missing kernel log modules" msgstr "WAN-Protokollierung nicht verfügbar: fehlende Kernel-Log-Module" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:102 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:108 msgid "WAN logging unavailable: no WAN zone" msgstr "WAN-Protokollierung nicht verfügbar: keine WAN-Zone" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:317 msgid "Waiting for firewall events" msgstr "Warte auf Firewall-Ereignisse" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2403 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1660 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2452 msgid "Watching" msgstr "Beobachtet" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2493 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2542 msgid "Wrap" msgstr "Umbrechen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:69 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2549 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598 msgid "block" msgstr "blockiert" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1171 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1218 msgid "buffer filling" msgstr "Puffer füllt sich" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1175 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1222 msgid "buffer full" msgstr "Puffer voll" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:127 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:133 msgid "default 10/minute" msgstr "Standard 10/Minute" @@ -869,8 +892,8 @@ msgstr "Standard 10/Minute" msgid "does not contain" msgstr "enthält nicht" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:70 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2550 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:72 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599 msgid "drop" msgstr "verworfen" @@ -878,16 +901,16 @@ msgstr "verworfen" msgid "is" msgstr "ist" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1170 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1217 msgid "loading buffer" msgstr "Lade Puffer" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1546 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1592 msgid "loading…" msgstr "lädt…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1399 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1408 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1446 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1455 msgid "none" msgstr "keine" @@ -895,101 +918,101 @@ msgstr "keine" msgid "not" msgstr "nicht" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1757 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803 msgid "not AH" msgstr "nicht AH" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1756 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802 msgid "not ESP" msgstr "nicht ESP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1755 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801 msgid "not GRE" msgstr "nicht GRE" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1752 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1798 msgid "not ICMP" msgstr "nicht ICMP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1753 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1799 msgid "not ICMPv6" msgstr "nicht ICMPv6" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1754 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1800 msgid "not IGMP" msgstr "nicht IGMP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1758 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804 msgid "not SCTP" msgstr "nicht SCTP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1750 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1796 msgid "not TCP" msgstr "nicht TCP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1751 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1797 msgid "not UDP" msgstr "nicht UDP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2555 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604 msgid "not block" msgstr "nicht blockieren" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2554 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2603 msgid "not drop" msgstr "nicht verwerfen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2553 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2602 msgid "not pass" msgstr "nicht erlauben" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2556 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2605 msgid "not reject" msgstr "nicht zurückweisen" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2557 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2606 msgid "not unknown" msgstr "nicht unbekannt" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2572 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2621 msgid "or type…" msgstr "oder tippen…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:68 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:70 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2597 msgid "pass" msgstr "erlaubt" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2551 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2600 msgid "reject" msgstr "abgelehnt" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1177 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1224 msgid "render paused (high rate)" msgstr "Rendering pausiert (hohe Rate)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1186 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1233 msgid "resolve paused (load)" msgstr "Auflösung pausiert (Last)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1188 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1235 msgid "scroll frozen — scroll to top to follow live" msgstr "Scrollen eingefroren — nach oben scrollen für Live-Ansicht" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1185 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1232 msgid "truncated" msgstr "gekürzt" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:60 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2601 msgid "unknown" msgstr "unbekannt" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:709 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:721 msgid "using fw4" msgstr "mit fw4" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:134 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:140 msgid "· %s" msgstr "· %s" diff --git a/luci-app-fwlive/po/ru/luci-app-fwlive.po b/luci-app-fwlive/po/ru/luci-app-fwlive.po index 5b3321c1d..89fa2129e 100644 --- a/luci-app-fwlive/po/ru/luci-app-fwlive.po +++ b/luci-app-fwlive/po/ru/luci-app-fwlive.po @@ -13,15 +13,15 @@ msgstr "" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1548 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1594 msgid "%d matching · %d/%d stored" msgstr "%d совпад. · сохранено %d/%d" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1341 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1388 msgid "%s · requested up to %d raw lines · %d firewall messages returned" msgstr "%s · запрошено до %d сырых строк · возвращено %d сообщений firewall" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1331 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1378 msgid "" "%s · requested up to %d raw lines · server limited fetch to %d · %d firewall " "messages returned" @@ -33,73 +33,82 @@ msgstr "" msgid "%s: %s" msgstr "%s: %s" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1550 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596 msgid "0 matching · %d/%d stored" msgstr "0 совпад. · сохранено %d/%d" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:440 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:447 msgid "Accessible (teal/orange)" msgstr "Доступный (бирюзовый/оранжевый)" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:353 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37 msgid "Action" msgstr "Действие" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:874 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:915 msgid "Administrator access is required to disable logging." msgstr "Для отключения журналирования требуются права администратора." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:851 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:882 msgid "Administrator access is required to enable logging." msgstr "Для включения журналирования требуются права администратора." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:208 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:214 msgid "Allow/deny rules, LAN logging, or anything else." msgstr "Правила allow/deny, журналирование LAN и любые другие настройки." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1742 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1788 msgid "Also seen" msgstr "Также встречаются" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:863 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:853 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:894 msgid "Another change is staged for the firewall; apply or revert it first." msgstr "" "В брандмауэре уже есть другое незафиксированное изменение; сначала примените " "или отмените его." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2547 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:910 +msgid "" +"Another change overwrote WAN logging after it was saved; check the current " +"state." +msgstr "" +"Другое изменение перезаписало журналирование WAN после сохранения; проверьте " +"текущее состояние." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2596 msgid "Any action" msgstr "Любое действие" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1735 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1781 msgid "Any protocol" msgstr "Любой протокол" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1382 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1369 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1429 msgid "Auto" msgstr "Авто" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:198 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204 msgid "Before you enable logging" msgstr "Перед включением журналирования" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:829 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:850 msgid "Cannot enable logging until kernel log modules are installed." msgstr "" "Невозможно включить журналирование, пока не установлены модули журнала ядра." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:201 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:207 msgid "Changes:" msgstr "Изменения:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:439 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:446 msgid "Classic (green/red)" msgstr "Классический (зелёный/красный)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2278 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2327 msgid "Classic uses green/red; Accessible uses teal/orange" msgstr "Классический — зелёный/красный; доступный — бирюзовый/оранжевый" @@ -107,7 +116,7 @@ msgstr "Классический — зелёный/красный; доступ msgid "Clear all" msgstr "Очистить всё" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2619 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2668 msgid "" "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for " "firewall settings · in Simple view, click a row for the full message" @@ -116,7 +125,7 @@ msgstr "" "правилу для настроек МЭ · в Простом виде щёлкните строку для полного " "сообщения" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2346 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2395 msgid "" "Click a row (Time or other non-link cells) to see the full log line (Simple " "view)." @@ -124,11 +133,11 @@ msgstr "" "Щёлкните строку (Время или другие ячейки без ссылки), чтобы увидеть полную " "строку журнала (Простой вид)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:156 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:158 msgid "Click a row for the full message" msgstr "Щёлкните строку для полного сообщения" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2351 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2400 msgid "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." @@ -136,63 +145,78 @@ msgstr "" "Щёлкните по IP, действию или протоколу для фильтрации; для исключения " "используйте меню «Протокол» (или ≠ на чипе)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1736 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1782 msgid "Common" msgstr "Частые" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1567 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1613 msgid "Connection lost — retrying…" msgstr "Соединение потеряно — повторная попытка…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:837 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:868 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:858 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:899 msgid "Could not acquire the logging lock." msgstr "Не удалось получить блокировку журналирования." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:866 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:905 +msgid "Could not clear the WAN zone log option." +msgstr "Не удалось очистить параметр журнала зоны WAN." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:912 msgid "Could not disable logging." msgstr "Не удалось отключить журналирование." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:842 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:873 msgid "Could not enable logging." msgstr "Не удалось включить журналирование." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:769 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:784 msgid "Could not load logging status." msgstr "Не удалось загрузить состояние журналирования." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:768 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:783 msgid "Could not refresh logging status; showing the last known state." msgstr "" "Не удалось обновить состояние журналирования; показан последний известный " "статус." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:839 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:868 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907 +msgid "Could not save the firewall configuration." +msgstr "Не удалось сохранить конфигурацию межсетевого экрана." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:860 msgid "Could not snapshot the current logging state." msgstr "Не удалось сохранить снимок текущего состояния журналирования." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2574 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:864 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:903 +msgid "Could not write the WAN zone log option." +msgstr "Не удалось записать параметр журнала зоны WAN." + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623 msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set." msgstr "Свой протокол (! для исключения). Переопределяет меню, если задан." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:47 msgid "DPort" msgstr "Порт назн." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1184 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1231 msgid "Degraded — sampling" msgstr "Упрощённый режим — выборка" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1353 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1400 msgid "Degraded — sampling (slow poll RTT; cadence reduced)." msgstr "Упрощённый режим — выборка (медленный RTT опроса; интервал увеличен)." #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:357 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45 msgid "Destination" msgstr "Назначение" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2653 msgid "Destination IP contains (! to exclude)" msgstr "IP назначения содержит (! для исключения)" @@ -200,41 +224,41 @@ msgstr "IP назначения содержит (! для исключения) msgid "Destination port" msgstr "Порт назначения" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2609 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658 msgid "Destination port (! to exclude)" msgstr "Порт назначения (! для исключения)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2462 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2511 msgid "Detail" msgstr "Подробно" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42 msgid "Dir" msgstr "Напр." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:130 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:136 msgid "Disabling…" msgstr "Отключение…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1180 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1227 msgid "Display limited to %d rows on this device" msgstr "На этом устройстве отображение ограничено %d строками" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2219 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2268 msgid "Display options" msgstr "Параметры отображения" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2311 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2360 msgid "Display options on the bar set Limit, row tint, palette, and hostnames." msgstr "" "Параметры отображения на панели задают лимит, окраску строк, палитру и имена " "узлов." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:212 msgid "Does not change:" msgstr "Не изменяет:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:223 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:229 msgid "Don’t show this again" msgstr "Больше не показывать" @@ -242,17 +266,17 @@ msgstr "Больше не показывать" msgid "Enable WAN drop/reject logging" msgstr "Включить журналирование drop/reject для WAN" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:183 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:189 msgid "Enable WAN zone drop/reject logging (same as Network → Firewall)." msgstr "" "Включить журналирование drop/reject для WAN-зоны (как в Сеть → Межсетевой " "экран)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:189 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 msgid "Enable logging" msgstr "Включить журнал" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2307 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2356 msgid "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." @@ -262,11 +286,11 @@ msgstr "" "журналирует обычный LAN-трафик." #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:60 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:189 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 msgid "Enabling…" msgstr "Включение…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1749 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1795 msgid "Exclude" msgstr "Исключить" @@ -274,11 +298,11 @@ msgstr "Исключить" msgid "Exclude instead" msgstr "Исключить вместо этого" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2233 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2282 msgid "Fetch budget" msgstr "Бюджет выборки" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2315 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2364 msgid "" "Fetch budget controls raw log lines per poll. Auto derives from Limit; " "Manual selects a bounded maximum." @@ -287,31 +311,31 @@ msgstr "" "из лимита; «Вручную» задаёт ограниченный максимум." #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:63 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:91 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:156 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:96 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:161 msgid "Filter by %s" msgstr "Фильтр по %s" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:129 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:134 msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings." msgstr "" "Фильтр журналов по правилу (подсказка: %s). Ctrl+клик открывает настройки " "файрвола." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2389 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2438 #: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 msgid "Firewall Live View" msgstr "Просмотр файрвола в реальном времени" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:48 msgid "Flags" msgstr "Флаги" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:48 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:50 msgid "Flow" msgstr "Поток" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2325 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2374 msgid "" "For a responsive table on a weak device, keep Limit at 250 rows or below. " "The weak-device cap affects rendered rows; the buffer can still retain more." @@ -320,7 +344,7 @@ msgstr "" "строк. Ограничение слабого устройства влияет на отображаемые строки; буфер " "всё ещё может хранить больше." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:59 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:61 msgid "Forwarded" msgstr "Транзитный" @@ -328,15 +352,15 @@ msgstr "Транзитный" msgid "Grant access to firewall live log view" msgstr "Предоставить доступ к живому просмотру журнала межсетевого экрана" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2298 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2347 msgid "Help" msgstr "Справка" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1460 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507 msgid "Hide rows" msgstr "Скрыть строки" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1516 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1562 msgid "" "High event rate — table refresh is throttled to protect the browser. The " "buffer still updates; refresh will resume automatically." @@ -344,19 +368,19 @@ msgstr "" "Высокая частота событий — обновление таблицы ограничено для защиты браузера. " "Буфер продолжает обновляться; отображение возобновится автоматически." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1360 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1407 msgid "Hostname resolve paused while the router is under load." msgstr "Разрешение имён приостановлено из-за нагрузки на маршрутизатор." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1740 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1786 msgid "ICMPv6" msgstr "ICMPv6" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 msgid "IN" msgstr "ВХОД" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2362 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2411 msgid "" "If Row tint looks missing, the active LuCI theme may omit success/error or " "info/warn CSS variables; fwlive falls back to local colors (air-gapped, no " @@ -366,7 +390,7 @@ msgstr "" "содержать CSS-переменные success/error или info/warn; fwlive использует " "локальные цвета (работает автономно, данные не покидают устройство)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:324 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328 msgid "" "If the WAN is quiet, wait for probes or use the optional ping check in " "Help / the enabling-logs guide." @@ -374,7 +398,7 @@ msgstr "" "Если WAN неактивен, дождитесь проб или используйте необязательную проверку " "ping в Справке / руководстве по включению журналов." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:57 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:59 msgid "Inbound" msgstr "Входящий" @@ -382,65 +406,64 @@ msgstr "Входящий" msgid "Include instead" msgstr "Включить вместо этого" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1566 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1612 msgid "Installation is incomplete. Reinstall luci-app-fwlive." msgstr "Установка не завершена. Переустановите luci-app-fwlive." #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:354 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39 msgid "Interface" msgstr "Интерфейс" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2589 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 msgid "Interface (prefix ! to exclude)" msgstr "Интерфейс (! в начале для исключения)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:244 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:361 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:250 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:365 msgid "I’ll configure this under Network → Firewall" msgstr "Я настрою это в разделе Сеть → Межсетевой экран" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:286 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:292 msgid "Kernel log modules missing" msgstr "Отсутствуют модули журналирования ядра" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:290 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:296 msgid "" -"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-" -"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall." +"Kernel netfilter log modules are missing. Install kmod-nf-log and kmod-nf-" +"log6, then reload the firewall." msgstr "" -"Отсутствуют модули журнала netfilter ядра. Установите kmod-nf-log-ipv4 и " -"kmod-nf-log-ipv6 (или kmod-nf-log / kmod-nf-log6), затем перезагрузите " -"файрвол." +"Отсутствуют модули журнала netfilter ядра. Установите kmod-nf-log и kmod-nf-" +"log6, затем перезагрузите файрвол." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:47 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:49 msgid "Len" msgstr "Длина" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2222 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2271 msgid "Limit" msgstr "Лимит" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:734 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:746 msgid "Live view may be incomplete — a legacy iptables table is registered" msgstr "" "Живой просмотр может быть неполным — зарегистрирована устаревшая таблица " "iptables" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:336 msgid "Logging is off on this router" msgstr "Журналирование на этом роутере отключено" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1383 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1369 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1430 msgid "Manual" msgstr "Вручную" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2251 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2300 msgid "Manual still uses server protection and poll cadence" msgstr "«Вручную» по-прежнему использует защиту сервера и частоту опроса" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2320 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2369 msgid "" "Manual still uses server protection and poll cadence; it changes the fetch " "budget, not the polling interval." @@ -448,22 +471,22 @@ msgstr "" "«Вручную» по-прежнему использует защиту сервера и частоту опроса; меняется " "бюджет выборки, а не интервал опроса." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:380 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:384 msgid "Manual test (System → Terminal):" msgstr "Ручной тест (Система → Терминал):" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2245 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2294 msgid "Maximum raw lines" msgstr "Максимум сырых строк" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:49 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:330 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2474 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2481 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:51 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:344 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2530 msgid "Message" msgstr "Сообщение" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2584 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633 msgid "More filters" msgstr "Больше фильтров" @@ -471,43 +494,43 @@ msgstr "Больше фильтров" msgid "Network → Firewall" msgstr "Сеть → Межсетевой экран" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:277 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:283 msgid "" "No WAN firewall zone found in /etc/config/firewall. Configure zones under:" msgstr "" "WAN-зона межсетевого экрана не найдена в /etc/config/firewall. Настройте " "зоны в разделе:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:272 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:278 msgid "No WAN zone found" msgstr "WAN-зона не найдена" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:835 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:866 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:856 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:897 msgid "No WAN zone found; cannot toggle logging without one." msgstr "WAN-зона не найдена; без неё нельзя переключить журналирование." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:241 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:247 msgid "Not now" msgstr "Не сейчас" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:355 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359 msgid "Nothing changes until you click Enable." msgstr "Ничего не изменится, пока вы не нажмёте «Включить»." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 msgid "OUT" msgstr "ВЫХОД" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2504 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2553 msgid "One line" msgstr "Одна строка" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332 msgid "Open firewall zone settings" msgstr "Открыть настройки зон файрвола" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:336 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:340 msgid "" "OpenWrt does not write firewall events to the log until you turn logging on. " "Live View only shows what the firewall already logs — it does not add allow/" @@ -517,33 +540,33 @@ msgstr "" "журналирование. Живой просмотр показывает только то, что firewall уже " "журналирует — он не добавляет правила allow/deny." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:58 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:60 msgid "Outbound" msgstr "Исходящий" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2272 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2321 msgid "Palette" msgstr "Палитра" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1615 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2428 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1661 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2477 msgid "Pause" msgstr "Пауза" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1660 msgid "Paused" msgstr "Пауза" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:355 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43 msgid "Proto" msgstr "Протокол" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2565 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2614 msgid "Protocol — common values" msgstr "Протокол — частые значения" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2544 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2593 msgid "Quick search" msgstr "Быстрый поиск" @@ -551,15 +574,15 @@ msgstr "Быстрый поиск" msgid "Remove filter" msgstr "Удалить фильтр" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1615 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1661 msgid "Resume" msgstr "Продолжить" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2262 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2311 msgid "Row tint" msgstr "Цвет строк" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2356 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2405 msgid "" "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/" "red, default) or Accessible (teal/orange). Action text stays colored either " @@ -569,7 +592,7 @@ msgstr "" "классический (зелёный/красный, по умолчанию) или доступный (бирюзовый/" "оранжевый). Цвет текста действия сохраняется в любом случае." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2412 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2461 msgid "" "Row tint used a local color fallback because the active LuCI theme did not " "apply pass/deny backgrounds." @@ -577,23 +600,23 @@ msgstr "" "Цветовая маркировка строк использовала локальный запасной цвет, так как " "активная тема LuCI не применяет фон пропуска/отказа." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 msgid "Rule" msgstr "Правило" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:725 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:737 msgid "Rule labels incomplete — map truncated" msgstr "Метки правил неполные — таблица обрезана" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:728 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:740 msgid "Rule labels unavailable" msgstr "Метки правил недоступны" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:727 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:739 msgid "Rule labels unavailable — temp file failed" msgstr "Метки правил недоступны — ошибка временного файла" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46 msgid "SPort" msgstr "Порт ист." @@ -601,49 +624,49 @@ msgstr "Порт ист." msgid "Search" msgstr "Поиск" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1351 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1398 msgid "Server adaptive protection is disabled." msgstr "Адаптивная защита сервера отключена." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1349 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1396 msgid "Server protection state is unknown." msgstr "Состояние защиты сервера неизвестно." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1356 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1403 msgid "Server shedding — at most %d log lines per poll." msgstr "Сервер ограничивает выдачу — не более %d строк журнала за опрос." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1358 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1405 msgid "Server truncated this poll (adaptive cap)." msgstr "Сервер сократил этот опрос (адаптивный предел)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2289 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2338 msgid "Show hostnames" msgstr "Показывать имена узлов" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2260 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2309 msgid "Show pass/deny row background colors" msgstr "Показывать цвета фона строк pass/deny" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1460 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2527 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2576 msgid "Show rows" msgstr "Показать строки" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1417 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1464 msgid "Showing a compact summary of the top of the shown sample." msgstr "Показана краткая сводка верхней части отображаемой выборки." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2451 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2500 msgid "Simple" msgstr "Простой" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:356 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44 msgid "Source" msgstr "Источник" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2594 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 msgid "Source IP contains (! to exclude)" msgstr "IP источника содержит (! для исключения)" @@ -651,23 +674,23 @@ msgstr "IP источника содержит (! для исключения)" msgid "Source port" msgstr "Порт источника" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 msgid "Source port (! to exclude)" msgstr "Порт источника (! для исключения)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1419 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1466 msgid "Summary data is unavailable; show rows to inspect the sample." msgstr "Данные сводки недоступны; покажите строки для проверки выборки." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2518 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2567 msgid "Summary mode" msgstr "Режим сводки" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1421 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1468 msgid "Summary was bounded before all categories could be included." msgstr "Сводка ограничена до включения всех категорий." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2330 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2379 msgid "" "Switching tabs pauses polling; returning performs one catch-up poll. " "Hostnames are off by default because lookups add work." @@ -676,14 +699,14 @@ msgstr "" "один догоняющий опрос. Имена хостов по умолчанию отключены, поскольку их " "разрешение создаёт дополнительную нагрузку." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:841 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:870 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:862 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:901 msgid "The firewall did not reload; saved and live logging may differ." msgstr "" "Брандмауэр не перезагрузился; сохранённые и активные настройки " "журналирования могут различаться." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2340 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2389 msgid "" "The rate shown for WAN logging is the firewall zone log_limit. OpenWrt " "defaults to 10/minute when no explicit limit is configured; fwlive does not " @@ -692,7 +715,7 @@ msgstr "" "Показанная частота WAN-журнала — log_limit зоны. По умолчанию OpenWrt: 10/" "мин без явного лимита; fwlive сам лимит не задаёт." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2335 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2384 msgid "" "The router log ring may evict older events before fwlive reads them. fwlive " "cannot recover evicted entries or change forwarding behavior." @@ -701,7 +724,7 @@ msgstr "" "fwlive их прочитает. fwlive не может восстановить вытесненные записи и не " "изменяет пересылку." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2302 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2351 msgid "" "The table updates automatically when your firewall logs traffic. Use Pause " "if it moves too fast." @@ -709,43 +732,43 @@ msgstr "" "Таблица обновляется автоматически, когда файрвол пишет в журнал. Используйте " "Паузу, если слишком быстро." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2415 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2464 msgid "Theme tint fallback" msgstr "Запасной цвет темы" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:388 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:392 msgid "Then ping the router." msgstr "Затем отправьте ping на роутер." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:306 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:310 msgid "" "This router reported a logging blocker that Live View does not recognize yet." msgstr "" "Этот маршрутизатор сообщил о блоке журналирования, который Live View пока не " "распознаёт." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36 msgid "Time" msgstr "Время" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1426 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 msgid "Top drops" msgstr "Частые сбросы" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1429 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1476 msgid "Top rules" msgstr "Частые правила" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1423 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1470 msgid "Top talkers" msgstr "Частые источники" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:213 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219 msgid "Turn it back off with the WAN logging control on the watch strip." msgstr "" "Отключите её снова элементом управления журналом WAN на панели наблюдения." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:350 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:354 msgid "" "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → " "Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal " @@ -755,31 +778,31 @@ msgstr "" "экран → wan → Журнал). Ограничено log_limit зоны (по умолчанию в OpenWrt 10/" "мин). Обычный LAN-трафик не журналируется." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:209 msgid "Turns on logging for the WAN firewall zone and reloads the firewall." msgstr "" "Включает журналирование для WAN-зоны межсетевого экрана и перезагружает " "firewall." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:217 msgid "Undo:" msgstr "Отмена:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2359 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2408 msgid "Use Detail for all columns (flags, length, raw message)." msgstr "" "Используйте «Подробно» для всех столбцов (флаги, длина, исходное сообщение)." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2433 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2439 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2482 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2488 msgid "View" msgstr "Вид" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:873 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:914 msgid "WAN drop/reject logging is off." msgstr "Журналирование drop/reject для WAN отключено." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:317 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321 msgid "" "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "here. Normal LAN browsing will not." @@ -787,7 +810,7 @@ msgstr "" "Журналирование drop/reject для WAN включено. Заблокированный входящий WAN-" "трафик будет отображаться здесь. Обычный LAN-трафик — нет." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:847 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:878 msgid "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." @@ -796,58 +819,58 @@ msgstr "" "должен появляться здесь по мере поступления — обычный LAN-трафик не " "отображается." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:849 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:880 msgid "WAN logging is already enabled." msgstr "WAN-журналирование уже включено." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:133 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:139 msgid "WAN logging on" msgstr "WAN-журнал: ВКЛ" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:142 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:148 msgid "WAN logging on (%s). Click to disable." msgstr "WAN-журнал: ВКЛ (%s). Нажмите, чтобы отключить." -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:307 msgid "WAN logging unavailable" msgstr "WAN-журналирование недоступно" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:112 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118 msgid "WAN logging unavailable: missing kernel log modules" msgstr "WAN-журналирование недоступно: отсутствуют модули журнала ядра" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:102 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:108 msgid "WAN logging unavailable: no WAN zone" msgstr "WAN-журналирование недоступно: нет зоны WAN" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:317 msgid "Waiting for firewall events" msgstr "Ожидание событий межсетевого экрана" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2403 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1660 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2452 msgid "Watching" msgstr "Наблюдение" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2493 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2542 msgid "Wrap" msgstr "Перенос" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:69 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2549 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598 msgid "block" msgstr "заблокирован" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1171 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1218 msgid "buffer filling" msgstr "буфер заполняется" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1175 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1222 msgid "buffer full" msgstr "буфер заполнен" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:127 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:133 msgid "default 10/minute" msgstr "по умолчанию 10/мин" @@ -855,8 +878,8 @@ msgstr "по умолчанию 10/мин" msgid "does not contain" msgstr "не содержит" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:70 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2550 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:72 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599 msgid "drop" msgstr "отброшен" @@ -864,16 +887,16 @@ msgstr "отброшен" msgid "is" msgstr "есть" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1170 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1217 msgid "loading buffer" msgstr "загрузка буфера" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1546 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1592 msgid "loading…" msgstr "загрузка…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1399 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1408 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1446 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1455 msgid "none" msgstr "нет" @@ -881,102 +904,102 @@ msgstr "нет" msgid "not" msgstr "не" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1757 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803 msgid "not AH" msgstr "не AH" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1756 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802 msgid "not ESP" msgstr "не ESP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1755 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801 msgid "not GRE" msgstr "не GRE" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1752 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1798 msgid "not ICMP" msgstr "не ICMP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1753 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1799 msgid "not ICMPv6" msgstr "не ICMPv6" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1754 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1800 msgid "not IGMP" msgstr "не IGMP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1758 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804 msgid "not SCTP" msgstr "не SCTP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1750 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1796 msgid "not TCP" msgstr "не TCP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1751 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1797 msgid "not UDP" msgstr "не UDP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2555 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604 msgid "not block" msgstr "не блокировать" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2554 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2603 msgid "not drop" msgstr "не отбрасывать" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2553 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2602 msgid "not pass" msgstr "не пропускать" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2556 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2605 msgid "not reject" msgstr "не отклонять" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2557 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2606 msgid "not unknown" msgstr "не неизвестно" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2572 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2621 msgid "or type…" msgstr "или введите…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:68 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:70 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2597 msgid "pass" msgstr "разрешён" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2551 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2600 msgid "reject" msgstr "отклонён" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1177 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1224 msgid "render paused (high rate)" msgstr "рендеринг приостановлен (высокая частота)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1186 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1233 msgid "resolve paused (load)" msgstr "Разрешение приостановлено (нагрузка)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1188 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1235 msgid "scroll frozen — scroll to top to follow live" msgstr "" "прокрутка заморожена — прокрутите вверх, чтобы следить в реальном времени" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1185 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1232 msgid "truncated" msgstr "обрезано" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:60 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2601 msgid "unknown" msgstr "неизвестно" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:709 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:721 msgid "using fw4" msgstr "используется fw4" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:134 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:140 msgid "· %s" msgstr "· %s" diff --git a/luci-app-fwlive/po/templates/luci-app-fwlive.pot b/luci-app-fwlive/po/templates/luci-app-fwlive.pot index 3ef7bd75e..85a5ede12 100644 --- a/luci-app-fwlive/po/templates/luci-app-fwlive.pot +++ b/luci-app-fwlive/po/templates/luci-app-fwlive.pot @@ -1,15 +1,15 @@ msgid "" msgstr "Content-Type: text/plain; charset=UTF-8" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1557 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1594 msgid "%d matching · %d/%d stored" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1350 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1388 msgid "%s · requested up to %d raw lines · %d firewall messages returned" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1339 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1378 msgid "" "%s · requested up to %d raw lines · server limited fetch to %d · %d firewall " "messages returned" @@ -19,11 +19,11 @@ msgstr "" msgid "%s: %s" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1559 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596 msgid "0 matching · %d/%d stored" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:446 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:447 msgid "Accessible (teal/orange)" msgstr "" @@ -32,57 +32,64 @@ msgstr "" msgid "Action" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:882 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:915 msgid "Administrator access is required to disable logging." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:859 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:882 msgid "Administrator access is required to enable logging." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:208 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:214 msgid "Allow/deny rules, LAN logging, or anything else." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1751 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1788 msgid "Also seen" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:839 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:870 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:853 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:894 msgid "Another change is staged for the firewall; apply or revert it first." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2559 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:910 +msgid "" +"Another change overwrote WAN logging after it was saved; check the current " +"state." +msgstr "" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2596 msgid "Any action" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1744 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1781 msgid "Any protocol" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1331 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1391 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1369 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1429 msgid "Auto" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:198 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204 msgid "Before you enable logging" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:837 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:850 msgid "Cannot enable logging until kernel log modules are installed." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:201 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:207 msgid "Changes:" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:445 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:446 msgid "Classic (green/red)" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2290 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2327 msgid "Classic uses green/red; Accessible uses teal/orange" msgstr "" @@ -90,13 +97,13 @@ msgstr "" msgid "Clear all" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2630 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2668 msgid "" "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for " "firewall settings · in Simple view, click a row for the full message" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2357 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2395 msgid "" "Click a row (Time or other non-link cells) to see the full log line (Simple " "view)." @@ -106,46 +113,61 @@ msgstr "" msgid "Click a row for the full message" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2362 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2400 msgid "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1745 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1782 msgid "Common" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1576 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1613 msgid "Connection lost — retrying…" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:845 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:876 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:858 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:899 msgid "Could not acquire the logging lock." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:879 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:866 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:905 +msgid "Could not clear the WAN zone log option." +msgstr "" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:912 msgid "Could not disable logging." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:850 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:873 msgid "Could not enable logging." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:777 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:784 msgid "Could not load logging status." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:776 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:783 msgid "Could not refresh logging status; showing the last known state." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:847 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:868 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907 +msgid "Could not save the firewall configuration." +msgstr "" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:860 msgid "Could not snapshot the current logging state." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2585 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:864 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:903 +msgid "Could not write the WAN zone log option." +msgstr "" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623 msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set." msgstr "" @@ -153,11 +175,11 @@ msgstr "" msgid "DPort" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1193 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1231 msgid "Degraded — sampling" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1362 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1400 msgid "Degraded — sampling (slow poll RTT; cadence reduced)." msgstr "" @@ -166,7 +188,7 @@ msgstr "" msgid "Destination" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2616 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2653 msgid "Destination IP contains (! to exclude)" msgstr "" @@ -174,11 +196,11 @@ msgstr "" msgid "Destination port" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2621 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658 msgid "Destination port (! to exclude)" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2474 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2511 msgid "Detail" msgstr "" @@ -186,27 +208,27 @@ msgstr "" msgid "Dir" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:130 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:136 msgid "Disabling…" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1189 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1227 msgid "Display limited to %d rows on this device" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2231 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2268 msgid "Display options" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2323 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2360 msgid "Display options on the bar set Limit, row tint, palette, and hostnames." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:212 msgid "Does not change:" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:223 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:229 msgid "Don’t show this again" msgstr "" @@ -214,26 +236,26 @@ msgstr "" msgid "Enable WAN drop/reject logging" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:183 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:189 msgid "Enable WAN zone drop/reject logging (same as Network → Firewall)." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:189 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 msgid "Enable logging" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2318 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2356 msgid "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:60 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:189 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 msgid "Enabling…" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1758 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1795 msgid "Exclude" msgstr "" @@ -241,11 +263,11 @@ msgstr "" msgid "Exclude instead" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2245 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2282 msgid "Fetch budget" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2326 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2364 msgid "" "Fetch budget controls raw log lines per poll. Auto derives from Limit; " "Manual selects a bounded maximum." @@ -257,11 +279,11 @@ msgstr "" msgid "Filter by %s" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:133 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:134 msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2401 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2438 #: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 msgid "Firewall Live View" msgstr "" @@ -274,7 +296,7 @@ msgstr "" msgid "Flow" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2336 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2374 msgid "" "For a responsive table on a weak device, keep Limit at 250 rows or below. " "The weak-device cap affects rendered rows; the buffer can still retain more." @@ -288,25 +310,25 @@ msgstr "" msgid "Grant access to firewall live log view" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2310 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2347 msgid "Help" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1469 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507 msgid "Hide rows" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1524 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1562 msgid "" "High event rate — table refresh is throttled to protect the browser. The " "buffer still updates; refresh will resume automatically." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1369 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1407 msgid "Hostname resolve paused while the router is under load." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1749 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1786 msgid "ICMPv6" msgstr "" @@ -314,14 +336,14 @@ msgstr "" msgid "IN" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2373 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2411 msgid "" "If Row tint looks missing, the active LuCI theme may omit success/error or " "info/warn CSS variables; fwlive falls back to local colors (air-gapped, no " "data leaves the device)." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:323 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328 msgid "" "If the WAN is quiet, wait for probes or use the optional ping check in " "Help / the enabling-logs guide." @@ -335,7 +357,7 @@ msgstr "" msgid "Include instead" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1575 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1612 msgid "Installation is incomplete. Reinstall luci-app-fwlive." msgstr "" @@ -344,72 +366,72 @@ msgstr "" msgid "Interface" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2601 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 msgid "Interface (prefix ! to exclude)" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:244 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:361 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:250 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:365 msgid "I’ll configure this under Network → Firewall" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:286 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:292 msgid "Kernel log modules missing" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:289 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:296 msgid "" -"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-" -"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall." +"Kernel netfilter log modules are missing. Install kmod-nf-log and kmod-nf-" +"log6, then reload the firewall." msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:49 msgid "Len" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2234 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2271 msgid "Limit" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:741 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:746 msgid "Live view may be incomplete — a legacy iptables table is registered" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:336 msgid "Logging is off on this router" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1331 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1392 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1369 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1430 msgid "Manual" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2263 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2300 msgid "Manual still uses server protection and poll cadence" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2331 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2369 msgid "" "Manual still uses server protection and poll cadence; it changes the fetch " "budget, not the polling interval." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:380 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:384 msgid "Manual test (System → Terminal):" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2257 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2294 msgid "Maximum raw lines" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:51 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:344 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2486 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2493 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2530 msgid "Message" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2596 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633 msgid "More filters" msgstr "" @@ -417,25 +439,25 @@ msgstr "" msgid "Network → Firewall" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:277 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:283 msgid "" "No WAN firewall zone found in /etc/config/firewall. Configure zones under:" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:272 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:278 msgid "No WAN zone found" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:843 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:874 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:856 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:897 msgid "No WAN zone found; cannot toggle logging without one." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:241 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:247 msgid "Not now" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:355 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359 msgid "Nothing changes until you click Enable." msgstr "" @@ -443,15 +465,15 @@ msgstr "" msgid "OUT" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2516 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2553 msgid "One line" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332 msgid "Open firewall zone settings" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:335 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:340 msgid "" "OpenWrt does not write firewall events to the log until you turn logging on. " "Live View only shows what the firewall already logs — it does not add allow/" @@ -462,16 +484,16 @@ msgstr "" msgid "Outbound" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2284 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2321 msgid "Palette" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1624 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2440 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1661 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2477 msgid "Pause" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1623 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1660 msgid "Paused" msgstr "" @@ -480,11 +502,11 @@ msgstr "" msgid "Proto" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2577 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2614 msgid "Protocol — common values" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2556 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2593 msgid "Quick search" msgstr "" @@ -492,22 +514,22 @@ msgstr "" msgid "Remove filter" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1624 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1661 msgid "Resume" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2274 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2311 msgid "Row tint" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2367 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2405 msgid "" "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/" "red, default) or Accessible (teal/orange). Action text stays colored either " "way." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2423 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2461 msgid "" "Row tint used a local color fallback because the active LuCI theme did not " "apply pass/deny backgrounds." @@ -517,15 +539,15 @@ msgstr "" msgid "Rule" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:733 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:737 msgid "Rule labels incomplete — map truncated" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:736 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:740 msgid "Rule labels unavailable" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:735 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:739 msgid "Rule labels unavailable — temp file failed" msgstr "" @@ -537,40 +559,40 @@ msgstr "" msgid "Search" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1360 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1398 msgid "Server adaptive protection is disabled." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1358 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1396 msgid "Server protection state is unknown." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1365 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1403 msgid "Server shedding — at most %d log lines per poll." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1367 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1405 msgid "Server truncated this poll (adaptive cap)." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2301 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2338 msgid "Show hostnames" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2272 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2309 msgid "Show pass/deny row background colors" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1469 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2539 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2576 msgid "Show rows" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1426 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1464 msgid "Showing a compact summary of the top of the shown sample." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2463 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2500 msgid "Simple" msgstr "" @@ -579,7 +601,7 @@ msgstr "" msgid "Source" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2606 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 msgid "Source IP contains (! to exclude)" msgstr "" @@ -587,61 +609,61 @@ msgstr "" msgid "Source port" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2611 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 msgid "Source port (! to exclude)" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1428 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1466 msgid "Summary data is unavailable; show rows to inspect the sample." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2530 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2567 msgid "Summary mode" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1430 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1468 msgid "Summary was bounded before all categories could be included." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2341 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2379 msgid "" "Switching tabs pauses polling; returning performs one catch-up poll. " "Hostnames are off by default because lookups add work." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:849 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:878 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:862 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:901 msgid "The firewall did not reload; saved and live logging may differ." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2351 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2389 msgid "" "The rate shown for WAN logging is the firewall zone log_limit. OpenWrt " "defaults to 10/minute when no explicit limit is configured; fwlive does not " "impose this cap." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2346 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2384 msgid "" "The router log ring may evict older events before fwlive reads them. fwlive " "cannot recover evicted entries or change forwarding behavior." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2313 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2351 msgid "" "The table updates automatically when your firewall logs traffic. Use Pause " "if it moves too fast." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2427 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2464 msgid "Theme tint fallback" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:388 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:392 msgid "Then ping the router." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:306 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:310 msgid "" "This router reported a logging blocker that Live View does not recognize yet." msgstr "" @@ -650,114 +672,114 @@ msgstr "" msgid "Time" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1435 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 msgid "Top drops" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1438 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1476 msgid "Top rules" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1432 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1470 msgid "Top talkers" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:213 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219 msgid "Turn it back off with the WAN logging control on the watch strip." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:349 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:354 msgid "" "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → " "Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal " "LAN browsing is not logged." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:209 msgid "Turns on logging for the WAN firewall zone and reloads the firewall." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:217 msgid "Undo:" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2371 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2408 msgid "Use Detail for all columns (flags, length, raw message)." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2445 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2451 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2482 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2488 msgid "View" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:881 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:914 msgid "WAN drop/reject logging is off." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:316 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321 msgid "" "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "here. Normal LAN browsing will not." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:854 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:878 msgid "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:857 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:880 msgid "WAN logging is already enabled." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:133 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:139 msgid "WAN logging on" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:142 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:148 msgid "WAN logging on (%s). Click to disable." msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:307 msgid "WAN logging unavailable" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:112 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118 msgid "WAN logging unavailable: missing kernel log modules" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:102 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:108 msgid "WAN logging unavailable: no WAN zone" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:317 msgid "Waiting for firewall events" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1623 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2415 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1660 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2452 msgid "Watching" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2505 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2542 msgid "Wrap" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2561 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598 msgid "block" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1180 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1218 msgid "buffer filling" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1184 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1222 msgid "buffer full" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:127 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:133 msgid "default 10/minute" msgstr "" @@ -766,7 +788,7 @@ msgid "does not contain" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:72 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2562 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599 msgid "drop" msgstr "" @@ -774,16 +796,16 @@ msgstr "" msgid "is" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1179 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1217 msgid "loading buffer" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1555 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1592 msgid "loading…" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1408 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1417 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1446 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1455 msgid "none" msgstr "" @@ -791,101 +813,101 @@ msgstr "" msgid "not" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1766 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803 msgid "not AH" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1765 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802 msgid "not ESP" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1764 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801 msgid "not GRE" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1761 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1798 msgid "not ICMP" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1762 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1799 msgid "not ICMPv6" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1763 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1800 msgid "not IGMP" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1767 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804 msgid "not SCTP" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1759 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1796 msgid "not TCP" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1760 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1797 msgid "not UDP" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2567 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604 msgid "not block" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2566 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2603 msgid "not drop" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2565 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2602 msgid "not pass" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2568 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2605 msgid "not reject" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2569 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2606 msgid "not unknown" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2584 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2621 msgid "or type…" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:70 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2560 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2597 msgid "pass" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2600 msgid "reject" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1186 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1224 msgid "render paused (high rate)" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1195 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1233 msgid "resolve paused (load)" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1197 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1235 msgid "scroll frozen — scroll to top to follow live" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1194 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1232 msgid "truncated" msgstr "" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2564 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2601 msgid "unknown" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:717 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:721 msgid "using fw4" msgstr "" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:134 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:140 msgid "· %s" msgstr "" diff --git a/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po b/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po index 012092946..f9970446c 100644 --- a/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po +++ b/luci-app-fwlive/po/zh_Hans/luci-app-fwlive.po @@ -12,15 +12,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1548 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1594 msgid "%d matching · %d/%d stored" msgstr "%d 条匹配 · 已存 %d/%d" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1341 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1388 msgid "%s · requested up to %d raw lines · %d firewall messages returned" msgstr "%s · 请求最多 %d 行原始日志 · 返回 %d 条防火墙消息" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1331 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1378 msgid "" "%s · requested up to %d raw lines · server limited fetch to %d · %d firewall " "messages returned" @@ -31,70 +31,77 @@ msgstr "" msgid "%s: %s" msgstr "%s: %s" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1550 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596 msgid "0 matching · %d/%d stored" msgstr "0 条匹配 · 已存 %d/%d" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:440 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:447 msgid "Accessible (teal/orange)" msgstr "无障碍(青绿/橙色)" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:353 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:35 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37 msgid "Action" msgstr "动作" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:874 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:915 msgid "Administrator access is required to disable logging." msgstr "需要管理员权限才能禁用日志记录。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:851 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:882 msgid "Administrator access is required to enable logging." msgstr "需要管理员权限才能启用日志记录。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:208 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:214 msgid "Allow/deny rules, LAN logging, or anything else." msgstr "允许/拒绝规则、LAN 日志记录或任何其他设置。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1742 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1788 msgid "Also seen" msgstr "其他常见" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:863 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:853 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:894 msgid "Another change is staged for the firewall; apply or revert it first." msgstr "防火墙另有未提交的更改;请先应用或还原。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2547 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:910 +msgid "" +"Another change overwrote WAN logging after it was saved; check the current " +"state." +msgstr "保存后另有更改覆盖了 WAN 日志设置;请检查当前状态。" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2596 msgid "Any action" msgstr "所有动作" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1735 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1781 msgid "Any protocol" msgstr "任意协议" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1382 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1369 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1429 msgid "Auto" msgstr "自动" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:198 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:204 msgid "Before you enable logging" msgstr "启用日志记录之前" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:829 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:850 msgid "Cannot enable logging until kernel log modules are installed." msgstr "无法启用日志记录,直到内核日志模块已安装。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:201 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:207 msgid "Changes:" msgstr "变更:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:439 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:446 msgid "Classic (green/red)" msgstr "经典(绿/红)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2278 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2327 msgid "Classic uses green/red; Accessible uses teal/orange" msgstr "经典使用绿/红;无障碍使用青绿/橙色" @@ -102,7 +109,7 @@ msgstr "经典使用绿/红;无障碍使用青绿/橙色" msgid "Clear all" msgstr "全部清除" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2619 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2668 msgid "" "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for " "firewall settings · in Simple view, click a row for the full message" @@ -110,78 +117,93 @@ msgstr "" "点击单元格筛选 · 芯片上的 ≠ 排除 · Ctrl+点击规则打开防火墙设置 · 在简单视图中" "点击行查看完整消息" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2346 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2395 msgid "" "Click a row (Time or other non-link cells) to see the full log line (Simple " "view)." msgstr "点击一行(时间或其他非链接单元格)可查看完整日志行(简单视图)。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:156 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:158 msgid "Click a row for the full message" msgstr "点击行可查看完整消息" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2351 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2400 msgid "" "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "chip) to exclude." msgstr "" "点击 IP、动作或协议进行筛选;使用协议菜单(或筛选标签上的 ≠)进行排除。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1736 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1782 msgid "Common" msgstr "常用" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1567 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1613 msgid "Connection lost — retrying…" msgstr "连接已断开 — 正在重试…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:837 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:868 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:858 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:899 msgid "Could not acquire the logging lock." msgstr "无法获取日志记录锁。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:871 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:866 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:905 +msgid "Could not clear the WAN zone log option." +msgstr "无法清除 WAN 区域日志选项。" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:912 msgid "Could not disable logging." msgstr "无法禁用日志记录。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:842 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:873 msgid "Could not enable logging." msgstr "无法启用日志记录。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:769 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:784 msgid "Could not load logging status." msgstr "无法加载日志状态。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:768 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:783 msgid "Could not refresh logging status; showing the last known state." msgstr "无法刷新日志状态;显示上次已知状态。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:839 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:868 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907 +msgid "Could not save the firewall configuration." +msgstr "无法保存防火墙配置。" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:860 msgid "Could not snapshot the current logging state." msgstr "无法快照当前日志记录状态。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2574 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:864 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:903 +msgid "Could not write the WAN zone log option." +msgstr "无法写入 WAN 区域日志选项。" + +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623 msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set." msgstr "自定义协议(! 排除)。有内容时覆盖菜单。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:47 msgid "DPort" msgstr "目的端口" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1184 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1231 msgid "Degraded — sampling" msgstr "降级 — 抽样" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1353 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1400 msgid "Degraded — sampling (slow poll RTT; cadence reduced)." msgstr "降级 — 抽样(轮询 RTT 较慢;已降低频率)。" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:357 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45 msgid "Destination" msgstr "目的地址" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2653 msgid "Destination IP contains (! to exclude)" msgstr "目的 IP 包含(! 表示排除)" @@ -189,39 +211,39 @@ msgstr "目的 IP 包含(! 表示排除)" msgid "Destination port" msgstr "目标端口" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2609 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658 msgid "Destination port (! to exclude)" msgstr "目的端口(! 表示排除)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2462 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2511 msgid "Detail" msgstr "详细" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42 msgid "Dir" msgstr "方向" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:130 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:136 msgid "Disabling…" msgstr "正在禁用…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1180 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1227 msgid "Display limited to %d rows on this device" msgstr "此设备上的显示限制为 %d 行" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2219 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2268 msgid "Display options" msgstr "显示选项" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2311 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2360 msgid "Display options on the bar set Limit, row tint, palette, and hostnames." msgstr "栏上的显示选项可设置行数限制、行着色、调色板和主机名。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:206 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:212 msgid "Does not change:" msgstr "不更改:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:223 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:229 msgid "Don’t show this again" msgstr "不再显示" @@ -229,15 +251,15 @@ msgstr "不再显示" msgid "Enable WAN drop/reject logging" msgstr "启用 WAN 丢弃/拒绝日志" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:183 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:189 msgid "Enable WAN zone drop/reject logging (same as Network → Firewall)." msgstr "启用 WAN 区域的丢弃/拒绝日志记录(同 网络 → 防火墙)。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:189 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 msgid "Enable logging" msgstr "启用日志" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2307 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2356 msgid "" "Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Firewall). It does not add rules or log normal LAN browsing." @@ -246,11 +268,11 @@ msgstr "" "则,也不会记录正常的 LAN 浏览。" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:60 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:189 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:195 msgid "Enabling…" msgstr "正在启用…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1749 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1795 msgid "Exclude" msgstr "排除" @@ -258,11 +280,11 @@ msgstr "排除" msgid "Exclude instead" msgstr "改为排除" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2233 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2282 msgid "Fetch budget" msgstr "抓取配额" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2315 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2364 msgid "" "Fetch budget controls raw log lines per poll. Auto derives from Limit; " "Manual selects a bounded maximum." @@ -271,29 +293,29 @@ msgstr "" "个有上限的最大值。" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:63 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:91 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:156 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:96 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:161 msgid "Filter by %s" msgstr "按 %s 筛选" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:129 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:134 msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings." msgstr "按规则筛选日志(提示:%s)。Ctrl+点击可打开防火墙设置。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2389 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2438 #: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 msgid "Firewall Live View" msgstr "防火墙实时视图" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:48 msgid "Flags" msgstr "标志" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:48 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:50 msgid "Flow" msgstr "流向" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2325 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2374 msgid "" "For a responsive table on a weak device, keep Limit at 250 rows or below. " "The weak-device cap affects rendered rows; the buffer can still retain more." @@ -301,7 +323,7 @@ msgstr "" "要让弱设备上的表格保持响应,请将 行数限制 保持在 250 行或更少。弱设备限制影响" "渲染的行数;缓冲区仍可保留更多行。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:59 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:61 msgid "Forwarded" msgstr "已转发" @@ -309,34 +331,34 @@ msgstr "已转发" msgid "Grant access to firewall live log view" msgstr "授予防火墙实时日志视图的访问权限" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2298 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2347 msgid "Help" msgstr "帮助" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1460 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507 msgid "Hide rows" msgstr "隐藏行" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1516 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1562 msgid "" "High event rate — table refresh is throttled to protect the browser. The " "buffer still updates; refresh will resume automatically." msgstr "" "事件速率过高 — 已限制表格刷新以保护浏览器。缓冲区仍在更新;刷新将自动恢复。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1360 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1407 msgid "Hostname resolve paused while the router is under load." msgstr "路由器负载较高,已暂停主机名解析。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1740 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1786 msgid "ICMPv6" msgstr "ICMPv6" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40 msgid "IN" msgstr "IN" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2362 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2411 msgid "" "If Row tint looks missing, the active LuCI theme may omit success/error or " "info/warn CSS variables; fwlive falls back to local colors (air-gapped, no " @@ -345,7 +367,7 @@ msgstr "" "如果行着色看起来缺失,当前 LuCI 主题可能未提供成功/错误或信息/警告 CSS 变量;" "fwlive 会回退到本地颜色(隔离运行,数据不会离开本设备)。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:324 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328 msgid "" "If the WAN is quiet, wait for probes or use the optional ping check in " "Help / the enabling-logs guide." @@ -353,7 +375,7 @@ msgstr "" "如果 WAN 侧较安静,请等待探测报文,或使用「帮助」/启用日志指南中的可选 ping " "检查。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:57 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:59 msgid "Inbound" msgstr "入站" @@ -361,84 +383,84 @@ msgstr "入站" msgid "Include instead" msgstr "改为包含" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1566 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1612 msgid "Installation is incomplete. Reinstall luci-app-fwlive." msgstr "安装不完整。请重新安装 luci-app-fwlive。" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:354 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:37 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39 msgid "Interface" msgstr "接口" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2589 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 msgid "Interface (prefix ! to exclude)" msgstr "接口(前缀 ! 表示排除)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:244 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:361 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:250 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:365 msgid "I’ll configure this under Network → Firewall" msgstr "稍后我会在 网络 → 防火墙 中配置" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:286 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:292 msgid "Kernel log modules missing" msgstr "缺少内核日志模块" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:290 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:296 msgid "" -"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-" -"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall." +"Kernel netfilter log modules are missing. Install kmod-nf-log and kmod-nf-" +"log6, then reload the firewall." msgstr "" -"缺少内核 netfilter 日志模块。请安装 kmod-nf-log-ipv4 和 kmod-nf-log-ipv6(或 " -"kmod-nf-log / kmod-nf-log6),然后重新加载防火墙。" +"缺少内核 netfilter 日志模块。请安装 kmod-nf-log 和 kmod-nf-log6,然后重新加载" +"防火墙。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:47 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:49 msgid "Len" msgstr "长度" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2222 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2271 msgid "Limit" msgstr "行数限制" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:734 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:746 msgid "Live view may be incomplete — a legacy iptables table is registered" msgstr "实时视图可能不完整 — 已注册旧版 iptables 表" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:336 msgid "Logging is off on this router" msgstr "此路由器的日志记录已关闭" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1383 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1369 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1430 msgid "Manual" msgstr "手动" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2251 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2300 msgid "Manual still uses server protection and poll cadence" msgstr "手动模式仍受服务器保护和轮询节奏约束" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2320 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2369 msgid "" "Manual still uses server protection and poll cadence; it changes the fetch " "budget, not the polling interval." msgstr "" "手动模式仍受服务器保护和轮询节奏约束;它改变的是抓取配额,而不是轮询间隔。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:380 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:384 msgid "Manual test (System → Terminal):" msgstr "手动测试(系统 → 终端):" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2245 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2294 msgid "Maximum raw lines" msgstr "最大原始行数" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:49 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:330 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2474 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2481 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:51 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:344 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2530 msgid "Message" msgstr "消息" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2584 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633 msgid "More filters" msgstr "更多筛选" @@ -446,42 +468,42 @@ msgstr "更多筛选" msgid "Network → Firewall" msgstr "网络 → 防火墙" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:277 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:283 msgid "" "No WAN firewall zone found in /etc/config/firewall. Configure zones under:" msgstr "" "在 /etc/config/firewall 中未找到 WAN 防火墙区域。请在以下位置配置区域:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:272 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:278 msgid "No WAN zone found" msgstr "未找到 WAN 区域" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:835 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:866 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:856 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:897 msgid "No WAN zone found; cannot toggle logging without one." msgstr "未找到 WAN 区域;没有它无法切换日志记录。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:241 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:247 msgid "Not now" msgstr "暂不" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:355 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359 msgid "Nothing changes until you click Enable." msgstr "点击「启用」之前不会有任何更改。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:39 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 msgid "OUT" msgstr "出接口" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2504 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2553 msgid "One line" msgstr "单行" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332 msgid "Open firewall zone settings" msgstr "打开防火墙区域设置" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:336 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:340 msgid "" "OpenWrt does not write firewall events to the log until you turn logging on. " "Live View only shows what the firewall already logs — it does not add allow/" @@ -490,33 +512,33 @@ msgstr "" "开启日志记录之前,OpenWrt 不会将防火墙事件写入日志。实时视图只显示防火墙已记" "录的内容 — 它不会添加允许/拒绝规则。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:58 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:60 msgid "Outbound" msgstr "出站" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2272 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2321 msgid "Palette" msgstr "配色" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1615 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2428 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1661 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2477 msgid "Pause" msgstr "暂停" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1660 msgid "Paused" msgstr "已暂停" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:355 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:41 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:43 msgid "Proto" msgstr "协议" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2565 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2614 msgid "Protocol — common values" msgstr "协议 — 常用值" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2544 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2593 msgid "Quick search" msgstr "快速搜索" @@ -524,15 +546,15 @@ msgstr "快速搜索" msgid "Remove filter" msgstr "移除筛选" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1615 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1661 msgid "Resume" msgstr "继续" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2262 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2311 msgid "Row tint" msgstr "行着色" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2356 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2405 msgid "" "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/" "red, default) or Accessible (teal/orange). Action text stays colored either " @@ -541,29 +563,29 @@ msgstr "" "勾选后行着色显示放行/拒绝行背景。可选经典(绿/红,默认)或无障碍(青绿/橙)。" "操作文本在两种模式下均保持着色。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2412 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2461 msgid "" "Row tint used a local color fallback because the active LuCI theme did not " "apply pass/deny backgrounds." msgstr "由于当前 LuCI 主题未应用放行/拒绝背景色,行着色使用了本地颜色回退。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38 msgid "Rule" msgstr "规则" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:725 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:737 msgid "Rule labels incomplete — map truncated" msgstr "规则标签不完整 — 映射被截断" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:728 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:740 msgid "Rule labels unavailable" msgstr "规则标签不可用" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:727 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:739 msgid "Rule labels unavailable — temp file failed" msgstr "规则标签不可用 — 临时文件创建失败" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46 msgid "SPort" msgstr "源端口" @@ -571,49 +593,49 @@ msgstr "源端口" msgid "Search" msgstr "搜索" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1351 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1398 msgid "Server adaptive protection is disabled." msgstr "服务器自适应保护已禁用。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1349 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1396 msgid "Server protection state is unknown." msgstr "服务器保护状态未知。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1356 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1403 msgid "Server shedding — at most %d log lines per poll." msgstr "服务器正在削减 — 每次轮询最多 %d 行日志。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1358 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1405 msgid "Server truncated this poll (adaptive cap)." msgstr "服务器已截断本次轮询(自适应上限)。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2289 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2338 msgid "Show hostnames" msgstr "显示主机名" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2260 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2309 msgid "Show pass/deny row background colors" msgstr "显示放行/拒绝行背景色" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1460 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2527 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2576 msgid "Show rows" msgstr "显示行" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1417 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1464 msgid "Showing a compact summary of the top of the shown sample." msgstr "显示当前样本顶部的简要摘要。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2451 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2500 msgid "Simple" msgstr "简洁" #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:356 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:42 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44 msgid "Source" msgstr "源地址" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2594 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 msgid "Source IP contains (! to exclude)" msgstr "源 IP 包含(! 表示排除)" @@ -621,23 +643,23 @@ msgstr "源 IP 包含(! 表示排除)" msgid "Source port" msgstr "源端口" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 msgid "Source port (! to exclude)" msgstr "源端口(! 表示排除)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1419 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1466 msgid "Summary data is unavailable; show rows to inspect the sample." msgstr "摘要数据不可用;显示行以检查样本。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2518 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2567 msgid "Summary mode" msgstr "摘要模式" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1421 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1468 msgid "Summary was bounded before all categories could be included." msgstr "摘要已受限,无法包含所有类别。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2330 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2379 msgid "" "Switching tabs pauses polling; returning performs one catch-up poll. " "Hostnames are off by default because lookups add work." @@ -645,12 +667,12 @@ msgstr "" "切换到其他标签页会暂停轮询;返回时执行一次追赶轮询。主机名默认关闭,因为解析" "会增加工作量。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:841 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:870 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:862 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:901 msgid "The firewall did not reload; saved and live logging may differ." msgstr "防火墙未能重新加载;已保存的日志设置与当前生效状态可能不一致。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2340 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2389 msgid "" "The rate shown for WAN logging is the firewall zone log_limit. OpenWrt " "defaults to 10/minute when no explicit limit is configured; fwlive does not " @@ -659,7 +681,7 @@ msgstr "" "WAN 日志显示的速率是防火墙区域的 log_limit。未显式配置时 OpenWrt 默认为 10 " "条/分钟;此上限并非 fwlive 施加。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2335 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2384 msgid "" "The router log ring may evict older events before fwlive reads them. fwlive " "cannot recover evicted entries or change forwarding behavior." @@ -667,46 +689,46 @@ msgstr "" "路由器日志环形缓冲区可能在 fwlive 读取前淘汰较旧事件。fwlive 无法恢复已淘汰的" "条目,也不会改变转发行为。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2302 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2351 msgid "" "The table updates automatically when your firewall logs traffic. Use Pause " "if it moves too fast." msgstr "防火墙记录流量时表格会自动更新。如果滚动太快,请使用「暂停」。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2415 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2464 msgid "Theme tint fallback" msgstr "主题着色回退" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:388 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:392 msgid "Then ping the router." msgstr "然后 ping 路由器。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:306 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:310 msgid "" "This router reported a logging blocker that Live View does not recognize yet." msgstr "此路由器报告了 Live View 尚无法识别的日志阻止项。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:34 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:36 msgid "Time" msgstr "时间" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1426 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 msgid "Top drops" msgstr "主要丢弃" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1429 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1476 msgid "Top rules" msgstr "主要规则" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1423 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1470 msgid "Top talkers" msgstr "主要通信方" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:213 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:219 msgid "Turn it back off with the WAN logging control on the watch strip." msgstr "通过监视栏上的 WAN 日志控件将其重新关闭。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:350 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:354 msgid "" "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → " "Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal " @@ -715,28 +737,28 @@ msgstr "" "开启 WAN 区域的丢弃/拒绝日志记录(同 网络 → 防火墙 → wan → 日志)。受区域 " "log_limit 限速(OpenWrt 默认 10 条/分钟)。正常的 LAN 浏览不会被记录。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:203 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:209 msgid "Turns on logging for the WAN firewall zone and reloads the firewall." msgstr "开启 WAN 防火墙区域的日志记录并重新加载防火墙。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:211 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:217 msgid "Undo:" msgstr "撤销:" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2359 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2408 msgid "Use Detail for all columns (flags, length, raw message)." msgstr "使用「详细」可查看全部列(标志、长度、原始消息)。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2433 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2439 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2482 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2488 msgid "View" msgstr "视图" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:873 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:914 msgid "WAN drop/reject logging is off." msgstr "WAN 丢弃/拒绝日志记录已关闭。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:317 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321 msgid "" "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "here. Normal LAN browsing will not." @@ -744,7 +766,7 @@ msgstr "" "WAN 丢弃/拒绝日志记录已开启。被阻止的 WAN 入站流量会显示在这里;正常的 LAN 浏" "览不会。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:847 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:878 msgid "" "WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "it happens — not normal LAN browsing." @@ -752,58 +774,58 @@ msgstr "" "WAN 丢弃/拒绝日志记录已开启。被阻止的入站流量会实时显示在这里 — 正常的 LAN 浏" "览不会。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:849 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:880 msgid "WAN logging is already enabled." msgstr "WAN 日志记录已处于启用状态。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:133 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:139 msgid "WAN logging on" msgstr "WAN 日志:开" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:142 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:148 msgid "WAN logging on (%s). Click to disable." msgstr "WAN 日志:开(%s)。点击可禁用。" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:307 msgid "WAN logging unavailable" msgstr "WAN 日志不可用" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:112 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:118 msgid "WAN logging unavailable: missing kernel log modules" msgstr "WAN 日志不可用:缺少内核日志模块" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:102 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:108 msgid "WAN logging unavailable: no WAN zone" msgstr "WAN 日志不可用:无 WAN 区域" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:317 msgid "Waiting for firewall events" msgstr "正在等待防火墙事件" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2403 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1660 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2452 msgid "Watching" msgstr "监视中" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2493 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2542 msgid "Wrap" msgstr "自动换行" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:69 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2549 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598 msgid "block" msgstr "拦截" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1171 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1218 msgid "buffer filling" msgstr "缓冲区填充中" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1175 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1222 msgid "buffer full" msgstr "缓冲区已满" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:127 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:133 msgid "default 10/minute" msgstr "默认 10 条/分钟" @@ -811,8 +833,8 @@ msgstr "默认 10 条/分钟" msgid "does not contain" msgstr "不包含" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:70 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2550 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:72 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599 msgid "drop" msgstr "丢弃" @@ -820,16 +842,16 @@ msgstr "丢弃" msgid "is" msgstr "是" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1170 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1217 msgid "loading buffer" msgstr "正在加载缓冲区" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1546 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1592 msgid "loading…" msgstr "加载中…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1399 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1408 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1446 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1455 msgid "none" msgstr "无" @@ -837,101 +859,101 @@ msgstr "无" msgid "not" msgstr "非" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1757 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803 msgid "not AH" msgstr "非 AH" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1756 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802 msgid "not ESP" msgstr "非 ESP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1755 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801 msgid "not GRE" msgstr "非 GRE" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1752 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1798 msgid "not ICMP" msgstr "非 ICMP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1753 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1799 msgid "not ICMPv6" msgstr "非 ICMPv6" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1754 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1800 msgid "not IGMP" msgstr "非 IGMP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1758 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804 msgid "not SCTP" msgstr "非 SCTP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1750 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1796 msgid "not TCP" msgstr "非 TCP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1751 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1797 msgid "not UDP" msgstr "非 UDP" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2555 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604 msgid "not block" msgstr "非阻止" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2554 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2603 msgid "not drop" msgstr "非丢弃" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2553 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2602 msgid "not pass" msgstr "非放行" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2556 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2605 msgid "not reject" msgstr "非拒绝" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2557 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2606 msgid "not unknown" msgstr "非未知" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2572 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2621 msgid "or type…" msgstr "或输入…" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:68 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:70 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2597 msgid "pass" msgstr "通过" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2551 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2600 msgid "reject" msgstr "拒绝" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1177 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1224 msgid "render paused (high rate)" msgstr "渲染已暂停(速率过高)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1186 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1233 msgid "resolve paused (load)" msgstr "解析已暂停(负载)" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1188 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1235 msgid "scroll frozen — scroll to top to follow live" msgstr "滚动已冻结 — 滚动到顶部以恢复跟随" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1185 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1232 msgid "truncated" msgstr "已截断" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:60 -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2601 msgid "unknown" msgstr "未知" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:709 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:721 msgid "using fw4" msgstr "使用 fw4" -#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:134 +#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:140 msgid "· %s" msgstr "· %s" diff --git a/luci-app-fwlive/root/lib/upgrade/keep.d/luci-app-fwlive b/luci-app-fwlive/root/lib/upgrade/keep.d/luci-app-fwlive new file mode 100644 index 000000000..5271b0c90 --- /dev/null +++ b/luci-app-fwlive/root/lib/upgrade/keep.d/luci-app-fwlive @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Lucas Albers +/etc/fwlive/ diff --git a/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh b/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh index 133380bf3..401e353f6 100755 --- a/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh +++ b/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh @@ -423,7 +423,11 @@ maybe_snapshot_wan_log_baseline() { if [ -z "${FWLIVE_WAN_LOG_BASELINE_FILE:-}" ] && [ "$path" = /etc/fwlive/wan-log-baseline ]; then wan_log_lock_dir_safe "$_dir" || return 1 fi - current=$(wan_zone_log_value "$zone") + if [ "$#" -ge 2 ]; then + current="$2" + else + current=$(wan_zone_log_value "$zone") + fi printf '%s' "$current" >"$path" 2>/dev/null || return 1 return 0 } @@ -1036,6 +1040,11 @@ enable_wan_logging() { current=$(wan_zone_log_value "$zone") if wan_filter_log_enabled "$current"; then + # Already-on with a lost baseline: reconstruct the usual first-enable + # snapshot (unset), not the current on-value. Recording "1" would make + # uninstall restore logging after disable. Skip-if-exists in + # the helper. Failure must not become baseline_snapshot_failed. + maybe_snapshot_wan_log_baseline "$zone" "" || true release_wan_log_lock printf '{"ok":true,"changed":false,"wan_zone":%s}' "$zone_json" return 0 diff --git a/luci-app-qosify/Makefile b/luci-app-qosify/Makefile new file mode 100644 index 000000000..904735bdc --- /dev/null +++ b/luci-app-qosify/Makefile @@ -0,0 +1,26 @@ +include $(TOPDIR)/rules.mk + +PKG_LICENSE:=MIT +PKG_MAINTAINER:=Ash Clarke + +LUCI_TITLE:=LuCI interface for qosify +LUCI_DESCRIPTION:=Web UI for the qosify CAKE/eBPF traffic shaping daemon +LUCI_DEPENDS:=+luci-base +qosify +PKG_PROVIDES:=luci-app-qos + +# qosify leaves clsact and ifb-dns behind when it stops, and its own prerm may +# run after this one, so a copy of cleanup waits in the background for qosify to +# exit. If qosify stays installed and running it touches nothing. +define Package/luci-app-qosify/prerm +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || [ "$${PKG_UPGRADE}" = 1 ] || { + cp /usr/share/qosify-luci/cleanup /tmp/qosify-luci-cleanup && + ( /tmp/qosify-luci-cleanup wait; rm -f /tmp/qosify-luci-cleanup ) \ + /dev/null 2>&1 & +} +exit 0 +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js b/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js new file mode 100644 index 000000000..fe6b0f98e --- /dev/null +++ b/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js @@ -0,0 +1,2354 @@ +// SPDX-License-Identifier: MIT +'use strict'; +'require view'; +'require fs'; +'require ui'; +'require uci'; +'require poll'; +'require rpc'; +'require dom'; + +var UCI_PATH='/etc/config/qosify'; +var RULES_PATH='/etc/qosify/00-defaults.conf'; +var DSCP=['CS0','CS1','CS2','CS3','CS4','CS5','CS6','CS7','AF11','AF12','AF13','AF21','AF22','AF23','AF31','AF32','AF33','AF41','AF42','AF43','EF','VA','NQB','LE','DF']; +var OVH=['none','manual','conservative','ethernet','docsis','pppoe-ptm','bridged-ptm','pppoe-vcmux','pppoe-llcsnap','pppoa-vcmux','pppoa-llc','bridged-vcmux','bridged-llcsnap','ipoa-vcmux','ipoa-llcsnap']; +var ENCAP=['atm','noatm','ptm']; +var MODES=['diffserv3','diffserv4','diffserv8','besteffort','precedence']; +var MAP_ROWS=200; +// Bar length is (row/largest row)^BAR_EXP: the largest row fills the track and a +// 0.1% row still shows at a tenth of it, so a bulk download does not hide the rest. +var BAR_EXP=1/3; +// codepoints[] in map.c. +var DSCP_VAL={CS0:0,DF:0,LE:1,CS1:8,AF11:10,AF12:12,AF13:14,CS2:16,AF21:18,AF22:20, + AF23:22,CS3:24,AF31:26,AF32:28,AF33:30,CS4:32,AF41:34,AF42:36,AF43:38,CS5:40, + VA:44,NQB:45,EF:46,CS6:48,CS7:56}; +// Counters order: EF first, then codepoint descending. LE (1) and CS1 (8) are +// CAKE's Background tin, so they sort below best effort; -1 is anything +// __qosify_map_dscp_value() would reject and sorts below them. +var DSCP_BULK={1:1,8:1}; +// Within an AF class the lowest drop precedence leads: AF41, AF42, AF43. +function dscpRank(v){return v<0?-1000:DSCP_BULK[v]?v-100:v===46?100:v>=10&&v<=38&&!(v&1)&&(v&7)?(v&56)+8-(v&7):v;} +// Colour by sorted class name, so a class keeps its colour as the bars reorder. +function qc(n){return 'var(--qos-c-'+n+')';} +var CN_COLORS=['blue','green','orange','purple','red','cyan','brown','pink'].map(qc); +// qosify_map_stats() appends these two default slots; they are not config classes. +var CN_SKIP={tcp_default:1,udp_default:1}; +// A class with no codepoint to place in a tin. +var CN_NONE=qc('none'); +// CAKE's DSCP to tin tables in sch_cake.c, already put through tin_order, so each +// digit is the column qosify-status prints that tin in. besteffort is one tin. +var TIN_MAP={besteffort:'0', + precedence:'0000000011111111222222223333333344444444555555556666666677777777', + diffserv8:'2012422212121212524242423232323262323232622262627222222272222222', + diffserv4:'1011211101111111212121212121212131212121311131313111111131111111', + diffserv3:'1011211101111111111111111111111111111111111121212111111121111111'}; +// Colour per tin, same index as TIN_MAP and the qosify-status tin columns, so a +// class bar takes the colour of the tin its codepoint lands in. One colour per kind +// of traffic across modes: red bulk, blue best effort, yellow video, green voice; +// diffserv8 and precedence add their extra tins between them. The names are +// qosify.css tokens, so the theme supplies the colours in light and dark. +var TIN_COLORS={besteffort:['blue'], + precedence:['blue','red','purple','yellow','orange','green','forest','pine'], + diffserv8:['grey','red','blue','yellow','cyan','purple','green','forest'], + diffserv4:['red','blue','yellow','green'], + diffserv3:['red','blue','green']}; +for(var tk in TIN_COLORS)TIN_COLORS[tk]=TIN_COLORS[tk].map(qc); +// qosify.init handles 'alias' with add_class and 'device' with add_interface, +// so those section types share the option set of class / interface. +var OPT_DESC={ + defaults:_('List of files with port/IP/host mappings'), + timeout:_('Default timeout for dynamically added entries'), + dscp_default_tcp:_('Default DSCP value for TCP packets'), + dscp_default_udp:_('Default DSCP value for UDP packets'), + dscp_icmp:_('DSCP value for ICMP packets'), + dscp_prio:_('DSCP value for priority-marked packets'), + dscp_bulk:_('DSCP value for bulk-marked packets'), + prio_max_avg_pkt_len:_('Maximum average packet length for marking a flow as priority'), + bulk_trigger_pps:_('Number of packets per second to trigger bulk flow detection'), + bulk_trigger_timeout:_('Time below bulk_trigger_pps threshold until a bulk flow mark is removed'), + value:_('DSCP value for ingress and egress, where they are not set'), + ingress:_('DSCP value for ingress'), + egress:_('DSCP value for egress'), + name:_('netifd interface (config interface) or netdev (config device) to enable QoS on'), + disabled:_('Skip this section'), + bandwidth_up:_('Uplink bandwidth (same format as tc)'), + bandwidth_down:_('Downlink bandwidth (same format as tc)'), + bandwidth:_('Bandwidth for both directions, where bandwidth_up or bandwidth_down is not set'), + 'if.ingress':_('Enable ingress shaping'), + 'if.egress':_('Enable egress shaping'), + mode:_('CAKE diffserv mode'), + nat:_('Enable CAKE NAT host detection via conntrack'), + host_isolate:_('Enable CAKE host isolation'), + autorate_ingress:_('Enable CAKE automatic rate estimation for ingress'), + overhead_type:_('CAKE overhead keyword added to options; manual uses overhead and overhead_encap'), + overhead:_('Adds overhead when overhead_type is manual'), + overhead_encap:_('Adds atm, noatm or ptm when overhead_type is manual'), + overhead_mpu:_('Adds mpu '), + overhead_vlan:_('Adds ether-vlan once per level (1 or 2)'), + ingress_options:_('CAKE ingress options'), + egress_options:_('CAKE egress options'), + options:_('CAKE options for ingress + egress') +}; +// Quick Add grids hold at most this many options per table. +var QA_COLS=7; + +// luci.setInitAction was dropped from luci-base in 4440b267d; the rc namespace +// (built into the rpcd core binary, so no extra dependency) replaces it. +var callRcInit=rpc.declare({ + object:'rc', + method:'init', + params:['name','action'], + reject:true +}); +// rc.list, service.list and qosify.status raise. Without reject:true a ubus +// status code (6 when the ACL no longer covers the object, 4 when it is gone) +// stays in result[0] and expect{'':{}} rewrites it to the same {} a working +// call with nothing to report returns, so a call that never ran cannot be told +// from one that ran and found nothing. Each site catches the rejection to null. +// skip_running_check keeps rc.list from forking `qosify running`, which waits +// on ubus for up to 10s while rpcd kills it after 3s. +var callRcList=rpc.declare({ + object:'rc', + method:'list', + params:['name','skip_running_check'], + expect:{'':{}}, + reject:true +}); +var callQosifyStatus=rpc.declare({ + object:'qosify', + method:'status', + expect:{'':{}}, + reject:true +}); +// reload re-reads the files in the defaults list (qosify_map_reload()) and +// nothing else; check_devices re-runs qosify_iface_check(). Both return an +// empty reply, so without reject:true a failure would read as success. +var callQosifyReload=rpc.declare({ + object:'qosify', + method:'reload', + reject:true +}); +var callQosifyCheckDevices=rpc.declare({ + object:'qosify', + method:'check_devices', + reject:true +}); +// get_stats and dump are in qosify 1501e09 (24.10, 25.12) and master; the +// get_stats reply shape differs by build and is rendered as found. +var callQosifyStats=rpc.declare({ + object:'qosify', + method:'get_stats', + expect:{'':{}}, + reject:true +}); +var callQosifyDump=rpc.declare({ + object:'qosify', + method:'dump', + expect:{'':{}}, + reject:true +}); +var callServiceList=rpc.declare({ + object:'service', + method:'list', + params:['name'], + expect:{'':{}}, + reject:true +}); +var callUciRevert=rpc.declare({ + object:'uci', + method:'revert', + params:['config'], + reject:true +}); +function isRunning(r){ + try{var i=r.qosify.instances;for(var k in i)if(i[k].running)return true;}catch(e){} + return false; +} +function runPid(r){ + try{var i=r.qosify.instances;for(var k in i)if(i[k].running&&i[k].pid)return i[k].pid;}catch(e){} + return 0; +} + +function clsOpt(c){var d=c.ingress===c.egress?c.egress:(c.ingress||'-')+'/'+(c.egress||'-');return c.name+(d?' ('+d+')':'');} +function trim(s){return (s||'').replace(/^\s+|\s+$/g,'');} +function $(id){return document.getElementById(id);} + +// ingress/egress/nat/host_isolate/autorate_ingress reach the daemon through +// qosify.init's `add_option boolean` -> json_add_boolean -> !!atoi(), so only a +// non-zero number is true: 'true', 'on' and 'yes' all mean off. +function numBool(v,def){ + if(v==null||v==='')return !!def; + var n=parseInt(v,10); + return !isNaN(n)&&n!==0; +} +// `disabled` is read with config_get_bool, which does accept the word forms. +function uciBool(v,def){ + if(v==null||v==='')return !!def; + switch(String(v).toLowerCase()){ + case '1':case 'on':case 'true':case 'yes':case 'enabled':return true; + case '0':case 'off':case 'false':case 'no':case 'disabled':return false; + } + return !!def; +} +function boolNum(v){return /^-?\d+$/.test(String(v==null?'':v));} + +// ubus call qosify status -> { devices:{}, interfaces:{ :{ active,... } } } +function statusActive(st){ + var groups=['interfaces','devices'],i,k,t; + for(i=0;i=0)l=l.slice(0,h); + if(trim(l))n++; + } + return n; +} + +function validateRules(d){ + if(/\x00/.test(d))return _('Binary content rejected'); + var lines=d.split('\n'); + for(var i=0;i1023)return _('Line %d is longer than 1023 characters — the rule loader reads fixed-size lines and would split it, comment included').format(i+1); + } + return null; +} +function fmtSize(n){return n<1024?n+'B':(n/1024).toFixed(1)+'K';} +function fmtMtime(t){if(!t)return '';return new Date(t*1000).toLocaleString();} +function fmtShare(p){ + if(!p)return '0%'; + if(p<0.1)return '<0.1%'; + return '%s%%'.format(p<10?p.toFixed(1):Math.round(p)); +} + +// The shaping section Quick Settings edits, or null. Prefers the first enabled +// section, and accepts `config device` since qosify.init feeds both section +// types through add_interface(). An anonymous section has a synthetic .name +// (cfgXXXXXX / newXXXXXX) that never appears in the file, so name is left empty +// for it and setOpts() locates the block by per-type ordinal instead. +function ifSect(){ + var a=[]; + ['interface','device'].forEach(function(t){ + var i=0; + uci.sections('qosify',t,function(s){ + a.push({type:t,id:s['.name'],name:s['.anonymous']?'':s['.name'],idx:i++,on:!uciBool(s.disabled,false)}); + }); + }); + for(var j=0;j3)bad(o,trim(st));return;} + if(!o||(w[0]!=='option'&&w[0]!=='list'))return; + k=w[1]||''; + if(w.length!==3){if(TCK.test(k))bad(o,k);return;} + if(w[0]==='list')(o[k]=[].concat(o[k]||[])).push(w[2]);else o[k]=w[2]; + }); + }); + return top['.bad']?[top].concat(out):out; +} +// Values are spliced into a single-quoted UCI string: strip quotes and line breaks, +// or a stray newline injects arbitrary option/config lines into the file. +function qv(v){return v==null?'':String(v).replace(/['"\r\n]/g,'');} +// Set/remove options inside one config block, preserving every other byte of the +// file (comments, ordering, lists, unknown options). kv[key]===null deletes. +// idx = ordinal among sections of this type, used when name is empty (anonymous). +function setOpts(txt,type,name,idx,kv){ + txt=(txt||'').replace(/\r\n/g,'\n'); + var lines=(txt||'').split('\n'),secs=cfgSections(txt),s=null,n=0,i,k; + for(i=0;i=0)return true; + if(dscpList().indexOf(v)>=0)return true; + var n=dscpNum(v); + return n!==null&&n<64; +} +// Match keys qosify_map_parse_line() loads: ports 1-65534 (strtoul base 0), +// one address for inet_pton(), or a dns pattern. +function portOk(p){ + var a=p.split('-'),x=dscpNum(a[0]),y=a.length>1?dscpNum(a[1]):x; + return a.length<3&&x!==null&&y!==null&&x>=1&&y>=x&&y<=65534; +} +function ip4Ok(a){return /^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.|$)){4}$/.test(a)&&!/\.$/.test(a);} +function ip6Ok(a){ + if(!/^[0-9a-fA-F:.]+$/.test(a)||a.indexOf(':')<0)return false; + try{new URL('http://['+a+']/');return true;}catch(e){return false;} +} +function matchOk(k){ + if(/^(dns|dns_c|dns_q):./.test(k))return true; + if(/^(tcp|udp):/.test(k))return portOk(k.slice(4)); + if(k.indexOf(':')>=0)return ip6Ok(k); + if(k.indexOf('.')>=0)return ip4Ok(k); + return false; +} +function ruleWarn(txt,names){ + var w=[],bad=[],bare=[],xtra=[],mt=[],lines=(txt||'').split('\n'); + function add(a,v){if(a.length<5&&a.indexOf(v)<0)a.push(v);} + for(var i=0;i=0)l=l.slice(0,h); + l=trim(l);if(!l)continue; + var f=l.split(/\s+/); + if(f.length<2){add(bare,String(i+1));continue;} + if(f.length>2){add(xtra,String(i+1));continue;} + if(!matchOk(f[0]))add(mt,String(i+1)); + if(!dscpOk(f[1],names))add(bad,f[1].replace(/^\+/,'')); + } + if(bare.length)w.push(_('No DSCP target on line %s — qosify skips single-field lines').format(bare.join(', '))); + if(xtra.length)w.push(_('More than two fields on line %s — qosify reads the rest of the line as the DSCP value and skips it').format(xtra.join(', '))); + if(mt.length)w.push(_('Match not loaded by qosify on line %s — ports 1-65534, one IPv4/IPv6 address (no CIDR or %%zone), or dns:, dns_c:').format(mt.join(', '))); + if(bad.length)w.push(_('Unknown class/DSCP target: %s').format(bad.join(', '))+(NQB_OK===false&&bad.indexOf('NQB')>=0?' '+_('(this qosify build has no NQB)'):'')); + return w; +} +// dscp_* and class values in config sections (uci.get objects or cfgOpts()). +// Defaults-level dscp_prio/dscp_bulk/dscp_icmp failing makes qosify_ubus_config() +// return before the interfaces are applied; the rest is dropped quietly. +// cmd_add_qdisc() pastes bandwidth, mode and the options unquoted into a tc +// command run with sh -c as root, so a shell metacharacter there is refused too. +function cfgLint(secs){ + var w=[],cls=[]; + secs.forEach(function(s){if((s['.type']==='class'||s['.type']==='alias')&&s['.name'])cls.push(s['.name']);}); + secs.forEach(function(s){ + var t=s['.type'],n=s['.name']||t; + (s['.bad']||[]).forEach(function(k){w.push({hard:true,t:_('%s: %s cannot be checked — write it as one plain or fully quoted value, with no backslash').format(n,k)});}); + if(t==='interface'||t==='device'){ + ['bandwidth_up','bandwidth_down','bandwidth','mode','ingress_options','egress_options','options'].forEach(function(k){ + if(s[k]&&/['"`$;&|<>(){}\\\n]/.test(String(s[k])))w.push({hard:true,t:_('%s: %s contains shell metacharacters — qosify runs the tc command with sh -c as root, so they would break it or be executed').format(n,k)}); + }); + } + else if(t==='defaults'){ + ['dscp_prio','dscp_bulk','dscp_icmp'].forEach(function(k){ + if(s[k]!=null&&s[k]!==''&&!dscpOk(s[k],cls))w.push({hard:true,t:_('%s: %s "%s" is not a class, codepoint or 0-63 — qosify rejects the whole config and interface changes are not applied').format(n,k,s[k])}); + }); + ['dscp_default_tcp','dscp_default_udp'].forEach(function(k){ + if(s[k]!=null&&s[k]!==''&&!dscpOk(s[k],cls))w.push({t:_('%s: %s "%s" is not a class, codepoint or 0-63 — qosify ignores it').format(n,k,s[k])}); + }); + } + else if(t==='class'||t==='alias'){ + ['ingress','egress'].forEach(function(k){ + var v=s[k]||s.value; + if(v&&!dscpOk(v))w.push({t:_('%s: %s "%s" is not a codepoint or 0-63 — qosify drops this class and the rules using it').format(n,k,v)}); + }); + ['dscp_prio','dscp_bulk'].forEach(function(k){ + if(s[k]!=null&&s[k]!==''&&!dscpOk(s[k],cls))w.push({t:_('%s: %s "%s" is not a class, codepoint or 0-63 — qosify ignores it').format(n,k,s[k])}); + }); + } + }); + return w; +} +// Shell glob from `list defaults` against a path, as qosify.init's unquoted $files. +function globHit(g,p){ + return new RegExp('^'+String(g).replace(/[.+^${}()|\\]/g,'\\$&').replace(/\*/g,'[^/]*').replace(/\?/g,'[^/]')+'$').test(p); +} +function rulesLoaded(secs){ + return secs.some(function(s){ + if(s['.type']!=='defaults')return false; + return [].concat(s.defaults||[]).some(function(g){return String(g).split(/\s+/).some(function(x){return globHit(x,RULES_PATH);});}); + }); +} + +var noteSeen={}; +function notify(msg,kind){ + var key=String(msg); + if(noteSeen[key])return null; + var n=ui.addNotification(null,E('p',{},msg),kind||'info'); + if(!n)return null; + noteSeen[key]=1; + var ms=(kind==='danger')?10000:(kind==='warning')?8000:5000; + setTimeout(function(){ + delete noteSeen[key]; + if(n&&n.parentNode)n.parentNode.removeChild(n); + },ms); + return n; +} + +// Stock LuCI markup: themes style .cbi-section, .table, .label and +// .cbi-value already, so qosify.css only draws the section boxes. +function badge(kind,t){return E('span',{'class':kind?'label '+kind:'label'},t);} +function desc(t){return E('div',{'class':'cbi-value-description'},t);} +function sdesc(t){return E('div',{'class':'cbi-section-descr'},t);} +function kvRow(k,v,id){return E('tr',{'class':'tr'},[E('td',{'class':'td left','width':'33%'},k),E('td',{'class':'td left','id':id||null},v)]);} +function kvTable(rows,id){return E('table',{'class':'table','id':id||null},rows);} +function emRow(t){return E('tr',{'class':'tr placeholder'},E('td',{'class':'td'},E('em',{},t)));} +function emP(t){return E('p',{},E('em',{},t));} +function gridTable(head,rows,empty){ + return E('table',{'class':'table cbi-section-table'},[E('tr',{'class':'tr cbi-section-table-titles'},head.map(function(h){return E('th',{'class':'th'},h);}))] + .concat(rows.length?rows.map(function(r){return E('tr',{'class':'tr cbi-section-table-row'},r.map(function(c,i){return E('td',{'class':'td','data-title':head[i]},c);}));}):[emRow(empty)])); +} +// A section that folds, its open state kept for the browser session. +function fold(id,title,kids,open){ + var k='qosify.fold.'+id,st=null,d; + try{st=sessionStorage.getItem(k);}catch(e){} + d=E('details',{'class':'cbi-section','id':id,'open':(st==null?open:st==='1')?'':null},[E('summary',{},E('h3',{},title))].concat(kids)); + d.addEventListener('toggle',function(){try{sessionStorage.setItem(k,d.open?'1':'0');}catch(e){}}); + return d; +} +function refBox(title,note,rows){ + return E('details',{},[E('summary',{},title),note?E('p',{},note):'', + rows.length?E('table',{'class':'table'},rows.map(function(r){return kvRow(E('code',{},[document.createTextNode(r[0])]),[document.createTextNode(r[1])]);})):'']); +} +function sect(title,kids,attrs){ + var a=attrs||{}; + a['class']='cbi-section'; + return E('div',a,[E('h3',{'id':a.id?a.id+'-title':null},title)].concat(kids||[])); +} +function colTable(cols,kids){ + var sum=cols.reduce(function(t,c){return t+c[1];},0); + return E('table',{'class':'table','style':'table-layout:fixed'},[E('colgroup',{},cols.map(function(c){ + return E('col',{'style':'width:'+(c[1]*100/sum).toFixed(2)+'%'});}))].concat(kids)); +} +function colHead(cols,id){ + return E('div',{'class':'qhead','id':id||null},colTable(cols,E('tr',{'class':'tr table-titles'}, + cols.map(function(c){return E('th',{'class':c[2]?'th qn':'th left'},c[0]);})))); +} +function valRow(lbl,el){ + var n=Array.isArray(el)?el[0]:el; + if(n&&!n.id&&n.querySelector)n=n.querySelector('[id]'); + return E('div',{'class':'cbi-value'},[E('label',{'class':'cbi-value-title','for':(n&&n.id)||null},lbl),E('div',{'class':'cbi-value-field'},el)]); +} + +// Remember the size/mtime an editor was loaded from, so a save can tell the +// difference between "the user changed this" and "something else changed the +// file underneath us". +function stampFile(el,st){ + el.dataset.mtime=st?String(st.mtime):''; + el.dataset.size=st?String(st.size):''; +} +function fileMoved(el,st){ + if(!el||el.dataset.mtime==null)return false; + var m=st?String(st.mtime):'',z=st?String(st.size):''; + return el.dataset.mtime!==m||el.dataset.size!==z; +} + +function confirmDialog(title,text,label,negative){ + return new Promise(function(resolve){ + var done=function(v){ui.hideModal();resolve(v);}; + ui.showModal(title,[ + E('p',{},text), + E('div',{'class':'right'},[ + E('button',{'class':'cbi-button','click':function(){done(false);}},_('Cancel')), + ' ', + E('button',{'class':'cbi-button '+(negative?'cbi-button-negative':'cbi-button-action'),'click':function(){done(true);}},label||_('Continue')) + ]) + ]); + }); +} + +return view.extend({ + handleSaveApply:null,handleSave:null,handleReset:null, + currentTab:'ov', + readonly:false, + + load:function(){ + return Promise.all([ + uci.load('qosify').catch(function(){return null;}), + this.gatherCtx(true), + callQosifyStats().then(nqbSeen,function(){}) + ]); + }, + + render:function(d){ + var self=this,ctx=d[1]; + + this.readonly=!L.hasViewPermission(); + + if(d[0]===null)notify(_('The qosify UCI configuration could not be loaded — class and interface lists may be incomplete.'),'warning'); + else this.lintAll(true).forEach(function(t){notify(t,'warning');}); + + var root=E('div',{'class':'cbi-map','id':'qos-app'}); + root.appendChild(E('link',{'rel':'stylesheet','href':L.resource('view/qosify/qosify.css')})); + root.appendChild(E('h2',{},_('qosify'))); + root.appendChild(E('div',{'class':'cbi-map-descr'},_('Traffic shaping and DSCP classification via qosify'))); + + var names={ov:'overview',cf:'config',ru:'rules',ad:'advanced',st:'status',cn:'counters'}; + var hash=(location.hash||'').slice(1),want='ov',k; + for(k in names)if(names[k]===hash)want=k; + + var group=E('div',{}); + [['ov',_('Overview'),this.tabOverview(ctx)], + ['cf',_('Config'),this.tabConfig(ctx)], + ['ru',_('Rules'),this.tabRules(ctx)], + ['st',_('Status'),this.tabStatus(ctx)], + ['cn',_('Counters'),this.tabCounters(ctx)], + ['ad',_('Advanced'),this.tabAdvanced(ctx)]].forEach(function(t){ + var pane=t[2]; + pane.setAttribute('data-tab',t[0]); + pane.setAttribute('data-tab-title',t[1]); + if(t[0]===want)pane.setAttribute('data-tab-active','true'); + pane.addEventListener('cbi-tab-active',function(){ + self.currentTab=t[0]; + try{history.replaceState(null,'','#'+names[t[0]]);}catch(e){} + // The Status tab costs a fork per active interface, so it is fetched + // when it is opened rather than on every page load; initTabGroup fires + // this from a requestAnimationFrame, so the pane is in the DOM. + if(t[0]==='st')self.refreshStatus(); + if(t[0]==='cn')self.refreshCounters(); + self.fitEditor(); + }); + group.appendChild(pane); + }); + root.appendChild(group); + ui.tabs.initTabGroup(group.childNodes); + this.currentTab=want; + window.addEventListener('resize',function(){self.fitEditor();}); + root.firstChild.addEventListener('load',function(){self.fitEditor();}); + + if(this.readonly){ + this.applyReadonly(root); + notify(_('You have read-only access to this page, so editing and service control are disabled.'),'warning'); + } + + this.installPollers(); + return root; + }, + + // Each tab ticks at luci.main.pollinterval, only while it is open. Overview + // is six ubus calls and no forks; Status and Counters fork qosify-status, + // which runs tc twice per active interface, so a slow box raises that interval. + // Poll.step() holds the next tick until the promise this returns settles, and + // refreshStatus()/refreshCounters() drop a call overlapping the one fired on + // tab open, so a fork slower than the interval skips ticks instead of stacking up. + installPollers:function(){ + var self=this; + poll.add(function(){if(self.currentTab!=='ov'||self._n)return;return self.refreshOverview();}); + poll.add(function(){if(self.currentTab!=='st'||self._n)return;return self.refreshStatus();}); + poll.add(function(){if(self.currentTab!=='cn'||self._n)return;return self.refreshCounters();}); + }, + + tabOverview:function(ctx){ + return E('div',{'id':'qos-ov'},[ + E('div',{'class':'cbi-section','id':'qos-svc-sect'},this.buildSvcSect(ctx)), + E('div',{'class':'cbi-section','id':'qos-qs-sect'},this.buildQsSect(ctx)), + E('div',{'class':'cbi-section','id':'qos-cfg-sect'},this.buildCfgSect(ctx)), + this.buildSvcActs(ctx) + ]); + }, + + buildSvcSect:function(ctx){ + return [E('h3',{},_('Service')),this.renderSvcTable(ctx)]; + }, + + buildCfgSect:function(ctx){ + return [E('h3',{},_('Files')),this.renderCfgFiles(ctx)]; + }, + + buildQsSect:function(ctx){ + var self=this; + var sn=ifSect(); + var w=(sn&&uci.get('qosify',sn.id))||{}; + var enChecked=(w['.name']!=null&&!uciBool(w.disabled,false)); + function chk(name,val){return E('input',{'type':'checkbox','class':'cbi-input-checkbox','id':'q-'+name,'data-q':name,'checked':val?'checked':null});} + function txt(name,val,ph){return E('input',{'type':'text','class':'cbi-input-text','id':'q-'+name,'data-q':name,'value':val||'','placeholder':ph||''});} + function sel(name,val,opts,def){ + val=qv(val); + var s=E('select',{'class':'cbi-input-select','id':'q-'+name,'data-q':name}),sv=val||def,known=false; + if(def==null)s.appendChild(E('option',{'value':''},'--')); + opts.forEach(function(o){var a={'value':o};if(sv===o){a.selected='selected';known=true;}s.appendChild(E('option',a,o));}); + if(val&&!known)s.appendChild(E('option',{'value':val,'selected':'selected'},_('%s (current)').format(val))); + return s; + } + function num(name,val,ph){var e=txt(name,val,ph);e.type='number';return e;} + function pane(id,title,rows,top,cls){ + return E('div',{'data-tab':id,'data-tab-title':title,'data-tab-active':id===self._qsTab?'true':null}, + E('div',{'class':'qs-box cbi-section-node'+(cls?' '+cls:'')},(top||[]).concat(rows.map(function(r){return Array.isArray(r)?valRow(r[0],r[1]):r;})))); + } + + var enBadge=E('span',{'id':'q-en-badge'}); + this.updateEnBadge(enBadge,ctx,enChecked); + // qosify.init passes `option name` to add_interface(); without it the daemon + // gets an empty device and the section is never applied. Only `config + // interface` is named after the netifd interface; a `config device` names a + // netdev (the shipped config has `config device wandev` with `option name + // wan`), so its section name is never a safe prefill. + var isDev=!!(sn&&sn.type==='device'); + // qosify.init falls back to option bandwidth for an unset direction. + var bwPh=w.bandwidth?_('%s (from bandwidth)').format(w.bandwidth):_('e.g. %s').format('850mbit'); + // CAKE is only given nat/nonat when host_isolate is on; otherwise it gets + // flow isolation and nat has no effect at all. + var hiCb=chk('host_isolate',numBool(w.host_isolate,true)); + var natNote=desc(_('Only sent with host isolation on — add nat to common CAKE options to force it')); + natNote.classList.add('qs-note'); + hiCb.addEventListener('change',function(){natNote.style.display=hiCb.checked?'none':'';}); + natNote.style.display=hiCb.checked?'none':''; + // qosify.init only reads overhead and overhead_encap under overhead_type manual. + var ovSel=sel('overhead',w.overhead_type,OVH,'none'); + var manRows=[valRow(_('Manual overhead'),[num('ovh_bytes',w.overhead,'--'),desc(_('Sent to CAKE as overhead, in bytes.'))]), + valRow(_('Encapsulation overhead'),[sel('overhead_encap',w.overhead_encap,ENCAP),desc(_('Sent to CAKE as atm, noatm or ptm.'))])]; + function syncOvh(){manRows.forEach(function(r){r.style.display=ovSel.value==='manual'?'':'none';});} + ovSel.addEventListener('change',syncOvh); + syncOvh(); + var grp=E('div',{},[ + pane('qs-basic',_('Basic'),[ + [_('QoS Enabled'),[E('span',{'class':'qs-ctl'},[chk('enabled',enChecked),' ',enBadge]),desc(_('Unticked sets disabled 1 and qosify skips this section.'))]], + [isDev?_('Device'):_('Interface'),[txt('name',w.name||(sn?(isDev?'':sn.name):'wan'),_('e.g. %s').format(isDev?'eth0':'wan')),desc(isDev?_('Netdev to enable QoS on. Required.'):_('netifd interface to enable QoS on. Required.'))]], + [_('Upload bandwidth'),[txt('bw_up',w.bandwidth_up,bwPh),desc(_('Uplink bandwidth, same format as tc. Set just below line speed.'))]], + [_('Download bandwidth'),[txt('bw_down',w.bandwidth_down,bwPh),desc(_('Downlink bandwidth, same format as tc. Set just below line speed.'))]], + [_('Queueing mode'),[sel('mode',w.mode,MODES,'diffserv4'),desc(OPT_DESC.mode)]] + ]), + pane('qs-shaping',_('Shaping'),[ + [_('Download shaping'),[chk('ingress',numBool(w.ingress,true)),desc(OPT_DESC['if.ingress'])]], + [_('Upload shaping'),[chk('egress',numBool(w.egress,true)),desc(OPT_DESC['if.egress'])]], + [_('Automatic download rate'),[chk('autorate',numBool(w.autorate_ingress,false)),desc(OPT_DESC.autorate_ingress)]], + [_('NAT awareness'),[chk('nat',numBool(w.nat,!isDev)),desc(OPT_DESC.nat),natNote]], + [_('Host isolation'),[hiCb,desc(OPT_DESC.host_isolate)]] + ]), + pane('qs-overhead',_('Overhead'),[ + [_('Overhead preset'),[ovSel,desc(_('CAKE overhead keyword. Use none if unsure.'))]], + manRows[0], + manRows[1], + [_('Minimum packet unit (MPU)'),[num('overhead_mpu',w.overhead_mpu,'--'),desc(_('Sent to CAKE as mpu, in bytes.'))]], + [_('VLAN tags'),[sel('overhead_vlan',w.overhead_vlan,['0','1','2'],'0'),desc(_('Sent to CAKE as ether-vlan, once per tag.'))]] + ]), + pane('qs-advanced',_('Advanced'),[ + [_('Ingress CAKE options'),[txt('ing_opts',w.ingress_options,_('e.g. %s').format('triple-isolate memlimit 32mb')),desc(_('CAKE ingress options, space separated.'))]], + [_('Egress CAKE options'),[txt('egr_opts',w.egress_options,_('e.g. %s').format('wash')),desc(_('CAKE egress options, space separated.'))]], + [_('Common CAKE options'),[txt('opts',w.options,_('e.g. %s').format('overhead 46 memlimit 32mb')),desc(OPT_DESC.options)]] + ],[E('div',{'class':'cbi-tab-descr'},_('Invalid CAKE options can stop qosify starting.'))],'qs-wide') + ]); + // Every pane is marked, as the sub tabs share LuCI's stored tab id with the page tabs. + if(!grp.querySelector('[data-tab-active="true"]'))grp.firstChild.setAttribute('data-tab-active','true'); + grp.childNodes.forEach(function(p){p.addEventListener('cbi-tab-active',function(){self._qsTab=p.getAttribute('data-tab');});}); + // initTabGroup puts the tab menu before grp in its parent, so grp needs one. + var wrap=E('div',{},grp); + ui.tabs.initTabGroup(grp.childNodes); + return [ + E('h3',{},_('%s quick settings').format(sn?sn.type+(sn.name?' '+sn.name:''):'interface wan')), + wrap, + E('div',{'class':'cbi-page-actions'}, + E('button',{'class':'cbi-button cbi-button-apply','click':function(){return self.saveQuick();}},_('Save & Apply'))) + ]; + }, + + // The controls sit once, at the bottom of Overview, rather than under every tab. + buildSvcActs:function(ctx){ + var self=this,acts=E('div',{'class':'cbi-page-actions','id':'qos-svc-btns'}, + E('button',{'id':'qos-btn-auto','click':function(){return self.svcAction(self._auto?'disable':'enable');}})); + // Reload is the init script's reload_service(), a full ubus config push. + // Reload Rules re-reads the mapping files alone and leaves the qdiscs and + // interface config untouched -- what a rules edit actually needs. + [['start','cbi-button-apply',_('Start')],['restart','cbi-button-action',_('Restart')], + ['reload','cbi-button-reload',_('Reload')],['maps','cbi-button-reload',_('Reload Rules'),1], + ['stop','cbi-button-negative',_('Stop')]].forEach(function(b){ + acts.appendChild(document.createTextNode(' ')); + acts.appendChild(E('button',{'class':'cbi-button '+b[1],'id':'qos-btn-'+b[0],'title':b[3]?_('Re-read the mapping files only'):null, + 'click':function(){return b[3]?self.mapReload():self.svcAction(b[0]);}},b[2])); + }); + this.svcButtons(ctx,acts); + return acts; + }, + + // Buttons that do not apply to the current state are disabled. Unknown is not + // Missing: with the state unknown the actions stay clickable, so a stale ACL + // answers with the call's own error instead of a bar of dead buttons. + svcButtons:function(ctx,root){ + var ro=this.readonly||ctx.hasInit===false,un=ctx.running==null,b, + g=function(id){return root?root.querySelector('#'+id):$(id);}; + if((b=g('qos-btn-auto')))this.autoButton(ctx,b); + [['start',!ctx.running],['restart',ctx.running],['reload',ctx.running],['stop',ctx.running]].forEach(function(x){ + if((b=g('qos-btn-'+x[0])))b.disabled=ro||!(un||x[1]); + }); + // Reload Rules is a ubus call, so it needs the daemon up but not the init script. + if((b=g('qos-btn-maps')))b.disabled=this.readonly||ctx.running===false; + }, + + fillSect:function(id,nodes){ + var el=$(id); + if(!el)return; + dom.content(el,''); + nodes.forEach(function(n){el.appendChild(n);}); + this.applyReadonly(el); + }, + + applyReadonly:function(el){ + if(!this.readonly||!el)return; + el.querySelectorAll('input,select,textarea,button').forEach(function(x){ + if(!x.getAttribute('data-ro-ok'))x.setAttribute('disabled',''); + }); + }, + + // true reached, false timed out, null the last service.list went unanswered: + // an unanswered call says nothing about qosify, so it never counts as stopped. + waitForState:function(want,timeoutMs){ + var deadline=Date.now()+(timeoutMs||3000); + function tick(){ + return callServiceList('qosify').then(isRunning,function(){return null;}).then(function(up){ + if(up===want)return true; + if(Date.now()>=deadline)return up===null?null:false; + return new Promise(function(res){setTimeout(res,400);}).then(tick); + }); + } + return tick(); + }, + + waitForRunning:function(timeoutMs){return this.waitForState(true,timeoutMs);}, + waitForStopped:function(timeoutMs){return this.waitForState(false,timeoutMs);}, + + // The config is already written and uci reloaded by the time this runs, so an + // unanswered service.list must not abort the apply and leave qosify on the old + // config: rc init is a write ACL entry and answers when the read half does not. + applyService:function(){ + var self=this; + return callServiceList('qosify').then(isRunning,function(){return null;}).then(function(run){ + if(run==null)return callRcInit('qosify','restart').then(function(){return self.pushConfig();}); + if(run)return callRcInit('qosify','reload'); + return callRcInit('qosify','start').then(function(){ + return self.waitForRunning(4000); + }).then(function(up){ + if(up==null)throw new Error(_('rpcd is not answering for qosify, so the service state is unknown.')); + if(!up)throw new Error(_('qosify did not come up — check the system log')); + return self.pushConfig(); + }); + }); + }, + + // qosify.init in 25.12 and master pushes its config from service_running(), + // which rc.common only calls for `running`, so start and restart leave the + // daemon with no config. reload pushes it once the ubus object is up; on 24.10, + // where service_started() already did, the same config changes nothing. + pushConfig:function(){ + var n=10; + function tick(){ + return callQosifyStatus().then(function(){return callRcInit('qosify','reload');},function(){ + if(--n<=0)throw new Error(_('qosify did not appear on ubus, so its config was not applied — press Reload')); + return new Promise(function(r){setTimeout(r,400);}).then(tick); + }); + } + return tick(); + }, + + updateEnBadge:function(el,ctx,enChecked){ + if(ctx.active==null&&enChecked){el.className='label warning';dom.content(el,_('Status Unknown'));} + else if(ctx.active){el.className='label success';dom.content(el,_('Active'));} + else if(ctx.running&&enChecked){el.className='label warning';dom.content(el,_('Enabled — Not Shaping (check config)'));} + else if(enChecked){el.className='label warning';dom.content(el,_('Enabled — Not Running'));} + else{el.className='label danger';dom.content(el,_('Disabled'));} + }, + + svcNodes:function(ctx){ + // Unknown is not Missing: a call rpcd did not answer says nothing about qosify. + function tri(v,f){return v==null?badge('warning',_('Unknown')):f(v);} + var run; + if(ctx.running==null)run=badge('warning',_('Unknown')); + else if(ctx.running&&ctx.active==null)run=badge('warning',_('Running — Shaping Unknown')); + else if(ctx.running&&ctx.active)run=badge('success',_('Running & Shaping')); + else if(ctx.running)run=badge('warning',_('Running — Not Shaping')); + else run=badge('danger',_('Not Running')); + // One line for the condition behind the Unknowns in the table: it goes on the + // first row that reads Unknown, rather than on a row that is known or on + // every row it reaches. + var note=ctx.rpcOk===false?E('em',{}, + _('rpcd is not answering for qosify — check the ACL in /usr/share/rpcd/acl.d and restart rpcd')):null; + function mark(n,unk){if(!note||!unk)return n;var w=[n,' ',note];note=null;return w;} + return { + // Keyed in display order, so the note lands on the first Unknown shown. + run:mark(run,ctx.running==null), + up:ctx.uptime!=null?'%t'.format(Math.floor(ctx.uptime)):'-', + auto:mark(tri(ctx.enabled,function(v){return badge(v?'success':'danger',v?_('Enabled'):_('Disabled'));}),ctx.enabled==null), + shaped:mark(tri(ctx.shaped,function(v){return v?N_(v,'%d interface','%d interfaces').format(v):E('em',{},_('none'));}),ctx.shaped==null), + init:mark(tri(ctx.hasInit,function(v){return badge(v?'success':'danger',v?_('Available'):_('Missing'));}),ctx.hasInit==null) + }; + }, + + // Status, then the per-interface rows from ubus call qosify status, which cost + // no forks, then the init script. + renderSvcTable:function(ctx){ + var n=this.svcNodes(ctx),rows=[ + kvRow(_('Status'),n.run), + kvRow(_('Uptime'),n.up), + kvRow(_('Autostart'),n.auto), + kvRow(_('Shaping'),n.shaped) + ]; + ['interfaces','devices'].forEach(function(g){ + var t=ctx.status&&ctx.status[g],k,e; + for(k in t){ + e=t[k]||{}; + rows.push(kvRow((g==='devices'?_('device %s'):_('interface %s')).format(k),[ + badge(e.active?'success':'danger',e.active?_('active'):_('inactive')),' ', + _('device: %s, ingress: %s, egress: %s').format(e.ifname||'-',e.ingress?_('yes'):_('no'),e.egress?_('yes'):_('no'))])); + } + }); + rows.push(kvRow(E('code',{},'/etc/init.d/qosify'),n.init)); + return kvTable(rows,'qos-svc-tbl'); + }, + + // Rows follow the configured interfaces, so the table is swapped whole; it + // holds no input or focus. + updateSvcTable:function(ctx){ + var t=$('qos-svc-tbl'); + if(t)t.parentNode.replaceChild(this.renderSvcTable(ctx),t); + this.svcButtons(ctx); + }, + + // The label is the state, so with the state unknown there is nothing to toggle. + // The click reads _auto, so a tick that changes the state changes the action. + autoButton:function(ctx,el){ + this._auto=ctx.enabled; + el.disabled=this.readonly||ctx.enabled==null; + if(ctx.enabled==null){ + el.className='cbi-button'; + el.title=_('Autostart state unknown — rpcd did not answer'); + dom.content(el,_('Unknown')); + return; + } + el.className='cbi-button '+(ctx.enabled?'cbi-button-positive':'cbi-button-negative'); + el.title=ctx.enabled?_('Click to disable autostart'):_('Click to enable autostart'); + dom.content(el,ctx.enabled?_('Enabled'):_('Disabled')); + }, + + renderCfgFiles:function(ctx){ + var rulesN=(ctx.rulesN!=null)?ctx.rulesN:countRules(ctx.rulesText); + var cfgOk=(ctx.cfgOk!=null)?ctx.cfgOk:((ctx.cfgRaw||'').length>10&&/(^|\n)config /.test(ctx.cfgRaw||'')); + var secN=uci.sections('qosify').length; + function row(path,st,ok,n){ + return [E('code',{},path),st?(ok?badge('success',_('Valid')):badge('warning',_('Found (empty or invalid)'))):badge('danger',_('Missing')), + st?n:'-',st?fmtSize(st.size):'-',st?fmtMtime(st.mtime):'-']; + } + return gridTable([_('File'),_('Status'),_('Entries'),_('Size'),_('Modified')],[ + row(UCI_PATH,ctx.cfgStat,cfgOk,N_(secN,'%d section','%d sections').format(secN)), + row(RULES_PATH,ctx.rulesStat,rulesN>0,N_(rulesN,'%d rule','%d rules').format(rulesN)) + ]); + }, + + tabConfig:function(ctx){ + var self=this; + var section=E('div',{'id':'qos-cf'}); + var classes=this.getClasses(); + var dscpChoices=classes.map(function(c){return c.name;}).concat(dscpList()); + function head(p,a,b){ + p.qaCells=[[_('section type'),E('select',{'class':'cbi-input-select','id':'qac-'+p.id.slice(9)+'-type','aria-label':_('section type')}, + [E('option',{'value':a},'config '+a),E('option',{'value':b},'config '+b)])], + [_('section name'),E('input',{'type':'text','class':'cbi-input-text','id':'qac-'+p.id.slice(9)+'-name','placeholder':_('section name'),'aria-label':_('section name')})]]; + } + function add(p){return E('button',{'class':'cbi-button cbi-button-add','click':function(){return self.qacAdd(p);}},_('Add'));} + var qa=E('div',{'class':'qa'}); + + // config defaults — add_defaults() in qosify.init + var qadDef=E('div',{'id':'qac-opts-defaults'}); + this.qaInput(qadDef,'defaults','list','/etc/qosify/*.conf'); + this.qaNum(qadDef,'timeout','3600'); + this.qaSelect(qadDef,'dscp_default_tcp',dscpChoices); + this.qaSelect(qadDef,'dscp_default_udp',dscpChoices); + this.qaSelect(qadDef,'dscp_icmp',dscpChoices); + this.qaSelect(qadDef,'dscp_prio',dscpChoices); + this.qaSelect(qadDef,'dscp_bulk',dscpChoices); + this.qaNum(qadDef,'prio_max_avg_pkt_len','500'); + this.qaNum(qadDef,'bulk_trigger_pps','100'); + this.qaNum(qadDef,'bulk_trigger_timeout','5'); + + // config class / config alias — add_class() + var qadCls=E('div',{'id':'qac-opts-class'}); + head(qadCls,'class','alias'); + this.qaSelect(qadCls,'value',dscpList()); + this.qaSelect(qadCls,'ingress',dscpList()); + this.qaSelect(qadCls,'egress',dscpList()); + this.qaSelect(qadCls,'dscp_prio',dscpChoices); + this.qaSelect(qadCls,'dscp_bulk',dscpChoices); + this.qaNum(qadCls,'prio_max_avg_pkt_len','500'); + this.qaNum(qadCls,'bulk_trigger_pps','100'); + this.qaNum(qadCls,'bulk_trigger_timeout','5'); + + // config interface / config device — add_interface() + var qadIf=E('div',{'id':'qac-opts-interface'}); + head(qadIf,'interface','device'); + this.qaInput(qadIf,'name','option','wan'); + this.qaSelect(qadIf,'disabled',['0','1']); + this.qaInput(qadIf,'bandwidth_up','option','100mbit'); + this.qaInput(qadIf,'bandwidth_down','option','100mbit'); + this.qaInput(qadIf,'bandwidth','option','100mbit'); + this.qaSelect(qadIf,'mode',MODES); + this.qaSelect(qadIf,'ingress',['0','1']); + this.qaSelect(qadIf,'egress',['0','1']); + this.qaSelect(qadIf,'nat',['0','1']); + this.qaSelect(qadIf,'host_isolate',['0','1']); + this.qaSelect(qadIf,'autorate_ingress',['0','1']); + this.qaSelect(qadIf,'overhead_type',OVH); + this.qaNum(qadIf,'overhead','44'); + this.qaSelect(qadIf,'overhead_encap',ENCAP); + this.qaNum(qadIf,'overhead_mpu','84'); + this.qaSelect(qadIf,'overhead_vlan',['0','1','2']); + this.qaInput(qadIf,'ingress_options','option','triple-isolate'); + this.qaInput(qadIf,'egress_options','option','triple-isolate wash'); + this.qaInput(qadIf,'options','option','overhead 44 mpu 84'); + + // Each stanza type folds on its own; the option reference is read back out + // of the form, so the two can never disagree. + [[qadDef,'defaults','config defaults',null,''], + [qadCls,'class','config class / config alias',_('Section name is the class name that rules and dscp_* values refer to. qosify.init reads config alias the same way as config class.'),''], + [qadIf,'interface','config interface / config device',_('config device takes the same options, with name set to a netdev. nat defaults to 1 for interfaces and 0 for devices.'),'if.'] + ].forEach(function(p){ + var opts=p[0].qaCells.filter(function(c){return c[1].hasAttribute('data-opt');}); + self.qaGrid(p[0],p[0].qaCells); + qa.appendChild(fold('qos-qa-'+p[1],_('Quick Add: %s').format(p[2]),[p[0],E('div',{'class':'qa-foot'},[ + refBox(_('Options'),p[3],opts.map(function(c){return [c[0],OPT_DESC[p[4]+c[0]]||OPT_DESC[c[0]]||''];})),add(p[1])])],false)); + }); + qa.appendChild(fold('qos-qa-ref',_('Reference'),[ + this.classRef('qos-cls-cfg'), + refBox(_('DSCP values'),_('DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF (NQB needs qosify from June 2026; 24.10 and 25.12 do not have it). A raw value from 0 to 63 is accepted too, and any dscp_* value may also name a class. Prefix with + to override only when the DSCP field is zero.'),[]), + refBox(_('Defaults'),_('Defaults qosify applies when a key is absent — interface: mode diffserv4, ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/prio_max_avg_pkt_len 0 (disabled).'),[]) + ],false)); + section.appendChild(qa); + section.addEventListener('toggle',function(){self.fitEditor();},true); + section.appendChild(this.editorSect('qos-config-ta',UCI_PATH,ctx.cfgRaw,ctx.cfgStat,function(){return self.clearCfg();},function(){return self.saveConfig();})); + return section; + }, + + + + qaId:function(parent,opt){return (parent.id||'qac')+'-'+opt;}, + qaGrid:function(parent,cells){ + var n=Math.ceil(cells.length/QA_COLS),cols=Math.ceil(cells.length/n),w='width:'+(100/cols).toFixed(2)+'%',i,c; + for(i=0;i[-]'],['udp:','udp:[-]'],['both:','tcp: + udp:'],['dns:','dns:'],['dnsr:','dns:/'],['dns_c:','dns_c:'],['dns_cr:','dns_c:/'],['ipv4:',_('IPv4 address, e.g. 1.1.1.1')],['ipv6:',_('IPv6 address, e.g. ff01::1')]].forEach(function(o){ + qarType.appendChild(E('option',{'value':o[0]},[document.createTextNode(o[1])])); + }); + var qarCls=E('select',{'class':'cbi-input-select','id':'qar-cls','aria-label':'dscp'},this.getClasses().map(function(c){return E('option',{'value':c.name},clsOpt(c));})); + var qr=[['match',5,qarType],['',8,E('input',{'type':'text','class':'cbi-input-text','id':'qar-val','aria-label':_('value'),'placeholder':_('e.g. %s').format('4500')})], + ['dscp',6,qarCls],['+',1,E('input',{'type':'checkbox','class':'cbi-input-checkbox','id':'qar-prio','aria-label':_('Only override the DSCP value if it is zero')})], + ['',2,E('button',{'class':'cbi-button cbi-button-add','click':function(){return self.qarAdd();}},_('Add'))]]; + section.appendChild(E('div',{'class':'qa'},fold('qos-qa-rule',_('Quick Add'),[ + colTable(qr,[E('tr',{'class':'tr cbi-section-table-titles'},qr.map(function(c){return E('th',{'class':'th','title':c[0]},c[0]);})), + E('tr',{'class':'tr cbi-section-table-row'},qr.map(function(c){return E('td',{'class':'td','data-title':c[0]},c[2]);}))]), + E('div',{'class':'qa-foot'},[refBox(_('Mapping file syntax'),_('Each line has two whitespace separated fields, match and dscp. dscp can be a raw value, a codepoint like CS0, or a class name. DNS entries are compared in the order in which they are specified, using the first matching entry.'),[ + ['tcp:[-]',_('TCP single port, or range from to ')], + ['udp:[-]',_('UDP single port, or range from to ')], + ['',_('IPv4 address, e.g. 1.1.1.1')], + ['',_('IPv6 address, e.g. ff01::1')], + ['dns:',_('fnmatch() pattern supporting * and ? as wildcard characters')], + ['dns:/',_('POSIX.2 extended regular expression for matching hostnames. Only works if dns lookups are passed to qosify via the add_dns_host ubus call.')], + ['dns_c:...',_('Like dns:... but only matches cname entries')], + ['+',_('Only override the DSCP value if it is zero')] + ]), + this.classRef('qos-cls-ru')]) + ],false))); + section.addEventListener('toggle',function(){self.fitEditor();},true); + section.appendChild(this.editorSect('qos-rules-ta',RULES_PATH,ctx.rulesText,ctx.rulesStat,function(){return self.clearRules();},function(){return self.saveRules();})); + return section; + }, + + tabAdvanced:function(ctx){ + var self=this; + function bkRow(i,path,fn,st){ + return [E('code',{},path),E('span',{'id':'qos-bk-sz-'+i},st?fmtSize(st.size):'-'), + E('span',{'id':'qos-bk-mt-'+i},st?fmtMtime(st.mtime):'-'),self.dlBtn(path,fn)]; + } + function reRow(path,id){ + // The native picker ignores the theme, so it stays hidden behind a LuCI button. + var nm=E('span',{'class':'qos-up-name'},_('No file selected')), + fi=E('input',{'type':'file','id':id,'style':'display:none','change':function(){nm.textContent=fi.files[0]?fi.files[0].name:_('No file selected');}}); + return [E('code',{},path),E('span',{},[fi,E('button',{'class':'cbi-button','click':function(){fi.click();}},_('Choose file…')),' ',nm])]; + } + return E('div',{'id':'qos-ad'},[ + sect(_('Backup'),[ + sdesc(_('Download the current files from the router.')), + gridTable([_('File'),_('Size'),_('Modified'),_('Download')],[ + bkRow(0,UCI_PATH,'qosify',ctx.cfgStat), + bkRow(1,RULES_PATH,'00-defaults.conf',ctx.rulesStat) + ]) + ]), + sect(_('Restore'),[ + sdesc(_('The selected files replace the ones on the router and qosify is reloaded.')), + gridTable([_('File'),_('Upload')],[ + reRow(UCI_PATH,'qos-up-cfg'), + reRow(RULES_PATH,'qos-up-rules') + ]), + E('div',{'class':'cbi-page-actions'}, + E('button',{'class':'cbi-button cbi-button-apply','click':function(){return self.uploadFiles();}},_('Upload & Apply'))) + ]), + sect(_('Maintenance'),[ + E('div',{'class':'cbi-section-node'},valRow(_('Re-check devices'),[ + E('button',{'class':'cbi-button cbi-button-action','id':'qos-btn-chkdev','click':function(){return self.checkDevices();}},_('Check Devices')), + desc(_('Re-runs the daemon\'s own device pass: every shaped section is looked up again, one whose device now exists is started and one whose device has gone is stopped. Nothing is reported back by the call — the result shows in the Service table on the Overview tab.'))])) + ]), + sect(_('Defaults'),[ + E('div',{'class':'cbi-section-node'},valRow(_('Restore qosify defaults'), + E('button',{'class':'cbi-button cbi-button-negative','click':function(){return self.resetDefaults();}},_('Reset')))) + ]) + ]); + }, + + updateFiles:function(ctx){ + [ctx.cfgStat,ctx.rulesStat].forEach(function(st,i){ + var z=$('qos-bk-sz-'+i),m=$('qos-bk-mt-'+i); + if(z)z.textContent=st?fmtSize(st.size):'-'; + if(m)m.textContent=st?fmtMtime(st.mtime):'-'; + }); + }, + + dlBtn:function(path,fn){ + return E('button',{'class':'cbi-button cbi-button-action','data-ro-ok':'1','click':function(){ + return fs.read(path).then(function(content){ + var b=new Blob([content||''],{type:'application/octet-stream'}); + var url=URL.createObjectURL(b); + var a=E('a',{'href':url,'download':fn,'style':'display:none'}); + document.body.appendChild(a); + a.click(); + setTimeout(function(){ + URL.revokeObjectURL(url); + if(a.parentNode)a.parentNode.removeChild(a); + },2000); + }).catch(function(e){ + notify(_('Could not read %s: %s').format(path,e),'danger'); + }); + }},_('Download')); + }, + + + tabStatus:function(ctx){ + var section=E('div',{'id':'qos-st'}); + var fs1=sect(_('qosify-status')); + var body=E('div',{'id':'qos-st-body'},[ + E('pre',{'id':'qos-st-pre','style':'display:none'}), + E('div',{'id':'qos-st-msg'}) + ]); + this.fillStatus(body,ctx); + fs1.appendChild(body); + section.appendChild(fs1); + return section; + }, + + // ubus call qosify get_stats. Master adds ebpf_map_entries, last_reload_time, + // dns_cache and classes/dscp/dns tables; 24.10 (1501e09) returns + // qosify_map_stats() at the top level, one table per class, packets only. + // Only what the reply contains is rendered. + isCounter:function(v){return !!v&&typeof v==='object'&&(v.packets!=null||v.bytes!=null);}, + // qosify_map_get_ebpf_entry_count() sums the IPv4 and IPv6 address maps only. + // 1501e09 sends none of these, so there the section stays hidden. + infoNodes:function(st){ + var rows=[]; + if(st.ebpf_map_entries!=null)rows.push(['ebpf_map_entries',String(st.ebpf_map_entries)]); + if(st.last_reload_time)rows.push(['last_reload_time',fmtMtime(st.last_reload_time)]); + if(st.dns_cache)rows.push(['dns_cache','size %d, hits %d, misses %d'.format(st.dns_cache.size||0,st.dns_cache.hits||0,st.dns_cache.misses||0)]); + if(!rows.length)return null; + return E('div',{'class':'qbox'},E('table',{'class':'table'},rows.map(function(r){return kvRow(E('code',{},r[0]),r[1]);}))); + }, + + // dump lists port, address and DNS entries, but pattern_stats is the only + // per-entry counter the datapath keeps, so only DNS patterns are listed; the + // rest is class totals. A raw DSCP as a number, -1 for anything + // __qosify_map_dscp_value() would reject (strtoul base 0, below 64). + dscpVal:function(v){ + var s=String(v==null?'':v).replace(/^\+/,'').toUpperCase(),n; + if(DSCP_VAL[s]!=null)return DSCP_VAL[s]; + n=dscpNum(s); + return n===null||n>=64?-1:n; + }, + // What each class marks with; ingress and egress already fall back to value. + dscpMarks:function(){ + var m={}; + this.getClasses().forEach(function(c){ + m[c.name]=(c.ingress===c.egress)?c.ingress:c.ingress+'/'+c.egress; + }); + return m; + }, + dscpRanks:function(){ + var m={},self=this; + this.getClasses().forEach(function(c){m[c.name]=dscpRank(self.dscpVal(c.egress||c.ingress));}); + return m; + }, + // DNS rows ordered by dscpRank(). Entries added over ubus (user, no file) carry + // a timeout and follow the file entries, so the MAP_ROWS cut falls on them. + mapRows:function(entries){ + var rows=[],dyn=[],cls=this.dscpRanks(),self=this,i,e,a,rk; + for(i=0;iMAP_ROWS?_('DNS Entries (%d of %d)').format(MAP_ROWS,rows.length):_('DNS Entries (%d)').format(rows.length); + if(lg&&lg.textContent!==t)lg.textContent=t; + var mb=$('qos-cn-map-box'),mh=$('qos-cn-map-head'); + if(mb&&mh)mh.style.paddingRight=Math.max(0,mb.offsetWidth-mb.clientWidth)+'px'; + }, + + // One service list and one get_stats, then dump alongside qosify-status: the + // map listing's traffic column reads the stats just fetched, so both are + // chained after them, and qosify-status is only forked while qosify runs. + // Master always opens the dns table, so its absence identifies the build + // rather than a quiet period: 25.12 and 24.10 (1501e09) have none, so dump is + // not called and DNS Entries stays hidden, and it shows by itself on any build + // that gains the table. fillMap() skips the rebuild while its signature is + // unchanged, so the one-entry-per-port dump costs a compare, not a redraw. + refreshCounters:function(){ + var self=this; + if(self.currentTab!=='cn'||self._cn)return Promise.resolve(); + self._cn=true; + return Promise.all([ + callServiceList('qosify').catch(function(){return null;}), + callQosifyStats().catch(function(){return null;}) + ]).then(function(d){ + var ctx={running:d[0]?isRunning(d[0]):null,stats:d[1]}; + self._cnStats=ctx.running?ctx.stats:null; + if(ctx.stats)self._cnDns=ctx.stats.dns!=null; + nqbSeen(ctx.stats); + self.fillCounters(ctx); + return Promise.all([self._cnDns?callQosifyDump().catch(function(){return null;}):null,ctx.running, + ctx.running&&!self.readonly?L.resolveDefault(fs.exec('/usr/sbin/qosify-status',[]),null):null]); + }).then(function(r){ + self.fillTins(r[1],r[2]); + self.fillMap(r[0],self._cnStats&&self._cnStats.dns); + }).finally(function(){self._cn=false;}); + }, + + tabCounters:function(){ + return E('div',{'id':'qos-cn'},[ + sect(_('Traffic by Class'),[E('div',{'id':'qos-cn-msg'}),E('div',{'id':'qos-cn-bars'})]), + sect(_('Traffic by CAKE Tin'),E('div',{'id':'qos-cn-tins'},emP(_('Loading...'))),{'id':'qos-cn-tin-sect','style':'display:none'}), + sect('get_stats',E('div',{'id':'qos-cn-info'}),{'id':'qos-cn-info-sect','style':'display:none'}), + sect(_('DNS Entries'),E('div',{'id':'qos-cn-map'},emP(_('Loading...'))),{'id':'qos-cn-map-sect','style':'display:none'}) + ]); + }, + + // Cumulative totals since the last reload, EF first and bulk last. A + // dscp_default_* naming a class is counted against that class, so the two + // default slots would double-count and are skipped. + // Grouped and coloured by the tin the class's egress codepoint lands in under + // mode, highest priority tin first as the tin bars are, then by codepoint within + // a tin. With no single mode to fold by, codepoint order and a colour per name. + classTotals:function(st,mode){ + var cls=st&&st.classes,k,rows=[],names=[],total=0,bytes=null,self=this, + fold=MODES.indexOf(mode)>=0,rank=this.dscpRanks(),mark=this.dscpMarks(),tin={}; + if(fold)this.getClasses().forEach(function(c){ + var v=self.dscpVal(c.egress||c.ingress); + tin[c.name]=v<0?-1:+TIN_MAP[mode].charAt(v); + }); + if(!cls){ + cls={}; + for(k in st)if(self.isCounter(st[k]))cls[k]=st[k]; + } + for(k in cls)if(cls[k].packets!=null&&!CN_SKIP[k])names.push(k); + names.sort(); + names.forEach(function(n,ix){ + var v=cls[n].packets||0; + total+=v; + if(cls[n].bytes!=null)bytes=(bytes||0)+cls[n].bytes; + var t=tin[n]!=null?tin[n]:-1; + rows.push({name:n,v:v,bytes:cls[n].bytes,tin:t, + color:!fold?CN_COLORS[ix%CN_COLORS.length]:t<0?CN_NONE:TIN_COLORS[mode][t], + mark:mark[n]||'',rk:rank[n]!=null?rank[n]:-1000}); + }); + rows.sort(function(a,b){return b.tin-a.tin||b.rk-a.rk||(a.name=0)mode=w;}); + if(r.indexOf(mode)<0)r.push(mode); + }); + }); + }); + return r; + }, + + // qosify-status, as the Status tab prints it: tc -s qdisc for each shaped + // direction. q_cake.c prints a column per tin in tin_order, lowest priority + // first, so a column is a TIN_COLORS index; rows are reversed to put the + // highest priority tin first, as the class bars are. Qdiscs running the same + // mode are summed tin by tin into one chart, egress and ingress together; a + // mode only one direction runs gets a chart of its own. + cakeTins:function(txt){ + var blk=[],grp=[],key={},b=null; + String(txt||'').split('\n').forEach(function(l){ + var m,w; + if(/^===== (?:interface|device) \S+: /.test(l)||/^(egress|ingress) status:$/.test(l))b=null; + else if(/^qdisc /.test(l)){ + w=l.split(/\s+/).filter(function(x){return MODES.indexOf(x)>=0;}); + b=/^qdisc cake /.test(l)?{mode:w.pop()}:null; + if(b)blk.push(b); + } + else if(b&&!b.names&&/^\s+(Bulk|Tin 0)\b/.test(l))b.names=l.trim().split(/\s{2,}/); + else if(b&&b.names&&(m=l.match(/^ (pkts|bytes|drops|marks)\s+(.*)$/))) + b[m[1]]=m[2].trim().split(/\s+/).map(Number); + }); + blk.forEach(function(b){ + if(!b.names||!b.pkts)return; + var k=b.mode+'|'+b.names.join('|'),g=key[k]; + if(!g)grp.push(g=key[k]={mode:b.mode,names:b.names,pkts:[],bytes:[],drops:[],marks:[]}); + ['pkts','bytes','drops','marks'].forEach(function(f){ + if(!b[f])g[f]=null; + else if(g[f])b[f].forEach(function(v,i){g[f][i]=(g[f][i]||0)+v;}); + }); + }); + return grp.map(function(g){ + var c=TIN_COLORS[g.mode],n=g.names.length,rows=g.names.map(function(t,i){ + var r={name:t,v:g.pkts[i]||0,bytes:g.bytes?g.bytes[i]||0:null, + drops:g.drops?g.drops[i]||0:null,marks:g.marks?g.marks[i]||0:null, + color:c&&c.length===n?c[i]:CN_COLORS[i%CN_COLORS.length]}; + return r; + }).reverse(); + rows.total=rows.reduce(function(t,r){return t+r.v;},0); + rows.bytes=g.bytes?rows.reduce(function(t,r){return t+r.bytes;},0):null; + return rows; + }); + }, + + // A compact box, header table above the rows as on DNS Entries: name, codepoint where a row has one, a + // .cbi-progressbar, then the counters under the names their source uses + // (get_stats packets/bytes, tc pkts/bytes/drops) and share, with a total row. + // Built again only when the rows or columns change; otherwise cells and bar + // widths are set in place. Length is (row/largest row)^BAR_EXP, a non-zero + // row kept at 1%. + drawChart:function(box,rows,empty,head){ + var total=rows.total||0,max=0,c=box.qosChart,sig, + dcol=rows.some(function(r){return r.mark;}), + bcol=rows.bytes!=null, + xcol=rows.some(function(r){return r.drops!=null;}), + pk=xcol?'pkts':'packets'; + if(!rows.length){box.qosChart=null;dom.content(box,emP(empty));return;} + rows.forEach(function(r){if(r.v>max)max=r.v;}); + sig=[dcol,bcol,xcol].concat(rows.map(function(r){return r.name;})).join('\n'); + function td(n,t){return E('td',{'class':n?'td qn':'td left','data-title':t});} + function set(el,v){if(el&&el.textContent!==v)el.textContent=v;} + function num(n){return Number(n||0).toLocaleString();} + if(!c||c.sig!==sig){ + var cols=[[head,20]]; + if(dcol)cols.push(['dscp',10]); + cols.push(['',34],[pk,13,1]); + if(bcol)cols.push(['bytes',13,1]); + if(xcol)cols.push(['drops',9,1]); + cols.push([_('share'),9,1]); + c=box.qosChart={sig:sig,rows:[]}; + var trs=rows.map(function(){ + var o={name:td(0,head),dscp:dcol?td(0,'dscp'):null,fill:E('div'),pkt:td(1,pk), + bytes:bcol?td(1,'bytes'):null,drops:xcol?td(1,'drops'):null,share:td(1,_('share'))}; + o.tr=E('tr',{'class':'tr'},[o.name,o.dscp||'', + E('td',{'class':'td'},E('div',{'class':'cbi-progressbar'},o.fill)), + o.pkt,o.bytes||'',o.drops||'',o.share]); + c.rows.push(o); + return o.tr; + }); + c.tot={pkt:td(1,pk),bytes:bcol?td(1,'bytes'):null,drops:xcol?td(1,'drops'):null}; + trs.push(E('tr',{'class':'tr qt'},[E('td',{'class':'td left'},_('total')),dcol?E('td',{'class':'td'}):'',E('td',{'class':'td'}), + c.tot.pkt,c.tot.bytes||'',c.tot.drops||'',E('td',{'class':'td qn'},'100%')])); + dom.content(box,E('div',{'class':'qbox'},[colHead(cols),colTable(cols,trs)])); + } + rows.forEach(function(r,i){ + var o=c.rows[i],share=total?(r.v/total)*100:0, + len=max&&r.v?Math.max(Math.pow(r.v/max,BAR_EXP)*100,1):0, + tip=r.marks!=null?'marks %d'.format(r.marks):''; + set(o.name,r.name); + if(o.name.title!==r.name)o.name.title=r.name; + set(o.dscp,r.mark||''); + set(o.pkt,num(r.v)); + if(o.bytes)set(o.bytes,'%1024.2mB'.format(r.bytes||0)); + if(o.drops)set(o.drops,r.drops!=null?num(r.drops):'-'); + set(o.share,fmtShare(share)); + if(o.tr.title!==tip)o.tr.title=tip; + o.fill.style.width=len.toFixed(2)+'%'; + o.fill.style.background=r.color; + }); + set(c.tot.pkt,num(total)); + if(c.tot.bytes)set(c.tot.bytes,'%1024.2mB'.format(rows.bytes)); + if(c.tot.drops)set(c.tot.drops,num(rows.reduce(function(t,r){return t+(r.drops||0);},0))); + }, + + drawBars:function(){ + var st=this._cnStats,box=$('qos-cn-bars'),m=this.cakeModes(); + if(!box)return; + if(st)this.drawChart(box,this.classTotals(st,m.length===1?m[0]:null),_('No per-class counters.'),'class'); + else{box.qosChart=null;dom.content(box,'');} + }, + + // CAKE's own per-tin counters, per qdisc since it was created, so they need + // not add up to the class totals, which count what the classifier matched. + // A fork that fails keeps the last charts rather than collapsing the section. + fillTins:function(running,r){ + var sect=$('qos-cn-tin-sect'),box=$('qos-cn-tins'),self=this,t; + if(!sect||!box)return; + sect.style.display=running?'':'none'; + if(!running)return; + if(!this.readonly&&!r&&this._tinOk)return; + t=this.readonly?[]:this.cakeTins(r&&r.stdout); + this._tinOk=t.length>0; + if(!t.length){ + box.qosGroups=0; + dom.content(box,emP(this.readonly?_('Requires write access.'):r&&r.stdout?_('No CAKE tin statistics.'):_('No output.'))); + return; + } + if(box.qosGroups!==t.length){ + box.qosGroups=t.length; + dom.content(box,t.map(function(){return E('div');})); + } + t.forEach(function(rows,i){self.drawChart(box.childNodes[i],rows,'','tin');}); + }, + + // Nothing here survives the daemon: get_stats counts since the last reload and + // the tin figures come from qdiscs a stop removes. So a stopped qosify clears + // the charts and drops every box but the notice, as the Status tab does, rather + // than leaving the last poll's numbers on screen looking live. _cnDns is reset + // with it, or DNS Entries would keep a stale listing until stats return. + fillCounters:function(ctx){ + var msg=$('qos-cn-msg'),info=$('qos-cn-info'),is=$('qos-cn-info-sect'), + nodes=ctx.running&&ctx.stats?this.infoNodes(ctx.stats):null; + if(is)is.style.display=nodes?'':'none'; + if(!ctx.running){ + this._cnDns=false; + if(info)dom.content(info,''); + this.drawBars(); + if(msg)dom.content(msg,E('div',{'class':'alert-message warning'},ctx.running==null? + _('rpcd is not answering for qosify, so the service state is unknown.'): + _('qosify is not running. Start from the Overview tab.'))); + return; + } + if(msg)dom.content(msg,ctx.stats?'':emP(_('get_stats did not answer.'))); + this.drawBars(); + if(info)dom.content(info,nodes||''); + }, + + // Rebuilt only when the listing's shape changes; otherwise only the figures + // and footer are rewritten. + fillMap:function(r,dns){ + var box=$('qos-cn-map'),lg=$('qos-cn-map-sect-title'),sc=$('qos-cn-map-sect'); + if(sc)sc.style.display=this._cnDns?'':'none'; + if(!box||!this._cnDns)return; + var e=(r&&r.entries)||[],rows=this.mapRows(e),sig,t; + if(!rows.length){ + this._mapSig=this._mapCells=null; + if(lg)lg.textContent=_('DNS Entries'); + t=_('No DNS entries.'); + if(box.textContent!==t)dom.content(box,emP(t)); + return; + } + sig=this.mapSig(rows); + if(sig!==this._mapSig){ + t=$('qos-cn-map-box'); + t=t?t.scrollTop:0; + this._mapSig=sig; + dom.content(box,this.mapNodes(rows)); + $('qos-cn-map-box').scrollTop=t; + } + this.mapValues(rows,dns); + }, + + + // load=true is the page-load subset: only what silently stops qosify using + // the config or the rules file. + lintAll:function(load){ + var out=[],all=uci.sections('qosify'); + function walk(type,dev){ + uci.sections('qosify',type,function(s){ + if(uciBool(s.disabled,false))return; + ifLint(s,dev).forEach(function(t){out.push(s['.name']+': '+t);}); + }); + } + if(!load){walk('interface',false);walk('device',true);} + cfgLint(all).forEach(function(x){if(!load||x.hard)out.push(x.t);}); + if(!rulesLoaded(all))out.push(_('%s is not in the defaults list — qosify does not load it, so the Rules tab has no effect').format(RULES_PATH)); + return out; + }, + + // Runs on every poll tick and twice per open, so only the summary table is + // rebuilt: replacing the
 would throw away the scroll position while it
+	// is being read. ctx.qstatus null means the fork has not returned yet, '' means
+	// it returned nothing -- the two used to look the same on screen.
+	fillStatus:function(body,ctx){
+		var pre=body.querySelector('#qos-st-pre'),msg=body.querySelector('#qos-st-msg');
+		if(!pre||!msg)return;
+		var note=function(t){dom.content(msg,emP(t));};
+		if(!ctx.running){
+			pre.style.display='none';
+			dom.content(msg,E('div',{'class':'alert-message warning'},ctx.running==null?
+				_('rpcd is not answering for qosify, so the service state is unknown.'):
+				_('qosify is not running. Start from the Overview tab.')));
+			return;
+		}
+		pre.style.display=ctx.qstatus?'':'none';
+		if(ctx.qstatus){
+			if(pre.textContent!==ctx.qstatus)pre.textContent=ctx.qstatus;
+			dom.content(msg,'');
+		}
+		else if(this.readonly)note(_('The detailed tc output needs write access to this page.'));
+		else if(ctx.qstatus==null)note(_('Reading tc output...'));
+		else note(_('qosify-status returned no output.'));
+	},
+
+	// === Actions ===
+
+	mapReload:function(){
+		var self=this;
+		self.lock();
+		ui.showModal(_('Working'),[E('p',{},_('Re-reading the mapping files...'))]);
+		return callQosifyReload().then(function(){
+			notify(_('Mapping files reloaded.'),'info');
+			return self.refreshOverview();
+		}).catch(function(e){
+			notify(_('Reload failed: %s').format(e),'danger');
+		}).finally(function(){
+			ui.hideModal();
+			self.unlock();
+		});
+	},
+
+	// check_devices arms a 10 ms uloop timer and returns before the pass runs, so
+	// wait for it before reading the state back.
+	checkDevices:function(){
+		var self=this;
+		self.lock();
+		ui.showModal(_('Working'),[E('p',{},_('Re-checking devices...'))]);
+		return callQosifyCheckDevices().then(function(){
+			return new Promise(function(r){setTimeout(r,800);});
+		}).then(function(){
+			notify(_('Device check done.'),'info');
+			return self.refreshOverview();
+		}).catch(function(e){
+			notify(_('Device check failed: %s').format(e),'danger');
+		}).finally(function(){
+			ui.hideModal();
+			self.unlock();
+		});
+	},
+
+	svcAction:function(action){
+		var self=this;
+		self.lock();
+		ui.showModal(_('Working'),[E('p',{},_('Sending %s to qosify...').format(action))]);
+		var p=callRcInit('qosify',action);
+		if(action==='start'||action==='restart')
+			p=p.then(function(){return self.waitForRunning(4000);}).then(function(up){
+				if(up==null)throw new Error(_('rpcd is not answering for qosify, so the service state is unknown.'));
+				if(!up)throw new Error(_('qosify did not come up — check the system log'));
+				return self.pushConfig();
+			});
+		if(action==='stop')
+			p=p.then(function(){return self.waitForStopped(4000);}).then(function(down){
+				if(down==null)throw new Error(_('rpcd is not answering for qosify, so the service state is unknown.'));
+				if(!down)throw new Error(_('qosify is still running — leaving the qdiscs alone'));
+				return fs.exec('/usr/share/qosify-luci/cleanup',[]).then(function(r){
+					if(r&&r.code)notify(_('Cleanup exited with code %d').format(r.code),'warning');
+				});
+			});
+		return p.then(function(){
+			return new Promise(function(r){setTimeout(r,800);});
+		}).then(function(){
+			return self.refreshOverview();
+		}).catch(function(e){
+			notify(_('Service action failed: %s').format(e),'danger');
+		}).finally(function(){
+			ui.hideModal();
+			self.unlock();
+		});
+	},
+
+	saveQuick:function(){
+		var self=this;
+		var get=function(id){var e=$('q-'+id);return e?e.value:'';};
+		var chk=function(id){var e=$('q-'+id);return e&&e.checked;};
+		var bw=function(s){return trim(s).replace(/\s+/g,'');};
+		var bwUp=bw(get('bw_up')),bwDn=bw(get('bw_down'));
+		var rate=/^(unlimited|\d+(\.\d+)?((k|m|g|t)?(bit|bps)|(ki|mi|gi)(bit|bps))?)$/i;
+		var ovh=get('overhead'),mode=get('mode'),mpu=trim(get('overhead_mpu')),vlan=get('overhead_vlan'),ob=trim(get('ovh_bytes'));
+		var iopts=trim(get('ing_opts')),eopts=trim(get('egr_opts')),gopts=trim(get('opts'));
+		var safe=/^[\w\s.:-]*$/;
+		if(!safe.test(iopts)||!safe.test(eopts)||!safe.test(gopts)||!safe.test(bwUp)||!safe.test(bwDn)){
+			notify(_('Error: invalid characters in bandwidth or options fields. Use alphanumeric, spaces, hyphens, dots, colons only.'),'danger');
+			return;
+		}
+		if(bwUp&&!rate.test(bwUp))notify(_('bandwidth_up does not look like a tc rate (100mbit, 12MBps, unlimited) — passing it through anyway').format(),'warning');
+		if(bwDn&&!rate.test(bwDn))notify(_('bandwidth_down does not look like a tc rate (100mbit, 12MBps, unlimited) — passing it through anyway').format(),'warning');
+		if(mpu&&!/^\d+$/.test(mpu)){notify(_('Error: overhead_mpu must be a whole number of bytes'),'danger');return;}
+		if(ovh==='manual'&&ob&&!/^-?\d+$/.test(ob)){notify(_('Error: overhead must be a whole number of bytes'),'danger');return;}
+		var en=chk('enabled'),s0=ifSect(),bwAll=((s0&&uci.get('qosify',s0.id))||{}).bandwidth;
+		if(en&&!bwAll&&(!bwUp||!bwDn))notify(_('Note: bandwidth not set — CAKE will run unlimited on that direction.'),'warning');
+
+		var sty=s0?s0.type:'interface',sec=s0?s0.name:'wan',sidx=s0?s0.idx:0;
+		// null = remove the option, so clearing a field actually clears it
+		var kv={
+			disabled:en?'0':'1',
+			bandwidth_up:bwUp||null,
+			bandwidth_down:bwDn||null,
+			overhead_type:ovh||null,
+			mode:mode||null,
+			ingress:chk('ingress')?'1':'0',
+			egress:chk('egress')?'1':'0',
+			nat:chk('nat')?'1':'0',
+			host_isolate:chk('host_isolate')?'1':'0',
+			autorate_ingress:chk('autorate')?'1':'0',
+			ingress_options:iopts||null,
+			egress_options:eopts||null,
+			options:gopts||null
+		};
+		// overhead and overhead_encap are dropped unless manual, as qosify ignores them.
+		kv.overhead=(ovh==='manual'&&ob)?ob:null;
+		kv.overhead_encap=(ovh==='manual'&&get('overhead_encap'))?get('overhead_encap'):null;
+		kv.overhead_mpu=mpu||null;
+		kv.overhead_vlan=vlan&&vlan!=='0'?vlan:null;
+		var nmEl=$('q-name');
+		if(nmEl){
+			var nm=trim(nmEl.value);
+			if(nm&&!/^[\w.@:-]+$/.test(nm)){notify(_('Error: name must be a device or interface name'),'danger');return;}
+			kv.name=nm||null;
+		}
+
+		self.lock();
+		ui.showModal(_('Saving'),[E('p',{},_('Saving settings and applying...'))]);
+		return callUciRevert('qosify').then(function(){
+			return Promise.all([fs.read(UCI_PATH),L.resolveDefault(fs.stat(UCI_PATH),null)]);
+		}).then(function(r){
+			var txt=r[0]||'',st=r[1];
+			if(!trim(txt)&&st&&st.size>0)
+				throw new Error(_('%s came back empty although it is %d bytes on disk — refusing to overwrite it').format(UCI_PATH,st.size));
+			return fs.write(UCI_PATH,setOpts(txt,sty,sec,sidx,kv));
+		}).then(function(){
+			uci.unload('qosify');
+			return uci.load('qosify');
+		}).then(function(){
+			return self.applyService();
+		}).then(function(){
+			return self.checkShapingForSave(_('Settings saved'));
+		}).then(function(msg){
+			ui.hideModal();
+			notify(msg.text,msg.kind);
+			self.lintAll().forEach(function(t){notify(t,'warning');});
+			return self.refreshAll();
+		}).catch(function(e){
+			ui.hideModal();
+			notify(_('Save failed: %s').format(e),'danger');
+		}).finally(function(){self.unlock();});
+	},
+
+	confirmFresh:function(el,path){
+		return L.resolveDefault(fs.stat(path),null).then(function(st){
+			if(!fileMoved(el,st))return true;
+			return confirmDialog(_('File changed on disk'),
+				_('%s has changed since this editor was loaded. Saving now discards those changes.').format(path),
+				_('Overwrite'),true);
+		});
+	},
+
+	saveConfig:function(){
+		var self=this;
+		var ta=$('qos-config-ta');
+		if(!ta)return;
+		var data=ta.value.replace(/\r\n/g,'\n');
+		if(data.length===0)return self.clearConfig(ta);
+		if(!/(^|\n)config /.test(data)){
+			notify(_('Error: No valid config stanzas found.'),'danger');return;
+		}
+		var hard=cfgLint(cfgOpts(data)).filter(function(x){return x.hard;});
+		if(hard.length){hard.forEach(function(x){notify(_('Error: %s').format(x.t),'danger');});return;}
+		return self.confirmFresh(ta,UCI_PATH).then(function(go){
+			if(!go)return null;
+			return self.writeConfig(ta,data);
+		});
+	},
+
+	// Truncating the file gets the file-changed check every other write gets, plus
+	// one of its own: when gatherCtx()'s read fails the editor is left empty but
+	// still carries the size and mtime it found on disk, so fileMoved() sees
+	// nothing wrong and confirmFresh() would wave a wipe through. dataset.orig is
+	// what separates "the user emptied it" from "it never loaded".
+	clearConfig:function(ta){
+		var self=this;
+		return L.resolveDefault(fs.stat(UCI_PATH),null).then(function(st){
+			if(st&&st.size>0&&!(ta.dataset.orig||'').length){
+				notify(_('%s is %d bytes on disk but was never loaded into the editor — refusing to truncate it. Reload the page first.').format(UCI_PATH,st.size),'danger');
+				return null;
+			}
+			return self.confirmFresh(ta,UCI_PATH).then(function(go){
+				if(!go)return null;
+				return confirmDialog(_('Clear configuration'),
+					_('An empty %s stops all shaping. Continue?').format(UCI_PATH),_('Write empty file'),true);
+			}).then(function(go){
+				if(!go)return null;
+				var stopped=false;
+				self.lock();
+				return callUciRevert('qosify').then(function(){
+					return fs.write(UCI_PATH,'');
+				}).then(function(){
+					return callRcInit('qosify','stop');
+				}).then(function(){
+					return self.waitForStopped(4000);
+				}).then(function(down){
+					// cleanup deletes the root and clsact qdiscs and the ifb devices, so
+					// it only runs once the daemon is confirmed down -- the same guard
+					// svcAction() applies to a plain stop.
+					stopped=down;
+					if(down==null){notify(_('rpcd is not answering for qosify, so the service state is unknown.'),'warning');return null;}
+					if(!down){notify(_('qosify is still running — leaving the qdiscs alone'),'warning');return null;}
+					return L.resolveDefault(fs.exec('/usr/share/qosify-luci/cleanup',[]),null);
+				}).then(function(){
+					uci.unload('qosify');
+					return uci.load('qosify');
+				}).then(function(){
+					ta.dataset.orig='';
+					notify(stopped?_('Config cleared, qosify stopped.'):_('Config cleared.'),'info');
+					return self.refreshAll('cfg');
+				}).catch(function(e){
+					notify(_('Save failed: %s').format(e),'danger');
+				}).finally(function(){self.unlock();});
+			});
+		});
+	},
+
+	writeConfig:function(ta,data){
+		var self=this;
+		self.lock();
+		ui.showModal(_('Saving'),[E('p',{},_('Writing config and reloading qosify...'))]);
+		return callUciRevert('qosify').then(function(){
+			return fs.write(UCI_PATH,data);
+		}).then(function(){
+			uci.unload('qosify');
+			return uci.load('qosify');
+		}).then(function(){
+			return self.applyService();
+		}).then(function(){
+			return self.checkShapingForSave(_('Config saved'));
+		}).then(function(msg){
+			ta.dataset.orig=data;
+			ui.hideModal();
+			notify(msg.text,msg.kind);
+			self.lintAll().forEach(function(t){notify(t,'warning');});
+			return self.refreshAll('cfg');
+		}).catch(function(e){
+			ui.hideModal();
+			notify(_('Save failed: %s').format(e),'danger');
+		}).finally(function(){self.unlock();});
+	},
+
+	// true shaping, false not shaping, null the status call did not answer. The
+	// qosify object goes with the daemon, so an unanswered call is only unknown
+	// while qosify runs; a stopped qosify is plainly not shaping. A retry costs one
+	// ubus call, so an unanswered one is retried like an idle reply.
+	waitForShaping:function(tries){
+		var self=this;
+		return callQosifyStatus().catch(function(){
+			return callServiceList('qosify').then(function(r){return isRunning(r)?null:{};},function(){return null;});
+		}).then(function(st){
+			var a=st?statusActive(st):null;
+			if(a||tries<=1)return a;
+			return new Promise(function(res){setTimeout(res,700);}).then(function(){return self.waitForShaping(tries-1);});
+		});
+	},
+
+	checkShapingForSave:function(prefix){
+		var sn=ifSect(),w=(sn&&uci.get('qosify',sn.id))||{};
+		if(uciBool(w.disabled,false))return Promise.resolve({text:_('%s, applied (QoS disabled).').format(prefix),kind:'info'});
+		return this.waitForShaping(3).then(function(active){
+			if(active)return {text:_('%s, applied.').format(prefix),kind:'info'};
+			// An unanswered status call is not a report of idle qdiscs: saying "not
+			// shaping" there turns a stale ACL into a false negative on a box that is.
+			if(active==null)return {text:_('%s, applied — qosify did not answer on ubus, so shaping could not be checked.').format(prefix),kind:'warning'};
+			return {text:_('Warning: %s but qosify is not shaping traffic — check the Status tab.').format(prefix),kind:'warning'};
+		});
+	},
+
+	saveRules:function(){
+		var self=this;
+		var ta=$('qos-rules-ta');
+		if(!ta)return;
+		var data=ta.value.replace(/\r\n/g,'\n');
+		var verr=validateRules(data);
+		if(verr){notify(_('Error: %s').format(verr),'danger');return;}
+		var rwarn=ruleWarn(data,self.getClasses().map(function(c){return c.name;}));
+		return self.confirmFresh(ta,RULES_PATH).then(function(go){
+			if(!go)return null;
+			return self.writeRules(ta,data,rwarn);
+		});
+	},
+
+	writeRules:function(ta,data,rwarn){
+		var self=this;
+		self.lock();
+		ui.showModal(_('Saving'),[E('p',{},_('Writing rules and reloading qosify...'))]);
+		return fs.write(RULES_PATH,data).then(function(){
+			return self.applyService();
+		}).then(function(){
+			return self.checkShapingForSave(_('Rules saved'));
+		}).then(function(msg){
+			ta.dataset.orig=data;
+			ui.hideModal();
+			notify(msg.text,msg.kind);
+			rwarn.forEach(function(t){notify(t,'warning');});
+			return self.refreshAll('rules');
+		}).catch(function(e){
+			ui.hideModal();
+			notify(_('Save failed: %s').format(e),'danger');
+		}).finally(function(){self.unlock();});
+	},
+
+	clearCfg:function(){
+		return confirmDialog(_('Clear editor'),_('Empty the config editor? Nothing is written until you click Save & Apply.'),_('Clear')).then(function(go){
+			var ta=$('qos-config-ta');
+			if(go&&ta)ta.value='';
+		});
+	},
+	clearRules:function(){
+		return confirmDialog(_('Clear editor'),_('Empty the rules editor? Nothing is written until you click Save & Apply.'),_('Clear')).then(function(go){
+			var ta=$('qos-rules-ta');
+			if(go&&ta)ta.value='';
+		});
+	},
+
+	uploadFiles:function(){
+		var self=this;
+		var u1=$('qos-up-cfg'),u2=$('qos-up-rules');
+		var f1=u1&&u1.files[0],f2=u2&&u2.files[0];
+		if(!f1&&!f2){notify(_('No files selected.'),'warning');return;}
+		return confirmDialog(_('Overwrite config files'),
+			_('The selected files replace the ones on the router and qosify is reloaded. Download a backup from this tab first if you need one.'),
+			_('Upload and apply'),true).then(function(go){
+			if(!go)return null;
+			return self.doUpload(u1,u2,f1,f2);
+		});
+	},
+
+	doUpload:function(u1,u2,f1,f2){
+		var self=this;
+
+		function readFile(f){
+			return new Promise(function(res,rej){
+				if(f.size<1)return rej(_('Empty file'));
+				if(f.size>65536)return rej(_('File too large (max 64KB)'));
+				var r=new FileReader();
+				r.onload=function(){res(String(r.result).replace(/\r\n/g,'\n'));};
+				r.onerror=function(){rej(_('Read error'));};
+				r.readAsText(f);
+			});
+		}
+		function validateUci(d){
+			if(/\x00/.test(d))return _('Binary content rejected');
+			if(!/(^|\n)config /.test(d))return _('No valid UCI config stanzas');
+			var hard=cfgLint(cfgOpts(d)).filter(function(x){return x.hard;});
+			return hard.length?hard.map(function(x){return x.t;}).join('; '):null;
+		}
+		self.lock();
+		ui.showModal(_('Uploading'),[E('p',{},_('Reading and validating files...'))]);
+		var names=[],errs=[],warns=[];
+		// Sequential on purpose: the uploaded UCI config is written and reloaded
+		// first, so ruleWarn() below sees the uploaded classes, not the old ones.
+		var p=Promise.resolve();
+		if(f1)p=p.then(function(){return readFile(f1).then(function(d){
+			var e=validateUci(d);
+			if(e){errs.push(_('Config: %s').format(e));return null;}
+			return callUciRevert('qosify').then(function(){
+				return fs.write(UCI_PATH,d);
+			}).then(function(){
+				names.push(UCI_PATH);
+				uci.unload('qosify');
+				return uci.load('qosify');
+			});
+		},function(e){errs.push(_('Config: %s').format(e));});});
+		if(f2)p=p.then(function(){return readFile(f2).then(function(d){
+			var e=validateRules(d);
+			if(e){errs.push(_('Rules: %s').format(e));return null;}
+			ruleWarn(d,self.getClasses().map(function(c){return c.name;})).forEach(function(t){warns.push(t);});
+			return fs.write(RULES_PATH,d).then(function(){names.push('00-defaults.conf');});
+		},function(e){errs.push(_('Rules: %s').format(e));});});
+
+		return p.then(function(){
+			if(names.length===0){
+				ui.hideModal();
+				notify(_('Upload error: %s').format(errs.join('; ')),'danger');
+				return;
+			}
+			uci.unload('qosify');
+			return uci.load('qosify').then(function(){
+				return self.applyService();
+			}).then(function(){
+				ui.hideModal();
+				var msg=_('%s uploaded, qosify reloaded.').format(names.join(' & '));
+				if(errs.length)msg+=' '+_('Errors:')+' '+errs.join('; ');
+				notify(msg,errs.length?'warning':'info');
+				warns.forEach(function(t){notify(t,'warning');});
+				[u1,u2].forEach(function(u){if(u){u.value='';u.dispatchEvent(new Event('change'));}});
+				return self.refreshAll();
+			});
+		}).catch(function(e){
+			ui.hideModal();
+			notify(_('Upload failed: %s').format(e),'danger');
+		}).finally(function(){self.unlock();});
+	},
+
+	resetDefaults:function(){
+		var self=this;
+		return confirmDialog(_('Reset to defaults'),
+			_('%s and %s are replaced with the templates shipped by the qosify package, and shaping is left disabled.').format(UCI_PATH,RULES_PATH),
+			_('Reset'),true).then(function(go){
+			if(!go)return null;
+			return self.doReset();
+		});
+	},
+
+	doReset:function(){
+		var self=this;
+		self.lock();
+		ui.showModal(_('Resetting'),[E('p',{},_('Restoring defaults...'))]);
+		return callUciRevert('qosify').then(function(){
+			return Promise.all([
+				fs.read('/usr/share/qosify-luci/qosify'),
+				fs.read('/usr/share/qosify-luci/00-defaults.conf')
+			]);
+		}).then(function(t){
+			return fs.write(UCI_PATH,t[0]).catch(function(e){
+				throw new Error(_('%s was not written: %s').format(UCI_PATH,e));
+			}).then(function(){
+				return fs.write(RULES_PATH,t[1]).catch(function(e){
+					throw new Error(_('%s was reset but %s was not written: %s').format(UCI_PATH,RULES_PATH,e));
+				});
+			});
+		}).then(function(){
+			uci.unload('qosify');
+			return uci.load('qosify');
+		}).then(function(){
+			return self.applyService();
+		}).then(function(){
+			ui.hideModal();
+			notify(_('Reset to defaults, applied.'),'info');
+			return self.refreshAll();
+		}).catch(function(e){
+			ui.hideModal();
+			notify(_('Reset failed: %s').format(e),'danger');
+		}).finally(function(){self.unlock();});
+	},
+
+	// === Quick Add handlers ===
+
+	qarPlaceholder:function(){
+		var t=$('qar-type').value;
+		var v=$('qar-val');
+		var eg=function(x){return _('e.g. %s').format(x);};
+		var ph={'tcp:':eg('4500'),'udp:':eg('4500'),'both:':eg('5060-5061'),
+			'dns:':eg('*teams*'),'dnsr:':eg('zoom[0-9]+\\.us'),'dns_c:':eg('*cdn*'),'dns_cr:':eg('cdn[0-9]+'),
+			'ipv4:':eg('1.1.1.1'),'ipv6:':eg('ff01::1')};
+		v.placeholder=ph[t]||'';
+	},
+
+	qarAdd:function(){
+		var ty=$('qar-type').value;
+		var val=trim($('qar-val').value);
+		var cls=$('qar-cls').value;
+		var pr=$('qar-prio').checked;
+		if(!val){notify(_('Enter a value.'),'danger');return;}
+		if(!cls){notify(_('No classes defined. Add classes in the Config tab first.'),'danger');return;}
+		var pt=(ty==='tcp:'||ty==='udp:'||ty==='both:');
+		if(pt){
+			var pp=val.split('-'),pn=[],j,n;
+			if(pp.length>2){notify(_('Port must be a number or a range (4500, 5060-5061).'),'danger');return;}
+			for(j=0;j65534){notify(_('Port must be 1-65534 (qosify rejects 65535).'),'danger');return;}
+				pn.push(n);
+			}
+			if(pn.length===2&&pn[0]>pn[1]){notify(_('Range start must not exceed end.'),'danger');return;}
+		}else if(/[\s#]/.test(val)){notify(_('No spaces or # allowed in patterns or addresses.'),'danger');return;}
+		if(ty==='ipv4:'){
+			if(!ip4Ok(val)){notify(_('Enter a single IPv4 address (qosify does not accept CIDR).'),'danger');return;}
+		}
+		// inet_pton(AF_INET6) also takes the IPv4-mapped form, so allow dots here
+		if(ty==='ipv6:'&&!ip6Ok(val)){notify(_('Enter a single IPv6 address (qosify does not accept CIDR or a %zone suffix).'),'danger');return;}
+		var pfx=pr?'+':'';
+		var ta=$('qos-rules-ta');if(!ta)return;
+		var lines=[];
+		if(ty==='both:'){lines.push('tcp:'+val+'\t'+pfx+cls);lines.push('udp:'+val+'\t'+pfx+cls);}
+		else if(ty==='ipv4:'||ty==='ipv6:')lines.push(val+'\t'+pfx+cls);
+		else if(ty==='dnsr:')lines.push('dns:/'+val+'\t'+pfx+cls);
+		else if(ty==='dns_cr:')lines.push('dns_c:/'+val+'\t'+pfx+cls);
+		else lines.push(ty+val+'\t'+pfx+cls);
+		var v=ta.value.replace(/\s+$/,'');
+		ta.value=v+(v?'\n\n':'')+lines.join('\n')+'\n';
+		$('qar-val').value='';
+		$('qar-prio').checked=false;
+		ta.scrollTop=ta.scrollHeight;
+	},
+
+
+	qacAdd:function(p){
+		var tsel=$('qac-'+p+'-type'),nmEl=$('qac-'+p+'-name'),ty=tsel?tsel.value:p;
+		var ta=$('qos-config-ta');if(!ta)return;
+		var nm='',secs=cfgSections(ta.value);
+		if(ty!=='defaults'){
+			nm=trim(nmEl.value);
+			if(!nm){notify(_('Enter a section name.'),'danger');return;}
+			if(!/^[a-zA-Z0-9_]+$/.test(nm)){notify(_('A section name may only contain letters, digits and underscores.'),'danger');return;}
+		}
+		if(ty==='defaults'&&secs.some(function(x){return x.type==='defaults';})){notify(_('A config defaults section already exists.'),'danger');return;}
+		if(nm&&secs.some(function(x){return x.type===ty&&x.name===nm;})){notify(_('Section %s already exists.').format(nm),'danger');return;}
+		var s='config '+ty+(nm?" '"+nm+"'":'');
+		var div=$('qac-opts-'+p);
+		var els=div.querySelectorAll('[data-opt]');
+		for(var i=0;i10&&/(^|\n)config /.test(ctx.cfgRaw||'');
+				if(ctx.cfgRaw===null)notify(_('%s could not be read — the editor is left empty and will not be saved over it.').format(UCI_PATH),'danger');
+			}
+			ctx.rulesN=self._rulesN;
+			ctx.cfgOk=self._cfgOk;
+			return self.uptime(d[0]).then(function(u){ctx.uptime=u;return ctx;});
+		});
+	},
+
+	// Seconds since the running qosify started, or null. procd's service list
+	// carries the pid but no start time, so starttime (field 22 of /proc//stat,
+	// USER_HZ ticks since boot) is set against /proc/uptime, both on the boot
+	// clock. A reload keeps the pid, so the result is cached per pid and later
+	// ticks advance it from performance.now(), which is monotonic: an NTP step or
+	// a date change does not skew it, and nothing is read until qosify restarts.
+	uptime:function(r){
+		var self=this,pid=runPid(r);
+		if(!pid){self._up=null;return Promise.resolve(null);}
+		if(self._up&&self._up.pid===pid)return Promise.resolve(self._up.up+(performance.now()-self._up.t)/1000);
+		return Promise.all([fs.read('/proc/'+pid+'/stat'),fs.read('/proc/uptime')]).then(function(d){
+			var st=String(d[0]),f=st.slice(st.lastIndexOf(')')+2).split(' '),up=parseFloat(d[1])-f[19]/100;
+			if(!(up>=0))return null;
+			self._up={pid:pid,up:up,t:performance.now()};
+			return up;
+		}).catch(function(){return null;});
+	},
+
+	// Poll path: six ubus calls (uci.get and gatherCtx(false)'s five), no shell
+	// forks, and the parts of the page that hold user input or focus are patched
+	// in place rather than rebuilt.
+	refreshOverview:function(){
+		var self=this;
+		self.lock();
+		uci.unload('qosify');
+		return uci.load('qosify').then(function(){
+			return self.gatherCtx(false);
+		}).then(function(ctx){
+			self.updateSvcTable(ctx);
+			self.fillSect('qos-cfg-sect',self.buildCfgSect(ctx));
+			var bd=$('q-en-badge');
+			if(bd){
+				var sn=ifSect(),w=(sn&&uci.get('qosify',sn.id))||{};
+				self.updateEnBadge(bd,ctx,w['.name']!=null&&!uciBool(w.disabled,false));
+			}
+			self.updateFiles(ctx);
+			return ctx;
+		}).finally(function(){self.unlock();});
+	},
+
+	refreshOverviewFull:function(){
+		var self=this;
+		return self.refreshOverview().then(function(ctx){
+			self.fillSect('qos-svc-sect',self.buildSvcSect(ctx));
+			self.fillSect('qos-qs-sect',self.buildQsSect(ctx));
+			return ctx;
+		});
+	},
+
+	refreshStatus:function(){
+		var self=this;
+		if(self.currentTab!=='st'||self._st)return Promise.resolve();
+		self._st=true;
+		var ex=self.readonly?Promise.resolve(null):L.resolveDefault(fs.exec('/usr/sbin/qosify-status',[]),null);
+		return callServiceList('qosify').catch(function(){return null;}).then(function(d){
+			var ctx={running:d?isRunning(d):null,qstatus:self.readonly?'':null};
+			var stb=$('qos-st-body');
+			if(stb)self.fillStatus(stb,ctx);
+			return ex.then(function(r){
+				ctx.qstatus=self.readonly?'':((r&&r.stdout)||'');
+				if(stb)self.fillStatus(stb,ctx);
+			});
+		}).finally(function(){self._st=false;});
+	},
+
+	// which = 'cfg' | 'rules' | undefined: the editor for the file just written is
+	// reloaded, the other one keeps whatever the user has typed.
+	refreshAll:function(which){
+		var self=this;
+		return self.refreshOverviewFull().then(function(){
+			self.refreshClasses();
+			return Promise.all([
+				self.reloadEditor('qos-config-ta',UCI_PATH,which==='cfg'),
+				self.reloadEditor('qos-rules-ta',RULES_PATH,which==='rules')
+			]);
+		});
+	},
+
+	reloadEditor:function(id,path,force){
+		var el=$(id);
+		if(!el)return Promise.resolve();
+		return Promise.all([
+			L.resolveDefault(fs.read(path),null),
+			L.resolveDefault(fs.stat(path),null)
+		]).then(function(r){
+			var disk=r[0];
+			if(disk==null)return;
+			var dirty=(el.dataset.orig!=null&&el.value!==el.dataset.orig);
+			if(dirty&&!force&&el.value!==disk){
+				notify(_('%s changed on disk — your unsaved edits are still in the editor.').format(path),'warning');
+				return;
+			}
+			el.value=disk;
+			el.dataset.orig=disk;
+			stampFile(el,r[1]);
+		});
+	}
+});
diff --git a/luci-app-qosify/htdocs/luci-static/resources/view/qosify/qosify.css b/luci-app-qosify/htdocs/luci-static/resources/view/qosify/qosify.css
new file mode 100644
index 000000000..ff7a0fbb5
--- /dev/null
+++ b/luci-app-qosify/htdocs/luci-static/resources/view/qosify/qosify.css
@@ -0,0 +1,398 @@
+/* SPDX-License-Identifier: MIT */
+
+/* The page is stock LuCI markup and each theme draws .cbi-section its own way,
+   so sections are left alone. Theme variables come first here, with a neutral
+   fallback for themes without them. */
+#qos-app {
+	--qos-bd: var(--border-color-medium, rgba(128, 128, 128, .35));
+	--qos-ln: var(--border-color-low, rgba(128, 128, 128, .2));
+	--qos-hd: var(--background-color-low, rgba(128, 128, 128, .06));
+	--qos-r: 4px;
+	--qos-gap: .75em;
+	--qos-pad: .45em 1em;
+}
+
+/* The service bar sits outside any section, so it gets its own box. Flex
+   items ignore float, so the theme's page-actions floats cannot reorder the
+   buttons and they stay in declared order whatever their colour class. */
+#qos-svc-btns {
+	display: flex;
+	flex-wrap: wrap;
+	justify-content: flex-end;
+	gap: .4em;
+	margin: 0 0 var(--qos-gap);
+	padding: var(--qos-pad);
+	border: 1px solid var(--qos-bd);
+	border-radius: var(--qos-r);
+}
+
+#qos-svc-btns::after {
+	content: none;
+}
+
+#qos-svc-btns > * {
+	margin: 0;
+}
+
+/* Bootstrap and OpenWrt 2020 define .label.warning and .label.success but no
+   .label.danger. */
+#qos-app .label.danger {
+	background-color: var(--error-color-high, #e8210d);
+	color: var(--on-error-color, #fff);
+}
+
+#qos-app .table {
+	margin: 0;
+}
+
+#qos-app .table .td {
+	padding: .45em .75em;
+	vertical-align: middle;
+	border-top-color: var(--qos-ln);
+}
+
+/* A folding section is a details.cbi-section whose summary holds the theme's h3. */
+#qos-app .cbi-section > summary {
+	margin: 0 0 var(--qos-gap);
+	cursor: pointer;
+	list-style: none;
+}
+
+#qos-app .cbi-section > summary::-webkit-details-marker {
+	display: none;
+}
+
+#qos-app .cbi-section > summary::before {
+	content: "\25B8";
+	display: inline-block;
+	width: 1.1em;
+	transition: transform .15s;
+}
+
+#qos-app details.cbi-section[open] > summary::before {
+	transform: rotate(90deg);
+}
+
+#qos-app details.cbi-section:not([open]) > summary {
+	margin-bottom: 0;
+}
+
+#qos-app summary > h3 {
+	display: inline;
+	margin: 0;
+}
+
+/* Reference panels inside a section. */
+#qos-app details:not(.cbi-section) {
+	margin: var(--qos-gap) 0 0;
+}
+
+#qos-app details:not(.cbi-section) > summary {
+	cursor: pointer;
+	font-weight: 600;
+}
+
+#qos-app details:not(.cbi-section) > p,
+#qos-app details:not(.cbi-section) > .table {
+	margin: .5em 0 0;
+}
+
+/* Quick Add: forms, so the grids stay tight; the folds are ordinary sections. */
+#qos-app .qa .table {
+	table-layout: fixed;
+	margin: 0 0 .3em;
+}
+
+#qos-app .qa .th,
+#qos-app .qa .td {
+	padding: .15em .3em;
+}
+
+#qos-app .qa .th {
+	font-size: .85em;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+#qos-app .qa .td input:not([type=checkbox]),
+#qos-app .qa .td select {
+	width: 100%;
+	min-width: 0;
+	box-sizing: border-box;
+}
+
+#qos-app .qa .td input[type=checkbox] {
+	margin: 0;
+	vertical-align: middle;
+}
+
+#qos-app .qa-foot {
+	display: flex;
+	align-items: flex-start;
+	gap: .3em 1em;
+}
+
+#qos-app .qa-foot > details {
+	flex: 1 1 0;
+	min-width: 0;
+	margin: .15em 0 0;
+}
+
+#qos-app .qa details:not(.cbi-section) > summary {
+	font-size: .9em;
+}
+
+#qos-app .qa details:not(.cbi-section) .td {
+	padding: .2em .5em;
+	border-top: 1px solid var(--qos-ln);
+}
+
+#qos-app .cbi-section-node > .cbi-value:last-child {
+	margin-bottom: 0;
+}
+
+#qos-app .qos-up-name {
+	margin-left: .5em;
+	opacity: .75;
+}
+
+/* The tc output takes the rest of the window: the viewport less the LuCI
+   header and the tab bar, caught by min-height on a short screen. The editors
+   are fitted to the window by fitEditor(); the calc() height only holds until
+   the first fit. Width is on the id so the theme's textarea width cannot win,
+   and resize: both keeps a wrong width correctable. */
+#qos-st-pre {
+	margin: 0;
+	padding: .75em;
+	overflow: auto;
+	white-space: pre;
+	height: calc(100vh - 310px);
+	min-height: 320px;
+	resize: vertical;
+	font-family: var(--font-mono, monospace);
+	font-size: 12px;
+	border: 1px solid var(--qos-bd);
+	border-radius: var(--qos-r);
+}
+
+#qos-config-ta,
+#qos-rules-ta {
+	width: 100%;
+	box-sizing: border-box;
+	height: calc(100vh - 300px);
+	min-height: 160px;
+	resize: both;
+	padding: 6px;
+	line-height: 1.4;
+	tab-size: 4;
+	font-family: var(--font-mono, monospace);
+	font-size: 12px;
+}
+
+/* Counters. Tin colours: bulk, best effort, video and voice take the theme's
+   error, primary, warn and success colours; the extra diffserv8 and precedence
+   tins use fallbacks with light and dark values, switched by LuCI's
+   data-darkmode or, on a theme that does not set it, prefers-color-scheme. */
+#qos-app {
+	--qos-f-red: #e41a1c; --qos-f-blue: #377eb8; --qos-f-yellow: #e6b422;
+	--qos-f-green: #4daf4a; --qos-f-purple: #984ea3; --qos-f-orange: #ff7f00;
+	--qos-f-cyan: #17becf; --qos-f-forest: #1b7837; --qos-f-pine: #0f4d24;
+	--qos-f-grey: #5c5c5c; --qos-f-brown: #a65628; --qos-f-pink: #f781bf;
+	--qos-f-none: rgba(128, 128, 128, .45);
+}
+
+:root[data-darkmode="true"] #qos-app {
+	--qos-f-red: #e57373; --qos-f-blue: #64a0d8; --qos-f-yellow: #d4b44a;
+	--qos-f-green: #66bb6a; --qos-f-purple: #b085c0; --qos-f-orange: #ffa24d;
+	--qos-f-cyan: #4dd0e1; --qos-f-forest: #43a047; --qos-f-pine: #2e7d4f;
+	--qos-f-grey: #9e9e9e; --qos-f-brown: #c08050; --qos-f-pink: #f4a6cf;
+	--qos-f-none: rgba(160, 160, 160, .45);
+}
+
+@media (prefers-color-scheme: dark) {
+	:root:not([data-darkmode]) #qos-app {
+		--qos-f-red: #e57373; --qos-f-blue: #64a0d8; --qos-f-yellow: #d4b44a;
+		--qos-f-green: #66bb6a; --qos-f-purple: #b085c0; --qos-f-orange: #ffa24d;
+		--qos-f-cyan: #4dd0e1; --qos-f-forest: #43a047; --qos-f-pine: #2e7d4f;
+		--qos-f-grey: #9e9e9e; --qos-f-brown: #c08050; --qos-f-pink: #f4a6cf;
+		--qos-f-none: rgba(160, 160, 160, .45);
+	}
+}
+
+#qos-app {
+	--qos-c-red: var(--error-color-high, var(--qos-f-red));
+	--qos-c-blue: var(--primary-color-high, var(--qos-f-blue));
+	--qos-c-yellow: var(--warn-color-high, var(--qos-f-yellow));
+	--qos-c-green: var(--success-color-high, var(--qos-f-green));
+	--qos-c-purple: var(--qos-f-purple); --qos-c-orange: var(--qos-f-orange);
+	--qos-c-cyan: var(--qos-f-cyan); --qos-c-forest: var(--qos-f-forest);
+	--qos-c-pine: var(--qos-f-pine); --qos-c-grey: var(--qos-f-grey);
+	--qos-c-brown: var(--qos-f-brown); --qos-c-pink: var(--qos-f-pink);
+	--qos-c-none: var(--qos-f-none);
+}
+
+/* A header table above a boxed body; bars and figures are updated in place. */
+#qos-cn .qbox {
+	overflow: hidden;
+	border: 1px solid var(--qos-bd);
+	border-radius: var(--qos-r);
+}
+
+#qos-cn .qbox + .qbox {
+	margin-top: var(--qos-gap);
+}
+
+#qos-cn .qhead {
+	background: var(--qos-hd);
+	border-bottom: 1px solid var(--qos-ln);
+}
+
+#qos-cn .qhead .th,
+#qos-cn .qbox .td {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+#qos-cn .qbox .tr:not(.table-titles):hover .td {
+	background: var(--qos-hd);
+}
+
+#qos-cn .cbi-progressbar {
+	height: .75em;
+	margin: 0;
+	min-width: 0;
+	border-radius: var(--qos-r);
+}
+
+#qos-cn .qn {
+	width: 1%;
+	text-align: right;
+	white-space: nowrap;
+	font-variant-numeric: tabular-nums;
+}
+
+#qos-cn .qt .td {
+	font-weight: 600;
+}
+
+#qos-cn-map-box {
+	height: 24rem;
+	min-height: 6rem;
+	overflow-y: scroll;
+	resize: vertical;
+}
+
+/* Quick settings: one inner box per tab, rows split by the same line as the
+   tables, and the control then its hint on one line so every hint starts in
+   the same column. */
+#qos-app .qs-box {
+	min-width: 0;
+	min-height: 17em;
+	padding: .7em 1.5em;
+	border: 1px solid var(--qos-bd);
+	border-radius: var(--qos-r);
+}
+
+#qos-qs-sect .cbi-value {
+	align-items: flex-start;
+	padding: .55em 0;
+}
+
+#qos-qs-sect .cbi-value + .cbi-value {
+	border-top: 1px solid var(--qos-ln);
+}
+
+#qos-qs-sect .cbi-value label.cbi-value-title {
+	flex: 0 0 16em;
+	padding-top: 0;
+	line-height: 30px;
+	font-weight: 600;
+}
+
+#qos-qs-sect .cbi-value-field {
+	display: grid;
+	grid-template-columns: minmax(0, 18em) minmax(0, 1fr);
+	align-items: center;
+	gap: .3em 1.5em;
+	flex: 1 1 auto;
+	margin-left: 1.5em;
+	min-width: 0;
+	line-height: 30px;
+}
+
+#qos-qs-sect .cbi-value-description {
+	margin-top: 0;
+	line-height: 1.4;
+}
+
+#qos-qs-sect .qs-note {
+	grid-column: 1 / -1;
+}
+
+#qos-qs-sect .cbi-value-field input[type=checkbox] {
+	justify-self: start;
+	margin: 0;
+	vertical-align: middle;
+}
+
+#qos-qs-sect .qs-ctl {
+	justify-self: start;
+}
+
+#qos-qs-sect .cbi-value-field input[type=text],
+#qos-qs-sect .cbi-value-field input[type=number],
+#qos-qs-sect .cbi-value-field select {
+	max-width: 100%;
+	box-sizing: border-box;
+}
+
+/* Sized by what goes in: a byte count gets a byte-sized box, CAKE options get the row. */
+#qos-qs-sect [data-q=ovh_bytes],
+#qos-qs-sect [data-q=overhead_mpu] {
+	width: 7em;
+}
+
+#qos-qs-sect [data-q=overhead_vlan],
+#qos-qs-sect [data-q=overhead_encap] {
+	width: 9em;
+}
+
+#qos-qs-sect [data-q=name],
+#qos-qs-sect [data-q=bw_up],
+#qos-qs-sect [data-q=bw_down] {
+	width: 14em;
+}
+
+#qos-qs-sect [data-q=mode],
+#qos-qs-sect [data-q=overhead] {
+	width: 18em;
+}
+
+#qos-qs-sect .qs-wide .cbi-value-field {
+	grid-template-columns: minmax(0, 28em) minmax(0, 1fr);
+}
+
+#qos-qs-sect [data-q$=opts] {
+	width: 100%;
+}
+
+/* Phones: label, then control, then hint, one under the other. */
+@media (max-width: 600px) {
+	#qos-qs-sect .cbi-value {
+		display: block;
+	}
+
+	#qos-qs-sect .cbi-value label.cbi-value-title {
+		line-height: 1.5;
+	}
+
+	#qos-qs-sect .cbi-value-field {
+		grid-template-columns: minmax(0, 1fr);
+		margin-left: 0;
+	}
+
+	#qos-qs-sect .cbi-value-field > * {
+		grid-column: 1;
+	}
+}
diff --git a/luci-app-qosify/po/cs/qosify.po b/luci-app-qosify/po/cs/qosify.po
new file mode 100644
index 000000000..8da351201
--- /dev/null
+++ b/luci-app-qosify/po/cs/qosify.po
@@ -0,0 +1,768 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: cs\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Pavel Borecki \n"
+"Language-Team: Czech \n"
+"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n"
+"PO-Revision-Date: 2026-08-22 12:51+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr "%s obsahuje uvozovku – qosify odmítne celou hodnotu"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr "%s nahráno – qosify znovunačteno."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr "%s, uplatněno (QoS zakázáno)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr "%s, uplatněno."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr "(alternativní název)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr "(sekce bez názvu)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr "Sekce výchozích nastavení už existuje."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "Aktivní"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "Přidat"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "Pokročilé"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr "Automatická šířka pásma vstupu"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr "Automatické spouštění"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "K dispozici"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr "Třídy k dispozici"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr "Zazálohovat stávající soubory"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr "Šířka pásma stahování"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr "Šířka pásma odesílání"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr "Binární obsah odmítnut"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr "Klasifikační pravidla"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "Vyčistit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr "Vyčistit editor nastavení? Obsah nebude uložen do kliknutí na Uložit."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+"Vyčistit editor pravidel? Obsah nebude uložen dokud nekliknete na Uložit."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr "Kliknutím zakážete automatické spouštění"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr "Kliknutím povolíte automatické spouštění"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr "Obecná nastavení omezování rychlosti – zapsaná přímo do %s, sekce %s."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "Nastavení"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr "Reference nastavení"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr "Nastavení vyčištěno, qosify zastaveno."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr "Nastavení uloženo"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr "Nastavení: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr "Soubory s nastaveními"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+"DSCP kódové body: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Jakékoli dscp_* "
+"hodnoty mohou také jmenovat třídu. Pro přebití pouze když kolonka DSCP je "
+"nula předešlete znakem +"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr "Pravidla DSCP mapování načtená qosify při spouštění."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+"Výchozí qosify je uplatňováno když klíč chybí – rozhraní: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. zařízení: "
+"identické s výjimkou nat0. výchozí: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (zakázáno)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "Zakázáno"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "Stáhnout"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr "Stáhnout si stávající soubory s nastavením před provedením změn."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+"Editory znovunačteny z disku – neuložené změny v editoru byly zahozeny."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr "Výstup"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr "Předvolby výstupu"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr "Vyprázdnění nastavení zastaví qosify. Pokračovat?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr "Vyprázdnit soubor"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "Povoleno"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr "Povoleno – nespuštěné"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr "Povoleno – neomezující provoz (zkontrolujte nastavení)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr "Zadejte název sekce (písmena a číslice / podtržítko)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr "Zadejte jedinou IPv4 adresu (qosify nepřijímá CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr "Zadejte jedinou IPv6 adresu (qosify nepřijímá CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr "Zadejte hodnotu."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr "Chyba: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr "Chyba: nenalezeny žádné platné sloky (stanza)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr "Chyba: je třeba, aby bandwith_down vypadalo jako např. 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr "Chyba: je třeba, aby bandwith_up vypadalo jako např. 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+"Chyba: neplatné znaky v kolonkách předvoleb. Použijte pouze písmena a "
+"číslice, mezery, pomlčky, tečky a středníky."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr "Chyba: je třeba, aby název bylo zařízení nebo (název) rozhraní"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr "Chyba: je třeba, aby režie bylo celé číslo bajtů"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr "Chyby:"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr "Soubor příliš velký (nejvýše 64KB)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr "Nalezeno (prázdné nebo neplatné)"
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr "Udělit přístup k LuCI aplikaci qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr "Izolovat hostitele"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr "IPv4 adresa"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr "IPv6 adresa"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr "Vstup"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr "Předvolby vstupu"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr "Vstup: %s / výstup: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr "Inicializační skript"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr "Nainstalováno"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr "Název rozhraní"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr "Neplatný řádek pravidla: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr "Chybí"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr "Název netdev"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr "V /etc/config/qosify nedefinovány žádné třídy"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr "Nedefinovány žádné třídy. Nejprve na kartě Nastavení nějaké přidejte."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr "Nedefinována žádná sekce výchozího nastavení"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr "Nevybrány žádné soubory."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr "Ve vzorech nebo adresách nejsou dovolené mezery nebo znaky #."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr "Žádné platné sloky (stanza) UCI nastavení"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr "Nespuštěné"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr "Nenainstalované"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+"Pozn.: šířka pásma nenastavena – CAKE bude v takovém směru provozováno "
+"neomezené."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr "Předvolby"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr "Bajtů režie"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr "Typ režie"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "Přehled"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr "Balíček"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr "Je třeba, aby port byl 1-65534 (qosify odmítne 65535)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr "Je třeba, aby port bylo číslo nebo rozsah (např. 4500 nebo 5060-5061)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+"Předešlete znakem + pro přebití pouze když je kolonka DSCP nula. Porty: "
+"tcp:443, udp:3074, rozsahy: tcp:5060-5061 (1-65534). DNS: dns:*teams*, "
+"regex: dns:/zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr "QoS povoleno"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr "Režim fronty"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr "Rychlé přidání nastavení"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr "Rychlé přidání pravidla"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr "Rychlá nastavení"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr "Je třeba, aby začátek rozsahu nebyl vyšší než jeho konec."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr "Chyba čtení"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr "Načítání a ověřování souborů…"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr "Znovunačíst"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+"Nahradí oba soubory s nastaveními výchozími hodnotami pro qosify – a to bude "
+"zakázáno."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr "Resetování se nezdařilo: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr "Resetovat nastavení qosify na výchozí hodnoty?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr "Resetovat na výchozí"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr "Reset na výchozí – uplatněn."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr "Resetovat na výchozí pro qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr "Resetování"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "Restart"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr "Obnovování výchozích…"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr "Pravidla uložena"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr "Pravidla: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "Spuštěné"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr "Spuštěné a provoz omezující"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr "Spuštěné – provoz neomezující"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr "Stejné předvolby jako třída – dejte existující třídě druhý název."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+"Stejné předvolby jako rozhraní, ale název je netdev. nat v tomto případě má "
+"jako výchozí 0 a 1 je pro rozhraní."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr "Uložit a použít"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr "Uložení se nezdařilo: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr "Ukládání"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr "Ukládání nastavení a uplatňování…"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr "Sekce %s už existuje."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+"Název sekce je názvem třídy, na kterou se pravidla a dscp_* hodnoty "
+"odkazují. Hodnota nastavuje vstup a výstup dohromady."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+"Vyberte soubory a klikněte na „Uložit a použít“ pro přebití a restart qosify."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr "Odesílání %s do qosify…"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr "Ovládání služby"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "Stav služby"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr "Akce služby se nezdařila: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr "Nastavení uložena"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "Spustit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "Stav"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "Zastavit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr "Omezování provozu a DSCP rozřazování prostřednictvím qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr "UCI nastavení – třídy, rozhraní, výchozí."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr "Neznámá třída / DSCP cíl: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr "Nahrát soubory s nastavením"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr "Nahrát a přepsat soubory s nastavením? qosify bude znovunačteno."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr "Chyba nahrávání: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr "Nahrání se nezdařilo: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr "Nahrávání"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr "Platné"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr "Varování: %s ale qosify neomezuje provoz – podívejte se na kartu Stav."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr "Pracující"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr "Zapisování nastavení a načítání qosify…"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr "Zapisování pravidel a znovunačítání qosify…"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr "Máte neuložené změny. Opustit tuto kartu?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr "dns vzor"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr "dns regulární výraz"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr "dns_c vzor"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr "dns_c regulární výraz"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr "vstup a výstup je obojí 0 – nic není omezováno"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+"název není nastavený – qosify.init odešle prázdný název zařízení a tato "
+"sekce nikdy nebude uplatněna"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+"název je netifd rozhraní. Šířka pásma je uplatňována pouze kde jsou "
+"odnastavené bandwidth_up/bandwidth_down. overhead a overhead_encap je "
+"uplatňováno pouze pokud overhead_type je manual."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+"nat není odeslán: qosify pouze vyšle nat/nonat uvnitř větve host_isolate. "
+"CAKE nepřijímá toky plus nat – pro jeho uplatnění dejte do předvoleb nat"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+"nat dosáhne pouze %s – dejte ho do předvoleb, nebo jak do ingress_options, "
+"tak do egress_options"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr "žádné bandwidth_down nebo bandwidth – vstupní CAKE běží neomezované"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr "žádné bandwidth_down nebo bandwidth – výstupní CAKE běží neomezované"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr "pouze pokud odnastaveno (+)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+"overhead a overhead_encap jsou ignorovány pokud overhead_type je manual"
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr "qosify není spuštěné. Napravte to z karty Přehled."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+"qosify toto pouze předá CAKE společně s Host Isolate – pro vynucení nat ho "
+"přidejte do Předvoleb"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr "qosify-status"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr "qosify-status nevrátilo žádný výstup."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr "vyžadováno – qosify přeskočí sekce, které nemají žádný název"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr "pravidla"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr "tcp port"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr "tcp+udp port"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr "udp port"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr "použito pouze pokud Overhead Type je manual"
diff --git a/luci-app-qosify/po/de/qosify.po b/luci-app-qosify/po/de/qosify.po
new file mode 100644
index 000000000..be589f41d
--- /dev/null
+++ b/luci-app-qosify/po/de/qosify.po
@@ -0,0 +1,741 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: de\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Pavel Borecki \n"
+"Language-Team: German \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"PO-Revision-Date: 2026-09-24 06:06+0000\n"
+"X-Generator: Weblate 2026.10.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "Aktiv"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "Hinzufügen"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "Verfügbar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "Einstellungen"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+"DSCP-Codepunkte: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Jeder dscp_*-Wert "
+"kann auch eine Klasse bezeichnen. Durch Voranstellen eines „+“ wird die "
+"Überschreibung nur dann wirksam, wenn das DSCP-Feld den Wert Null hat."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+"Standardwerte, die qosify anwendet, wenn ein Schlüssel fehlt – "
+"Schnittstelle: Modus diffserv4, Eingangsbandbreite 1, Ausgangsbandbreite 1, "
+"NAT 1, Host-Isolation 1, Autorate_Ingress 0. Gerät: identisch, außer NAT 0. "
+"Standardwerte: Timeout 3600, dscp_default_tcp/udp CS0, dscp_prio/dscp_bulk/"
+"dscp_icmp nicht gesetzt, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (deaktiviert)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "Deaktiviert"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "Aktiviert"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr "Ingress"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "Übersicht"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "Status"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "Stopp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr ""
diff --git a/luci-app-qosify/po/es/qosify.po b/luci-app-qosify/po/es/qosify.po
new file mode 100644
index 000000000..2880a4ca1
--- /dev/null
+++ b/luci-app-qosify/po/es/qosify.po
@@ -0,0 +1,791 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: es\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Franco Castillo \n"
+"Language-Team: Spanish \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"PO-Revision-Date: 2026-09-23 19:06+0000\n"
+"X-Generator: Weblate 2026.10.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr "%s contiene una comilla — qosify rechaza todo el valor"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr "%s subido, qosify recargado."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr "%s, aplicado (QoS desactivado)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr "%s, aplicado."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr "(alias)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr "(sección sin nombre)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr "Ya existe una sección de configuración predeterminada."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "Activo"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "Añadir"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "Avanzado"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr "Entrada de tasa automática"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr "Inicio automático"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "Disponible"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr "Clases disponibles"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr "Respaldar los archivos actuales"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr "Ancho de banda de descarga"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr "Ancho de banda de carga"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr "Contenido binario rechazado"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr "Reglas de clasificación"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "Limpiar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+"¿Vaciar el editor de configuración? El contenido no se guardará hasta que "
+"haga clic en Guardar."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+"¿Vaciar el editor de reglas? El contenido no se guardará hasta que haga clic "
+"en Guardar."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr "Haga clic para desactivar el inicio automático"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr "Haga clic para activar el inicio automático"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr "Configuración de forma común — escrita directamente en %s, sección %s."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "Configuración"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr "Referencia de configuración"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr "Config borrada, qosify detenido."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr "Configuración guardada"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr "Configuración: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr "Archivos de configuración"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+"Puntos de código DSCP: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Cualquier "
+"valor dscp_* también puede nombrar una clase. Anteponga + para anular la "
+"configuración solo cuando el campo DSCP sea cero."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr "Reglas de mapeo DSCP cargadas por qosify al iniciar el sistema."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+"Se aplica qosify por defecto cuando falta una clave — interfaz: modo "
+"diffserv4, ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. "
+"Dispositivo: idéntico excepto nat 0. Valores predeterminados: timeout 3600, "
+"dscp_default_tcp/udp CS0, dscp_prio/dscp_bulk/dscp_icmp no configurado, "
+"bulk_trigger_pps/bulk_trigger_timeout/prio_max_avg_pkt_len 0 (desactivado)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "Desactivado"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "Descargar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr ""
+"Descargue los archivos de configuración actuales antes de realizar cambios."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+"Los editores se recargaron desde el disco — los cambios no guardados se "
+"descartaron."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr "Salida"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr "Opciones de salida"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr "Una configuración vacía detendrá qosify. ¿Continuar?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr "Archivo vacío"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "Activado"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr "Activado — No se está ejecutando"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr "Activado — No se está modelando (verificar configuración)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr "Introduzca un nombre para la sección (alfanumérico/guión bajo)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr "Introduzca una única dirección IPv4 (qosify no acepta CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr "Introduzca una única dirección IPv6 (qosify no acepta CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr "Introduzca un valor."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr "Error: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr "Error: No se encontraron secciones de configuración válidas."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr "Error: bandwidth_down debe tener el formato 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr "Error: bandwidth_up debe tener el formato 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+"Error: caracteres no válidos en los campos de opciones. Utilice únicamente "
+"caracteres alfanuméricos, espacios, guiones, puntos y dos puntos."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr "Error: el nombre debe ser un nombre de dispositivo o interfaz"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr "Error: la sobrecarga debe ser un número entero de bytes"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr "Errores:"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr "El archivo es demasiado grande (máximo 64 KB)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr "Encontrado (vacío o no válido)"
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr "Otorgar acceso a la aplicación LuCI qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr "Aislamiento del host"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr "Dirección IPv4"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr "Dirección IPv6"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr "Ingreso"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr "Opciones de ingreso"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr "Ingreso: %s / Egreso: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr "Script de inicialización"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr "Instalado"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr "Nombre de interfaz"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr "Línea de regla no válida: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr "Ausente"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+#, fuzzy
+msgid "Netdev Name"
+msgstr "Nombre de Netdev"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr "No se han definido clases en /etc/config/qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+"No hay clases definidas. Añada primero las clases en la pestaña "
+"Configuración."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr "No se ha definido ninguna sección de configuración predeterminada."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr "No se ha seleccionado ningún archivo."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr ""
+"No se permiten espacios ni el símbolo # en los patrones ni en las "
+"direcciones."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr "No hay secciones de configuración UCI válidas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr "No se está ejecutando"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr "No instalado"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+"Nota: el ancho de banda no está configurado; CAKE funcionará sin "
+"restricciones en esa dirección."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr "Opciones"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr "Bytes de sobrecarga"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr "Tipo de sobrecarga"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "Vista general"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr "Paquete"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr "El puerto debe ser 1-65534 (qosify rechaza el 65535)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr ""
+"El puerto debe ser un número o un rango (por ejemplo, 4500 o 5060-5061)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+"Anteponer + para anular solo cuando el campo DSCP sea cero. Puertos: "
+"tcp:443, udp:3074, rangos: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: "
+"dns:/zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr "QoS activado"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr "Modo de cola"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+#, fuzzy
+msgid "Quick Add Config"
+msgstr "Configuración de adición rápida"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+#, fuzzy
+msgid "Quick Add Rule"
+msgstr "Regla de adición rápida"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr "Configuración rápida"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr "El inicio del rango no debe exceder el final."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr "Error de lectura"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr "Lectura y validación de archivos..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr "Recargar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+"Reemplaza ambos archivos de configuración con los valores predeterminados de "
+"qosify; qosify quedará desactivado."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr "Error al reiniciar: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr "¿Restablecer la configuración de qosify a los valores predeterminados?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr "Restablecer valores predeterminados"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr "Restablecer a valores predeterminados, aplicado."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr "Restablecer a los valores predeterminados de qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+#, fuzzy
+msgid "Resetting"
+msgstr "Restableciendo"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "Reiniciar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr "Restaurando valores predeterminados..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr "Reglas guardadas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr "Reglas: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "Ejecutándose"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr "Ejecutándose y modelando"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr "Ejecutándose — Sin modelado"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+"Las mismas opciones que para la clase — le da un segundo nombre a una clase "
+"existente."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+"Las mismas opciones que para la interfaz, pero el nombre es un netdev. nat "
+"tiene un valor predeterminado de 0 aquí y de 1 para las interfaces."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr "Guardar y aplicar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr "Error al guardar: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr "Guardando"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr "Guardando la configuración y aplicándola..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr "La sección %s ya existe."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+"El nombre de la sección es el nombre de la clase a la que se refieren las "
+"reglas y los valores dscp_*. El valor establece el ingreso y el egreso "
+"conjuntamente."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+"Seleccione los archivos y haga clic en Guardar y aplicar para sobrescribir y "
+"reiniciar qosify."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr "Enviando %s a qosify..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr "Controles de servicio"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "Estado del servicio"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr "La acción del servicio falló: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr "Configuración guardada"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "Iniciar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "Estado"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "Detener"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr "Modelado de tráfico y DSCP mediante qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr "Configuración de UCI — clases, interfaces, valores predeterminados."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr "Destino de clase/DSCP desconocido: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr "Cargar archivos de configuración"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr ""
+"¿Cargar y sobrescribir los archivos de configuración? qosify se reiniciará."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr "Error de carga: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr "Error al cargar: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr "Cargando"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr "Válido"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+"Advertencia: %s pero qosify no está modelando el tráfico — compruebe la "
+"pestaña Estado."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr "Trabajando"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr "Escribiendo la configuración y recargando qosify..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr "Escribiendo reglas y recargando qosify..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr "Tiene cambios sin guardar. ¿Quiere salir de esta pestaña?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr "patrón dns"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr "expresión regular dns"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr "patrón dns_c"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr "expresión regular dns_c"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr "ingress y egress son ambos 0 — no se modela nada"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+"el nombre no está configurado — qosify.init envía un nombre de dispositivo "
+"vacío y esta sección nunca se aplica"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+"el nombre es la interfaz netifd. El ancho de banda se aplica solo cuando "
+"bandwidth_up/bandwidth_down no están configurados. La sobrecarga y "
+"overhead_encap se aplican solo cuando overhead_type es manual."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+"nat no se envía: qosify solo emite nat/nonat dentro de la rama host_isolate. "
+"CAKE sí acepta flujos más nat — agregue nat en las opciones para aplicarlo"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+"nat solo llega a %s — inclúyalo en las opciones, o en ingress_options y "
+"egress_options"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr ""
+"sin bandwidth_down o bandwidth — ingress CAKE se ejecuta de forma ilimitada"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr ""
+"sin bandwidth_up ni bandwidth — egress CAKE se ejecuta de forma ilimitada"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr "solo si no está establecido (+)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+"overhead y overhead_encap se ignoran a menos que overhead_type sea manual"
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "Qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr "qosify no se está ejecutando. Comience desde la pestaña Vista general."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+"qosify solo pasa esto a CAKE junto con Host Isolate — agregue nat a Options "
+"para forzarlo"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr "qosify-status"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr "qosify-status no devolvió ningún resultado."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr "requerido — qosify omite las secciones sin nombre"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr "reglas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr "puerto tcp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr "puerto tcp+udp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr "puerto udp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr "se utiliza únicamente cuando «Tipo de sobrecarga» es manual"
diff --git a/luci-app-qosify/po/ga/qosify.po b/luci-app-qosify/po/ga/qosify.po
new file mode 100644
index 000000000..e84d6d7f6
--- /dev/null
+++ b/luci-app-qosify/po/ga/qosify.po
@@ -0,0 +1,787 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: ga\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Aindriú Mac Giolla Eoin \n"
+"Language-Team: Irish \n"
+"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :(n>"
+"6 && n<11) ? 3 : 4;\n"
+"PO-Revision-Date: 2026-09-02 17:28+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr "Tá luachan i %s — diúltaíonn qosify don luach iomlán"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr "%s uaslódáilte, qosify athlódáilte."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr "%s, curtha i bhfeidhm (QoS díchumasaithe)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr "%s, curtha i bhfeidhm."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr "(ainm cleite)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr "(roinn gan ainm)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr "Tá rannóg réamhshocruithe cumraíochta ann cheana féin."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "Gníomhach"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "Cuir leis"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "Ardleibhéil"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr "Uathoibriú Iontrála"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr "Uaththosú"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "Ar fáil"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr "Ranganna atá ar Fáil"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr "Cúltaca de Chomhaid Reatha"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr "Bandaleithead Síos"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr "Bandaleithead Suas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr "Ábhar dénártha diúltaithe"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr "Rialacha Aicmithe"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "Glan"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+"Glan an t-eagarthóir cumraíochta? Ní shábhálfar an t-ábhar go dtí go "
+"gcliceálann tú ar Shábháil."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+"Glan an t-eagarthóir rialacha? Ní shábhálfar an t-ábhar go dtí go "
+"gcliceálann tú ar Shábháil."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr "Cliceáil chun uaththosú a dhíchumasú"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr "Cliceáil chun uaththosú a chumasú"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr "Socruithe múnlaithe coitianta — scríofa díreach chuig %s, alt %s."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "Cumraíocht"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr "Tagairt Chumraíochta"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr "Glanadh an chumraíocht, stopadh qosify."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr "Cumraíocht sábháilte"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr "Cumraíocht: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr "Comhaid Chumraíochta"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+"Códphointí DSCP: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Féadfaidh aon "
+"luach dscp_* ainm a thabhairt d'aicme freisin. Cuir réimír + leis chun é a "
+"shárú ach amháin nuair atá an réimse DSCP nialasach."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr "Rialacha mapála DSCP luchtaithe ag qosify ag am tosaithe."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+"Réamhshocruithe bíonn feidhm ag qosify nuair a bhíonn eochair as láthair — "
+"comhéadan: modh diffserv4, ingress 1, egress 1, nat 1, host_isolate 1, "
+"autorate_ingress 0. gléas: mar an gcéanna seachas nat 0. réamhshocruithe: "
+"timeout 3600, dscp_default_tcp/udp CS0, dscp_prio/dscp_bulk/dscp_icmp unset, "
+"bulk_trigger_pps/bulk_trigger_timeout/prio_max_avg_pkt_len 0 (díchumasaithe)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "Míchumasaithe"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "Íoslódáil"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr "Íoslódáil comhaid chumraíochta reatha sula ndéantar athruithe."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+"Athluchtáladh eagarthóirí ón diosca — caitheadh na hathruithe eagarthóra "
+"neamhshábháilte i leataobh."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr "Imeacht"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr "Roghanna Imeachta"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr "Cuirfidh cumraíocht fholamh stop le Qosify. Lean ar aghaidh?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr "Comhad folamh"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "Cumasaithe"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr "Cumasaithe — Ní Rith"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr "Cumasaithe — Gan Múnlú (seiceáil cumraíocht)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr "Cuir isteach ainm rannóige (alfa-uimhriúil/fo-scor)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr "Cuir isteach seoladh IPv4 amháin (ní ghlacann qosify le CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr "Cuir isteach seoladh IPv6 amháin (ní ghlacann qosify le CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr "Cuir isteach luach."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr "Earráid: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr "Earráid: Níor aimsíodh aon véarsaí cumraíochta bailí."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr "Earráid: ní mór go mbeadh cuma 100mbit ar bandwidth_down"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr "Earráid: ní mór go mbeadh cuma 100mbit ar bandwidth_up"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+"Earráid: carachtair neamhbhailí i réimsí roghanna. Úsáid alfa-uimhriúla, "
+"spásanna, fleiscíní, poncanna, agus idirstadáin amháin."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr "Earráid: ní mór ainm gléis nó comhéadain a bheith ann"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr "Earráid: ní mór don fhorchostais a bheith ina líon iomlán beart"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr "Earráidí:"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr "Comhad ró-mhór (uasmhéid 64KB)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr "Aimsithe (folamh nó neamhbhailí)"
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr "Deonaigh rochtain ar aip LuCI qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr "Leithlisiú Óstach"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr "Seoladh IPv4"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr "Seoladh IPv6"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr "Dul Isteach"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr "Roghanna Dul Isteach"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr "Dul Isteach: %s / Imeacht: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr "Script Tosaithe"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr "Suiteáilte"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr "Ainm an Chomhéadain"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr "Líne riail neamhbhailí: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr "Ar iarraidh"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr "Ainm Netdev"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr "Níl aon ranganna sainmhínithe i /etc/config/qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+"Níl aon ranganna sainmhínithe. Cuir ranganna leis sa chluaisín Cumraíochta "
+"ar dtús."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr "Níl aon rannóg réamhshocruithe cumraíochta sainmhínithe"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr "Níl aon chomhaid roghnaithe."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr "Ní cheadaítear spásanna ná # i bpatrúin ná i seoltaí."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr "Gan aon véarsaí cumraíochta UCI bailí"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr "Gan Rith"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr "Gan suiteáil"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+"Nóta: níl an bandaleithead socraithe — rithfidh CAKE gan teorainn sa treo "
+"sin."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr "Roghanna"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr "Béiteanna Forchostais"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr "Cineál Lastuas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "Forbhreathnú"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr "Pacáiste"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr "Ní mór don phort a bheith 1-65534 (diúltaíonn qosify do 65535)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr ""
+"Ní mór don phort a bheith ina uimhir nó ina raon (m.sh. 4500 nó 5060-5061)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+"Cuir réimír + leis chun é a shárú ach amháin nuair atá an réimse DSCP "
+"nialasach. Calafoirt: tcp:443, udp:3074, raonta: tcp:5060-5061 (1-65534). "
+"DNS: dns:*teams*, regex: dns:/zoom[0-9]+, CNAME amháin: dns_c:. IP: 1.1.1.1, "
+"ff01::1"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr "QoS Cumasaithe"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr "Mód Scuaine"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr "Cumraíocht Cuir Leis Thapa"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr "Riail Cuir Leis Thapa"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr "Socruithe Tapa"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr "Ní mór tús an raoin a bheith níos faide ná deireadh an raoin."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr "Earráid léite"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr "Ag léamh agus ag bailíochtú comhad..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr "Athlódáil"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+"Cuireann sé réamhshocruithe qosify in ionad an dá chomhad cumraíochta, "
+"díchumasófar qosify."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr "Theip ar an athshocrú: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr "Athshocraigh cumraíocht qosify go dtí na socruithe réamhshocraithe?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr "Athshocraigh go Réamhshocruithe"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr "Athshocrú chuig réamhshocruithe, curtha i bhfeidhm."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr "Athshocraigh chun Réamhshocruithe Qosify a dhéanamh"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr "Ag athshocrú"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "Atosaigh"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr "Ag athchóiriú réamhshocruithe..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr "Rialacha sábháilte"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr "Rialacha: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "Ag rith"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr "Rith & Múnlú"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr "Rith — Ní Múnlú"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+"Na roghanna céanna le rang — tugann sé dara hainm do rang atá ann cheana "
+"féin."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+"Na roghanna céanna leis an gcomhéadan, ach is netdev an t-ainm. Is é 0 an "
+"réamhshocrú ar an luach nat anseo agus 1 do chomhéadain."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr "Sábháil & Cuir i bhFeidhm"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr "Theip ar shábháil: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr "Ag sábháil"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr "Ag sábháil socruithe agus ag cur i bhfeidhm..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr "Tá an roinn %s ann cheana féin."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+"Is é ainm na rannóige ainm an ranga a dtagraíonn luachanna rialacha agus "
+"dscp_* dó. Socraíonn luach an fheidhm isteach agus an fheidhm imeachta le "
+"chéile."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+"Roghnaigh comhaid agus cliceáil Sábháil & Cuir i bhFeidhm chun qosify a "
+"athscríobh agus a atosú."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr "Ag seoladh %s chuig qosify..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr "Rialuithe Seirbhíse"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "Stádas Seirbhíse"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr "Theip ar ghníomh seirbhíse: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr "Socruithe sábháilte"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "Tosaigh"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "Stádas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "Stop"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr "Múnlú tráchta agus aicmiú DSCP trí qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr "Cumraíocht UCI — ranganna, comhéadain, réamhshocruithe."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr "Sprioc ranga/DSCP anaithnid: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr "Uaslódáil Comhaid Chumraíochta"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr "Uaslódáil agus athscríobh comhaid chumraíochta? Athlódáilfidh qosify."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr "Earráid uaslódála: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr "Theip ar an uaslódáil: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr "Ag uaslódáil"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr "Bailí"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+"Rabhadh: %s ach níl qosify ag múnlú tráchta — seiceáil an cluaisín Stádas."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr "Ag obair"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr "Ag scríobh cumraíochta agus ag athlódáil qosify..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr "Rialacha a scríobh agus qosify a athlódáil..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr ""
+"Tá athruithe neamhshábháilte agat. An bhfuil tú ag iarraidh an cluaisín seo "
+"a fhágáil?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr "patrún dns"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr "regex dns"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr "patrún dns_c"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr "dns_c regex"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr ""
+"Tá an bealach isteach agus an bealach amach araon 0 — níl aon rud múnlaithe"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+"Níl an t-ainm socraithe — seolann qosify.init ainm gléis folamh agus ní "
+"chuirtear an chuid seo i bhfeidhm choíche"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+"Is é ainm an chomhéadan netifd. Ní bhaineann bandaleithead ach amháin nuair "
+"nach bhfuil bandaleithead_up/bandwidth_down socraithe. Ní bhaineann overhead "
+"agus overhead_encap ach amháin nuair a bhíonn overhead_type de láimh."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+"Ní sheoltar nat: ní astaíonn qosify ach nat/nonat laistigh den bhrainse "
+"host_isolate. Glacann CAKE le sreafaí móide nat — cuir nat sna roghanna chun "
+"é a chur i bhfeidhm"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+"ní shroicheann nat ach %s — cuir sna roghanna é, nó sna roghanna "
+"ingress_options agus egress_options araon"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr "gan bandwidth_down ná bandwidth — ritheann CAKE ingress gan teorainn"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr "gan bandwidth_up ná bandwidth — ritheann CAKE amach gan teorainn"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr "ach amháin mura bhfuil sé socraithe (+)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+"Déantar neamhaird de overhead agus overhead_encap mura bhfuil overhead_type "
+"de láimh"
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr "Níl qosify ag rith. Tosaigh ón táb Forbhreathnú."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+"Ní sheolann qosify ach seo chuig CAKE mar aon le Host Isolate — cuir nat "
+"leis na Roghanna chun é a fhorchur"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr "qosify-status"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr "Níor thug qosify-status aon aschur ar ais."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr "riachtanach — scipeálann qosify rannóga gan ainm"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr "rialacha"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr "port tcp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr "tcp+udp port"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr "udp port"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr "úsáidte ach amháin nuair a bhíonn Cineál Forchostais de láimh"
diff --git a/luci-app-qosify/po/ko/qosify.po b/luci-app-qosify/po/ko/qosify.po
new file mode 100644
index 000000000..f282adda2
--- /dev/null
+++ b/luci-app-qosify/po/ko/qosify.po
@@ -0,0 +1,732 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: ko\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Hyeonjeong Lee \n"
+"Language-Team: Korean \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"PO-Revision-Date: 2026-08-17 17:59+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr "(별칭)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "활성"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "추가"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "고급"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr "자동 시작"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "사용 가능"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr "현재 파일 백업"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr "다운로드 대역폭"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr "업로드 대역폭"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr "분류 규칙"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "설정"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr "설정: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr "설정 파일"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "비활성화"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "다운로드"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr "이그레스"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr "이그레스 옵션"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr "빈 파일"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "활성화"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr "오류: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr "오류:"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr "LuCI 앱 qosify에 접근 권한 부여"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr "호스트 격리"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr "IPv4 주소"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr "IPv6 주소"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr "인그레스"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr "인그레스 옵션"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr "인그레스: %s / 이그레스: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr "초기화 스크립트"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr "설치됨"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr "인터페이스 이름"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr "누락됨"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr "네트워크 장치 이름"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr "중지됨"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr "미설치"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr "옵션"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr "오버헤드 바이트"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr "오버헤드 유형"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "개요"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr "패키지"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr "대기열 모드"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr "빠른 설정 추가"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr "빠른 규칙 추가"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr "빠른 설정"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr "읽기 오류"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr "다시 불러오기"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr "초기화 실패: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr "기본값으로 초기화"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr "초기화 중"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "재시작"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr "기본값 복원 중..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr "규칙: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "실행 중"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr "저장 및 적용"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr "저장 실패: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr "저장 중"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr "설정 저장 및 적용 중..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr "서비스 제어"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "서비스 상태"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "시작"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "상태"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "중지"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr "설정 파일 업로드"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr "업로드 오류: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr "업로드 실패: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr "업로드 중"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "QoSify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr "규칙"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr ""
diff --git a/luci-app-qosify/po/lo/qosify.po b/luci-app-qosify/po/lo/qosify.po
new file mode 100644
index 000000000..42fde316c
--- /dev/null
+++ b/luci-app-qosify/po/lo/qosify.po
@@ -0,0 +1,756 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: lo\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Bone NI \n"
+"Language-Team: Lao \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"PO-Revision-Date: 2026-08-15 13:06+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr "%s ມີເຄື່ອງໝາຍຄໍາເວົ້າ — qosify ປະຕິເສດຄ່ານີ້ທັງໝົດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr "ອັບໂຫຼດ %s ແລ້ວ, ໂຫຼດ qosify ໃໝ່ແລ້ວ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr "%s, ນຳໃຊ້ແລ້ວ (ປິດ QoS)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr "%s, ນຳໃຊ້ແລ້ວ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr "(ຊື່ແທນ)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr "(ສ່ວນທີ່ບໍ່ມີຊື່)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr "ມີສ່ວນຄ່າເລີ່ມຕົ້ນການຕັ້ງຄ່າຢູ່ແລ້ວ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "ກຳລັງເຮັດວຽກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "ເພີ່ມ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "ຂັ້ນສູງ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr "ປັບຄວາມໄວຂາເຂົ້າອັດຕະໂນມັດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr "ເລີ່ມອັດຕະໂນມັດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "ມີຢູ່"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr "ຄລາສທີ່ມີຢູ່"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr "ສຳຮອງໄຟລ໌ປັດຈຸບັນ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr "ແບນວິດຂາເຂົ້າ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr "ແບນວິດຂາອອກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr "ເນື້ອຫາໄບນາຣີຖືກປະຕິເສດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr "ກົດເກນການຈັດປະເພດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "ລ້າງອອກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr "ລ້າງຕົວແກ້ໄຂການຕັ້ງຄ່າບໍ? ເນື້ອຫາຈະບໍ່ຖືກບັນທຶກຈົນກວ່າເຈົ້າຈະກົດ ບັນທຶກ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr "ລ້າງຕົວແກ້ໄຂກົດເກນບໍ? ເນື້ອຫາຈະບໍ່ຖືກບັນທຶກຈົນກວ່າເຈົ້າຈະກົດ ບັນທຶກ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr "ກົດເພື່ອປິດການເລີ່ມອັດຕະໂນມັດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr "ກົດເພື່ອເປີດການເລີ່ມອັດຕະໂນມັດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr "ການຕັ້ງຄ່າການຈັດສັນທຣາຟິກທົ່ວໄປ — ຂຽນລົງໃນ %s ໂດຍກົງ, ສ່ວນ %s."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "ຕັ້ງຄ່າ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr "ບ່ອນອ້າງອີງການຕັ້ງຄ່າ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr "ລ້າງການຕັ້ງຄ່າແລ້ວ, qosify ຢຸດເຮັດວຽກແລ້ວ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr "ບັນທຶກການຕັ້ງຄ່າແລ້ວ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr "ການຕັ້ງຄ່າ: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr "ໄຟລ໌ການຕັ້ງຄ່າ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. ຄ່າ dscp_* "
+"ໃດໆກໍຕາມອາດໃຊ້ຕັ້ງຊື່ຄລາສໄດ້. ໃສ່ເຄື່ອງໝາຍ + ທາງໜ້າເພື່ອຂຽນທັບສະເພາະເມື່ອຟິລ DSCP ເປັນສູນເທົ່ານັ້ນ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr "ກົດເກນການຈັບຄູ່ DSCP ທີ່ qosify ໂຫຼດຂຶ້ນມາຕອນເລີ່ມຕົ້ນ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+"ຄ່າເລີ່ມຕົ້ນທີ່ qosify ນຳໃຊ້ເມື່ອບໍ່ມີຄ່າທີ່ກຳນົດ — interface: mode diffserv4, ingress 1, "
+"egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: ຄືກັນຍົກເວັ້ນ nat 0. "
+"defaults: timeout 3600, dscp_default_tcp/udp CS0, dscp_prio/dscp_bulk/"
+"dscp_icmp ບໍ່ໄດ້ກຳນົດ, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (ປິດໃຊ້ງານ)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "ປິດໃຊ້ງານ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "ດາວໂຫຼດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr "ດາວໂຫຼດໄຟລ໌ການຕັ້ງຄ່າປັດຈຸບັນກ່ອນທີ່ຈະປ່ຽນແປງ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr "ໂຫຼດຕົວແກ້ໄຂຈາກດິສກ໌ຄືນໃໝ່ — ການປ່ຽນແປງທີ່ບໍ່ໄດ້ບັນທຶກຈະຖືກຖິ້ມໄປ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr "ຂາອອກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr "ຕົວເລືອກຂາອອກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr "ການຕັ້ງຄ່າທີ່ວ່າງເປົ່າຈະເຮັດໃຫ້ qosify ຢຸດເຮັດວຽກ. ດຳເນີນການຕໍ່ບໍ?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr "ໄຟລ໌ວ່າງເປົ່າ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "ເປີດໃຊ້ງານ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr "ເປີດໃຊ້ງານ — ແຕ່ບໍ່ເຮັດວຽກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr "ເປີດໃຊ້ງານ — ແຕ່ບໍ່ມີການຈັດສັນທຣາຟິກ (ກວດສອບການຕັ້ງຄ່າ)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr "ໃສ່ຊື່ສ່ວນ (ຕົວອັກສອນ/ຕົວເລກ/ຂີດກ້ອງ)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr "ໃສ່ທີ່ຢູ່ IPv4 ດຽວ (qosify ບໍ່ຮອງຮັບ CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr "ໃສ່ທີ່ຢູ່ IPv6 ດຽວ (qosify ບໍ່ຮອງຮັບ CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr "ໃສ່ຄ່າ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr "ຂໍ້ຜິດພາດ: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr "ຂໍ້ຜິດພາດ: ບໍ່ພົບສ່ວນການຕັ້ງຄ່າທີ່ຖືກຕ້ອງ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr "ຂໍ້ຜິດພາດ: bandwidth_down ຕ້ອງເປັນຮູບແບບເຊັ່ນ 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr "ຂໍ້ຜິດພາດ: bandwidth_up ຕ້ອງເປັນຮູບແບບເຊັ່ນ 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+"ຂໍ້ຜິດພາດ: ພົບຕົວອັກສອນທີ່ບໍ່ຖືກຕ້ອງໃນຊ່ອງຕົວເລືອກ. ໃຫ້ໃຊ້ສະເພາະ ຕົວອັກສອນ, ຕົວເລກ, ຍະຫວ່າງ, ຂີດຕໍ່, "
+"ຈ້ໍາ, ແລະ ຈ້ໍາຄູ່ ເທົ່ານັ້ນ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr "ຂໍ້ຜິດພາດ: ຊື່ຕ້ອງເປັນຊື່ອຸປະກອນ ຫຼື ຊື່ອິນເຕີເຟສ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr "ຂໍ້ຜິດພາດ: overhead ຕ້ອງເປັນຕົວເລກຈຳນວນເຕັມຂອງໄບຕ໌"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr "ຂໍ້ຜິດພາດ:"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr "ໄຟລ໌ໃຫຍ່ເກີນໄປ (ສູງສຸດ 64KB)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr "ພົບ (ວ່າງເປົ່າ ຫຼື ບໍ່ຖືກຕ້ອງ)"
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr "ແຍກໂຮສຕ໌"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr "ທີ່ຢູ່ IPv4"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr "ທີ່ຢູ່ IPv6"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr "ຂາເຂົ້າ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr "ຕົວເລືອກຂາເຂົ້າ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr "ຂາເຂົ້າ: %s / ຂາອອກ: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr "ສະຄຣິບເລີ່ມຕົ້ນ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr "ຕິດຕັ້ງແລ້ວ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr "ຊື່ອິນເຕີເຟສ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr "ແຖວກົດເກນບໍ່ຖືກຕ້ອງ: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr "ຫາຍໄປ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr "ຊື່ອຸປະກອນເຄືອຂ່າຍ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr "ບໍ່ມີການກຳນົດຄລາສໃນ /etc/config/qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr "ບໍ່ມີການກຳນົດຄລາສ. ໃຫ້ເພີ່ມຄລາສໃນແທັບ ການຕັ້ງຄ່າ ກ່ອນ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr "ບໍ່ມີການກຳນົດສ່ວນຄ່າເລີ່ມຕົ້ນການຕັ້ງຄ່າ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr "ບໍ່ໄດ້ເລືອກໄຟລ໌."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr "ບໍ່ອະນຸຍາດໃຫ້ມີຍະຫວ່າງ ຫຼື # ໃນຮູບແບບ ຫຼື ທີ່ຢູ່."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr "ບໍ່ພົບສ່ວນການຕັ້ງຄ່າ UCI ທີ່ຖືກຕ້ອງ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr "ບໍ່ເຮັດວຽກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr "ບໍ່ໄດ້ຕິດຕັ້ງ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr "ໝາຍເຫດ: ບໍ່ໄດ້ຕັ້ງຄ່າແບນວິດ — CAKE ຈະເຮັດວຽກແບບບໍ່ຈຳກັດຄວາມໄວໃນທິດທາງນັ້ນ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr "ຕົວເລືອກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr "ໄບຕ໌ສ່ວນເກີນ (Overhead)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr "ປະເພດສ່ວນເກີນ (Overhead)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "ພາບລວມ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr "ແພັກເກດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr "ພອດຕ້ອງຢູ່ລະຫວ່າງ 1-65534 (qosify ປະຕິເສດ 65535)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr "ພອດຕ້ອງເປັນຕົວເລກ ຫຼື ຊ່ວງ (ເຊັ່ນ: 4500 ຫຼື 5060-5061)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+"ໃສ່ເຄື່ອງໝາຍ + ທາງໜ້າເພື່ອຂຽນທັບສະເພາະເມື່ອຟິລ DSCP ເປັນສູນ. ພອດ: tcp:443, udp:3074, "
+"ຊ່ວງ: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/zoom[0-9]+, "
+"CNAME-ເທົ່ານັ້ນ: dns_c:. IP: 1.1.1.1, ff01::1"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr "ເປີດໃຊ້ QoS"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr "ໂໝດຄິວ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr "ເພີ່ມການຕັ້ງຄ່າແບບດ່ວນ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr "ເພີ່ມກົດເກນແບບດ່ວນ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr "ການຕັ້ງຄ່າດ່ວນ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr "ຄ່າເລີ່ມຕົ້ນຂອງຊ່ວງຕ້ອງບໍ່ເກີນຄ່າສຸດທ້າຍ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr "ອ່ານຜິດພາດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr "ກຳລັງອ່ານ ແລະ ກວດສອບຄວາມຖືກຕ້ອງຂອງໄຟລ໌..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr "ໂຫຼດໃໝ່"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr "ແທນທີ່ໄຟລ໌ການຕັ້ງຄ່າທັງສອງດ້ວຍຄ່າເລີ່ມຕົ້ນຂອງ qosify, ແລະ qosify ຈະຖືກປິດໃຊ້ງານ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr "ການຣີເຊັດລົ້ມເຫຼວ: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr "ຣີເຊັດການຕັ້ງຄ່າ qosify ເປັນຄ່າເລີ່ມຕົ້ນບໍ?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr "ຣີເຊັດເປັນຄ່າເລີ່ມຕົ້ນ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr "ຣີເຊັດເປັນຄ່າເລີ່ມຕົ້ນ ແລະ ນຳໃຊ້ແລ້ວ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr "ຣີເຊັດເປັນຄ່າເລີ່ມຕົ້ນຂອງ qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr "ກຳລັງຣີເຊັດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "ເລີ່ມໃໝ່"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr "ກຳລັງຄືນຄ່າເລີ່ມຕົ້ນ..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr "ບັນທຶກກົດເກນແລ້ວ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr "ກົດເກນ: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "ກຳລັງເຮັດວຽກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr "ກຳລັງເຮັດວຽກ ແລະ ຈັດສັນທຣາຟິກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr "ກຳລັງເຮັດວຽກ — ແຕ່ບໍ່ມີການຈັດສັນທຣາຟິກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr "ໃຊ້ຕົວເລືອກດຽວກັນກັບຄລາສ — ຕັ້ງຊື່ທີສອງໃຫ້ກັບຄລາສທີ່ມີຢູ່."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+"ໃຊ້ຕົວເລືອກດຽວກັນກັບອິນເຕີເຟສ, ແຕ່ຊື່ອັນນີ້ແມ່ນ netdev. nat ຈະເປັນ 0 ຢູ່ບ່ອນນີ້ ແລະ ເປັນ 1 "
+"ສຳລັບອິນເຕີເຟສ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr "ບັນທຶກ ແລະ ນຳໃຊ້"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr "ບັນທຶກລົ້ມເຫຼວ: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr "ກຳລັງບັນທຶກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr "ກຳລັງບັນທຶກການຕັ້ງຄ່າ ແລະ ນຳໃຊ້..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr "ສ່ວນ %s ມີຢູ່ແລ້ວ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+"ຊື່ສ່ວນແມ່ນຊື່ຄລາສທີ່ກົດເກນ ແລະ ຄ່າ dscp_* ອ້າງອີງເຖິງ. ຄ່ານີ້ຈະກຳນົດທັງຂາເຂົ້າ ແລະ ຂາອອກໄປພ້ອມກັນ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr "ເລືອກໄຟລ໌ ແລະ ກົດ ບັນທຶກ ແລະ ນຳໃຊ້ ເພື່ອຂຽນທັບ ແລະ ເລີ່ມ qosify ໃໝ່."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr "ກຳລັງສົ່ງ %s ໄປຫາ qosify..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr "ການຄວບຄຸມການບໍລິການ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "ສະຖານະການບໍລິການ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr "ການດຳເນີນການບໍລິການລົ້ມເຫຼວ: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr "ບັນທຶກການຕັ້ງຄ່າແລ້ວ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "ເລີ່ມ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "ສະຖານະ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "ຢຸດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr "ການຈັດສັນທຣາຟິກ ແລະ ການຈັດປະເພດ DSCP ຜ່ານ qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr "ການຕັ້ງຄ່າ UCI — ຄລາສ, ອິນເຕີເຟສ, ຄ່າເລີ່ມຕົ້ນ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr "ບໍ່ຮູ້ຈັກຄລາສ/ເປົ້າໝາຍ DSCP: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr "ອັບໂຫຼດໄຟລ໌ການຕັ້ງຄ່າ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr "ອັບໂຫຼດ ແລະ ຂຽນທັບໄຟລ໌ການຕັ້ງຄ່າບໍ? qosify ຈະໂຫຼດໃໝ່."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr "ອັບໂຫຼດຜິດພາດ: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr "ອັບໂຫຼດລົ້ມເຫຼວ: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr "ກຳລັງອັບໂຫຼດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr "ຖືກຕ້ອງ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr "ຄຳເຕືອນ: %s ແຕ່ qosify ບໍ່ໄດ້ຈັດສັນທຣາຟິກ — ໃຫ້ກວດສອບທີ່ແທັບ ສະຖານະ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr "ກຳລັງເຮັດວຽກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr "ກຳລັງຂຽນການຕັ້ງຄ່າ ແລະ ໂຫຼດ qosify ໃໝ່..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr "ກຳລັງຂຽນກົດເກນ ແລະ ໂຫຼດ qosify ໃໝ່..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr "ເຈົ້າມີການປ່ຽນແປງທີ່ບໍ່ໄດ້ບັນທຶກ. ອອກຈາກແທັບນີ້ບໍ?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr "ຮູບແບບ dns"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr "dns regex"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr "ຮູບແບບ dns_c"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr "dns_c regex"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr "ຂາເຂົ້າ ແລະ ຂາອອກ ເປັນ 0 ທັງສອງ — ຈະບໍ່ມີການຈັດສັນທຣາຟິກ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr "ບໍ່ໄດ້ຕັ້ງຊື່ — qosify.init ສົ່ງຊື່ອຸປະກອນທີ່ວ່າງເປົ່າ ແລະ ສ່ວນນີ້ຈະບໍ່ຖືກນຳໃຊ້"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+"ຊື່ແມ່ນອິນເຕີເຟສ netifd. ແບນວິດຈະນຳໃຊ້ສະເພາະເມື່ອບໍ່ໄດ້ກຳນົດ bandwidth_up/bandwidth_down. "
+"overhead ແລະ overhead_encap ຈະນຳໃຊ້ສະເພາະເມື່ອ overhead_type ເປັນ manual."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+"ບໍ່ໄດ້ສົ່ງ nat: qosify ຈະສົ່ງ nat/nonat ສະເພາະພາຍໃນກຸ່ມ host_isolate ເທົ່ານັ້ນ. CAKE "
+"ຮອງຮັບ flows ບວກກັບ nat — ໃຫ້ໃສ່ nat ໃນຕົວເລືອກເພື່ອເປີດໃຊ້ງານ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+"nat ສົ່ງໄປຮອດແຕ່ %s — ໃຫ້ໃສ່ມັນໃນຕົວເລືອກ, ຫຼື ໃສ່ທັງໃນ ingress_options ແລະ "
+"egress_options"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr "ບໍ່ມີ bandwidth_down ຫຼື bandwidth — CAKE ຂາເຂົ້າຈະເຮັດວຽກແບບບໍ່ຈຳກັດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr "ບໍ່ມີ bandwidth_up ຫຼື bandwidth — CAKE ຂາອອກຈະເຮັດວຽກແບບບໍ່ຈຳກັດ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr "ສະເພາະເມື່ອຍັງບໍ່ໄດ້ກຳນົດ (+) ເທົ່ານັ້ນ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+"overhead ແລະ overhead_encap ຈະຖືກລະເລີຍ ນອກຈາກວ່າ overhead_type ຈະເປັນ manual"
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr "qosify ບໍ່ເຮັດວຽກ. ໃຫ້ເລີ່ມຈາກແທັບ ພາບລວມ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+"qosify ຈະສົ່ງຄ່ານີ້ໃຫ້ CAKE ພ້ອມກັບ Host Isolate ເທົ່ານັ້ນ — ໃຫ້ເພີ່ມ nat ເຂົ້າໃນ ຕົວເລືອກ "
+"ເພື່ອບັງຄັບໃຊ້"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr "ສະຖານະ qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr "qosify-status ບໍ່ມີຂໍ້ມູນອອກມາ."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr "ຈຳເປັນ — qosify ຈະຂ້າມສ່ວນທີ່ບໍ່ມີຊື່"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr "ກົດເກນ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr "ພອດ tcp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr "ພອດ tcp+udp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr "ພອດ udp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr "ໃຊ້ສະເພາະເມື່ອ ປະເພດສ່ວນເກີນ (Overhead Type) ເປັນ manual ເທົ່ານັ້ນ"
diff --git a/luci-app-qosify/po/lt/qosify.po b/luci-app-qosify/po/lt/qosify.po
new file mode 100644
index 000000000..f33985648
--- /dev/null
+++ b/luci-app-qosify/po/lt/qosify.po
@@ -0,0 +1,793 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: lt\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Džiugas Januševičius \n"
+"Language-Team: Lithuanian \n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"(n%100<10 || n%100>=20) ? 1 : 2);\n"
+"PO-Revision-Date: 2026-08-27 04:49+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr "%s yra kabutėse — „qosify“ atmeta visą reikšmę"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr "%s įkelta, „qosify“ perleista."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr "%s, pritaikyta („QoS“ išjungtas/išgalintas)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr "%s, pritaikyta."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr "(pseudonimai)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr "(nepavadintas skyrius)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr "Numatytųjų konfigūracijos reikšmių skyrius jau egzistuoja."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "Aktyvus/-i"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "Pridėti"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "Pažangus"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr "Automatinis įeinančių signalų reguliavimas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr "Automatinis paleidimas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "Pasiekiamas/-a/-i/-os"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr "Pasiekiamos klasės"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr "Atsarginės kopijos dabartiniai failai"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr "Duomenų siuntimo ir perdavimo srauto pralaidumo išėjimo greitis"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr "Duomenų siuntimo ir perdavimo srauto pralaidumo įėjimo greitis"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr "Dvejetainės turinys atmestas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr "Klasifikavimo taisyklės"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "Išvalyti"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+"Išvalyti konfigūracijos redaktorių? Turinys nebus išsaugotas, kol Jūs "
+"nepaspausite – „Išsaugoti“."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+"Išvalyti taisyklių redaktorių? Turinys nebus išsaugotas, kol Jūs "
+"nepaspausite – „Išsaugoti“."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr "Spustelėkite, jei norite išjungti/išgalinti automatinį paleidimą"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr "Spustelėkite, jei norite įjungti/įgalinti automatinį paleidimą"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr "Įprasti formavimo nustatymai – rašomi tiesiai į – %s, %s skyriuje."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "Konfigūracija"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr "Konfigūracijos pamatinis"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr "Konfigūracija išvalyta, „qosify“ sustabdytas."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr "Konfigūracija išsaugota"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr "Konfigūracija: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr "Konfigūracijos failai"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+"„DSCP“ kodo taškai: „CS0–CS7“, „AF11–AF43“, „EF“, „VA“, „NQB“, „LE“, „DF“. "
+"Bet kuri „dscp_*“ reikšmė taip pat gali pavadinti klasę. Prielinksnis/"
+"Priešdėlis su „+“, norint perkeisti/perrašyti tik tada, kai „DSCP“ laukas "
+"yra nulis."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr "„DSCP“ atvaizdavimo taisyklės įkeliamos „qosify“ paleidimo metu."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+"Numatytosios reikšmės „qosify“ taikomos, kai nėra rakto — sąsaja ir/arba "
+"sietuvas: veiksena „diffserv4“, „ingress 1“, „egress 1“, „nat 1“, "
+"„host_isolate 1“, „autorate_ingress 0“. įrenginys: identiškas, išskyrus „nat "
+"0“. numatytosios reikšmės: „timeout 3600“, „dscp_default_tcp/udp CS0“, "
+"„dscp_prio/dscp_bulk/dscp_icmp unset“, „bulk_trigger_pps/"
+"bulk_trigger_timeout/prio_max_avg_pkt_len 0“ (išjungta/išgalinta)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "Išjungtas/Išgalintas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "Atsisiųsti"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr "Atsisiųsti dabartinės konfigūracijos failus prieš darant pakeitimus."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+"Redaktoriai perleisti iš disko — neišsaugoti redaktoriaus pakeitimai buvo "
+"atmesti."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr "Išėjimas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr "Išėjimo parinktys"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr "Tuščia konfigūracija sustabdys „qosify“. Tęsti?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr "Tuščias failas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "Įjungtas/Įgalintas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr "Įjungtas/Įgalintas – nevykdomas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr "Įjungtas/Įgalintas – neformuojamas (patikrinkite konfigūraciją)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr "Įveskite skyriaus pavadinimą (tekstinis/pabraukimas)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr "Įveskite vieną IPv4 adresą („qosify“ nepriima „CIDR“)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr "Įveskite vieną IPv6 adresą („qosify“ nepriima „CIDR“)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr "Įveskite reikšmę."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr "Klaida: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr "Klaida: nerasta tinkamos konfigūracijos „posma“."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr "Klaida: „bandwidth_down“ turi atrodyti kaip 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr "Klaida: „bandwidth_up“ turi atrodyti kaip 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+"Klaida: negalimos rašmenys parinkčių laukuose. Naudokite tik tekstinius, "
+"tarpus, brūkšnelius, taškus ir dvitaškius."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr ""
+"Klaida: pavadinimas turi būti įrenginio arba sąsajos ir/arba sietuvo "
+"pavadinimas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr "Klaida: pridėtinė suma turi būti sveikasis baitų skaičius"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr "Klaidos:"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr "Failas per didelis (maks. 64 KB)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr "Rasta (tuščia arba negalima)"
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr "Suteikti prieigą prie „LuCI“ programėles – „qosify“"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr "Skleidėjo/Vedėjo izoliatas („Host isolate“)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr "IPv4 adresas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr "IPv6 adresas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr "Įėjimas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr "Įėjimo parinktys"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr "Įėjimas: %s/Išėjimas: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr "Inicializavimo skriptas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr "Įdiegta/-s/-i"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr "Sąsajos ir/arba Sietuvo pavadinimas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr "Negalima taisyklės eilutė: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr "Trūksta(mos)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "„NAT“"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr "„Netdev“ pavadinimas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr "Nėra apibrėžtų klasių per – „/etc/config/qosify“"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+"Nėra apibrėžtų klasių. Pirmiausia pridėkite klases skirtuke – "
+"„Konfigūracija“."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr "Nėra apibrėžto numatytųjų konfigūracijos parametrų skyriaus"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr "Nėra pasirinktų failų."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr "Neleidžiami tarpai arba „#“ šablonuose ar adresuose."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr "Nėra tinkamos „UCI“ konfigūracijos „posmos“"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr "Nevykdomas/-a"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr "Neįdiegta"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+"Pastaba: duomenų siuntimo ir perdavimo srauto pralaidumas nenustatytas – "
+"„CAKE“ ta kryptimi veiks neribotai."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr "Parinktys"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr "Pridėtinės sumos baitai"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr "Pridėtinės sumos tipas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "Apžiūra"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr "Prog. įrang. paketas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr "Prievadas turi būti diapazone nuo 1 iki 65534 („qosify“ atmeta 65535)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr ""
+"Prievadas turi būti skaičius arba diapazonas (pvz., 4500 arba 5060–5061)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+"Prielinksnis/Priešdėlis su „+“ perkeičia/perrašo tik tada, kai „DSCP“ laukas "
+"yra nulis. Prievadai: „tcp:443“, „udp:3074“, diapazonai: „tcp:5060-5061“ "
+"(1-65534). „DNS“: „dns:*teams“*, reguliarusis reiškinys: „dns:/zoom[0-9]+“, "
+"tik „CNAME: dns_c:.“ IP: 1.1.1.1, ff01::1"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr "„QoS“ įjungtas/įgalintas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr "Eilės veiksena"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr "Greitai pridėti konfigūraciją"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr "Greitai pridėti taisyklė"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr "Greiti nustatymai"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr "Diapazono pradžia negali viršyti pabaigos."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr "Nuskaitymo klaida"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr "Nuskaitomi ir tikrinami failai..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr "Perleisti"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+"Pakeičia abu konfigūracijos failus numatytosiomis „qosify“ reikšmėmis, "
+"„qosify“ bus išjungtas/išgalintas."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr "Grąžinti į pradinę padėti ar būsena nepavyko: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr ""
+"Grąžinti „qosify“ konfigūraciją į pradinės padėties ar būsenos numatytuosius?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr "Grąžinti į pradinės padėties ar būsenos numatytuosius"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr "Grąžinti į pradinės padėties ar būsenos numatytuosius, pritaikyta."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr "Grąžinti „qosify“ į pradinės padėties ar būsenos numatytuosius"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr "Grąžinamą į pradinę padėti ar būseną"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "Paleisti iš naujo"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr "Paleidžiami iš naujo numatytieji..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr "Taisyklės išsaugotos"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr "Taisyklės: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "Vykdomas/-a"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr "Vykdomas/-a ir formuojamas/-a"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr "Vykdomas/-a – neformuojamas/-a"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+"Tos pačios parinktys kaip ir klasės – suteikia esamai klasei antrą "
+"pavadinimą."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+"Tos pačios parinktys kaip ir sąsajos ir/arba sietuvo atveju, bet pavadinimas "
+"yra – „netdev“. „NAT“ čia pagal numatytuosius nustatymus yra – 0-is, o "
+"sąsajoms – 1-as."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr "Išsaugoti ir pritaikyti"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr "Išsaugoti nepavyko: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr "Išsaugojamas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr "Išsaugojami nustatymai ir pritaikomi..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr "Skyrius %s jau egzistuoja."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+"Skyriaus pavadinimas yra klasės pavadinimas, į kurį nurodo taisyklės ir "
+"„dscp_*“ reikšmės. Reikšmės nustato įėjimus ir išėjimus kartu."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+"Pasirinkite failus ir spustelėkite – „Išsaugoti ir pritaikyti“, kad "
+"perrašytumėte ir paleistumėte iš naujo „qosify“."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr "Siunčiama %s į – „qosify“..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr "Tarnybos valdymai"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "Tarnybos būklė/būsena"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr "Tarnybos veiksmas nepavyko: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr "Nustatymas išsaugotas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "Paleisti"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "Būklė/Būsena"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "Stabdyti"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr "Srauto formavimas ir „DSCP“ klasifikavimas per – „qosify“"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr "„UCI“ konfigūracija — klasės, sąsajos ir/arba sietuvai, numatytieji."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr "Nežinoma klasė/„DSCP“ taikomasis: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr "Įkelti konfigūracijos failus"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr "Įkelti ir perrašyti konfigūracijos failus? „qosify“ bus perleistas."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr "Įkėlimo klaida: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr "Įkėlimas nepavyko: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr "Įkeliama"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr "Tinkamas/-a"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+"Įspėjimas: %s, bet „qosify“ neformuoja srauto – patikrinkite skirtuką – "
+"„Būklė/Būsena“."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr "Veikia"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr "Rašoma konfigūracija ir perleidžiama „qosify“..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr "Rašomos taisyklės ir perleidžiama „qosify“..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr "Jūs turite neišsaugotų pakeitimų. Išeiti iš šio skirtuko?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr "„dns“ šablonas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr "„dns“ reguliarusis reiškinys"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr "„dns_c“ šablonas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr "„dns_c“ reguliarusis reiškinys"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr "Įėjimas ir išėjimas abu lygūs 0-iui – niekas nėra suformuota"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+"pavadinimas nėra nustatytas — „qosify.init“ siunčia tuščią įrenginio "
+"pavadinimą ir šis skyrius niekada nėra pritaikomas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+"„name“ yra „netifd“ sąsaja ir/arba sietuvas. „bandwidth“ pritaikoma tik "
+"tada, kai „bandwidth_up“/„bandwidth_down“ nėra nustatyti. „overhead“ ir "
+"„overhead_encap“ pritaikomi tik tada, kai „overhead_type“ yra nustatytas "
+"rankiniu būdu."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+"„nat“ nėra išsiųstas: „qosify“ išduoda „nat/nonat“ tik „host_isolate“ šakos "
+"viduje. „CAKE“ priima srautus ir „nat“ – įveskite/pridėkite „nat“ "
+"parinktyse, kad jį pritaikytumėte"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+"„nat“ pasiekia tik %s – įveskite/pridėkite tai į – „options“ arba į abi – "
+"„ingress_options“ ir „egress_options“"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr ""
+"nėra „bandwidth_down“ ar „bandwidth“ — įeinantis „CAKE“ veikia neribotai"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr ""
+"nėra „bandwidth_down“ ar „bandwidth“ — išeinantis „CAKE“ veikia neribotai"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr "tik jei nenustatyta (+)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+"„overhead“ ir „overhead_encap“ yra ignoruojami, nebent „overhead_type“ yra "
+"rankinis"
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "„qosify“"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr "„qosify“ neveikia. Paleiskite iš skirtuko – „Apžiūra“."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+"„qosify“ perduoda tai tik „CAKE“ kartu su „Host Isolate“ – pridėkite „nat“ į "
+"–„Parinktys“, kad tai būtų priverstinai atlikta"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr "„qosify-status“"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr "„qosify-status“ negrąžino jokios išvesties."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr "Reikalinga — „qosify“ praleidžia skyrius be pavadinimo"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr "Taisyklės"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr "„tcp“ prievadas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr "„tcp+udp“ prievadas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr "„udp“ prievadas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr "naudojamas tik tada, kai „overhead_type“ yra rankinis"
diff --git a/luci-app-qosify/po/pl/qosify.po b/luci-app-qosify/po/pl/qosify.po
new file mode 100644
index 000000000..733d6aff8
--- /dev/null
+++ b/luci-app-qosify/po/pl/qosify.po
@@ -0,0 +1,781 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: pl\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Matthaiks \n"
+"Language-Team: Polish \n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2);\n"
+"PO-Revision-Date: 2026-08-16 22:03+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr "%s zawiera cudzysłów — qosify odrzuca całą wartość"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr "%s przesłano, qosify załadowano ponownie."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr "%s, zastosowano (QoS wyłączone)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr "%s, zastosowano."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr "(alias)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr "(sekcja bez nazwy)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr "Sekcja ustawień domyślnych już istnieje."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "Aktywna"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "Dodaj"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "Zaawansowane"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr "Automatyczna przepustowość wejścia"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr "Autostart"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "Dostępne"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr "Dostępne klasy"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr "Utwórz kopię zapasową bieżących plików"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr "Przepustowość pobierania"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr "Przepustowość wysyłania"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr "Zawartość binarna odrzucona"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr "Zasady klasyfikacji"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "Wyczyść"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+"Wyczyścić edytor konfiguracji? Zawartość nie zostanie zapisana, dopóki nie "
+"klikniesz „Zapisz”."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+"Wyczyścić edytor zasad? Zawartość nie zostanie zapisana, dopóki nie "
+"klikniesz „Zapisz”."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr "Kliknij, aby wyłączyć autostart"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr "Kliknij, aby włączyć autostart"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr ""
+"Wspólne ustawienia kształtowania — zapisywane bezpośrednio w %s, sekcja %s."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "Konfiguracja"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr "Odwołanie do konfiguracji"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr "Konfiguracja wyczyszczona, qosify zatrzymany."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr "Konfiguracja zapisana"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr "Konfiguracja: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr "Pliki konfiguracyjne"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+"Punkty kodowe DSCP: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Dowolna wartość "
+"dscp_* może również określać klasę. Prefiks + służy do nadpisania tylko "
+"wtedy, gdy pole DSCP jest równe zero."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr "Zasady mapowania DSCP ładowane przez qosify podczas uruchamiania."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+"Domyślne wartości qosify są stosowane, gdy klucz jest nieobecny — interfejs: "
+"mode diffserv4, ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress "
+"0. urządzenie: identyczne, z wyjątkiem nat 0. wartości domyślne: timeout "
+"3600, dscp_default_tcp/udp CS0, dscp_prio/dscp_bulk/dscp_icmp unset, "
+"bulk_trigger_pps/bulk_trigger_timeout/prio_max_avg_pkt_len 0 (wyłączone)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "Wyłączone"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "Pobierz"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr "Przed wprowadzeniem zmian pobierz aktualne pliki konfiguracyjne."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+"Edytory zostały ponownie załadowane z dysku — niezapisane zmiany edytora "
+"zostały odrzucone."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr "Wyjście"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr "Opcje wyjścia"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr "Pusta konfiguracja zatrzyma qosify. Kontynuować?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr "Pusty plik"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "Włączone"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr "Włączone — nieuruchomione"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr "Włączone — brak kształtowania (sprawdź konfigurację)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr "Wpisz nazwę sekcji (alfanumeryczną/podkreśloną)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr "Wpisz pojedynczy adres IPv4 (qosify nie akceptuje CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr "Wpisz pojedynczy adres IPv6 (qosify nie akceptuje CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr "Wpisz wartość."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr "Błąd: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr "Błąd: nie znaleziono prawidłowych sekcji konfiguracji."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr "Błąd: bandwidth_down musi wyglądać jak 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr "Błąd: bandwidth_up musi wyglądać jak 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+"Błąd: nieprawidłowe znaki w polach opcji. Używaj tylko znaków "
+"alfanumerycznych, spacji, myślników, kropek i dwukropków."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr "Błąd: nazwa musi być nazwą urządzenia lub interfejsu"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr "Błąd: narzut musi być liczbą całkowitą bajtów"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr "Błędy:"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr "Plik jest za duży (maks. 64 KB)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr "Znaleziono (puste lub nieprawidłowe)"
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr "Udziel dostępu do aplikacji LuCI qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr "Izolacja hosta"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr "Adres IPv4"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr "Adres IPv6"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr "Wejście"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr "Opcje wejścia"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr "Wejście: %s / Wyjście: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr "Skrypt inicjujący"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr "Zainstalowane"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr "Nazwa interfejsu"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr "Nieprawidłowy wiersz zasady: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr "Brak"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr "Nazwa netdev"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr "Brak zdefiniowanych klas w /etc/config/qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+"Brak zdefiniowanych klas. Najpierw dodaj klasy na karcie „Konfiguracja”."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr "Brak zdefiniowanej sekcji ustawień domyślnych konfiguracji"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr "Nie wybrano plików."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr "We wzorcach i adresach nie można używać spacji ani znaków #."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr "Brak prawidłowych stanów konfiguracji UCI"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr "Nieuruchomione"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr "Niezainstalowane"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+"Uwaga: przepustowość nie została ustawiona — CAKE będzie działać "
+"nieograniczenie w tym kierunku."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr "Opcje"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr "Bajty narzutu"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr "Typ narzutu"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "Przegląd"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr "Pakiet"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr "Port musi być 1-65534 (qosify odrzuca port 65535)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr "Port musi być liczbą lub zakresem (np. 4500 lub 5060-5061)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+"Dodaj prefiks +, aby zastąpić tylko wtedy, gdy pole DSCP jest zerowe. Porty: "
+"tcp:443, udp:3074, zakresy: tcp:5060-5061 (1-65534). DNS: dns:*teams*, "
+"wyrażenie regularne: dns:/zoom[0-9]+, tylko CNAME: dns_c:. IP: 1.1.1.1, "
+"ff01::1"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr "QoS włączono"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr "Tryb kolejki"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr "Szybkie dodawanie konfiguracji"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr "Szybkie dodawanie zasady"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr "Szybkie ustawienia"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr "Początek zakresu nie może przekraczać końca."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr "Błąd odczytu"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr "Odczytywanie i sprawdzanie poprawności plików..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr "Przeładuj"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+"Zastępuje oba pliki konfiguracyjne domyślnymi ustawieniami qosify, który "
+"zostanie wyłączony."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr "Resetowanie nie powiodło się: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr "Zresetować konfigurację qosify do wartości domyślnych?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr "Resetuj do ustawień domyślnych"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr "Przywróć ustawienia domyślne i zastosuj."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr "Restetuj do ustawień domyślnych qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr "Resetowanie"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "Restartuj"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr "Przywracanie ustawień domyślnych..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr "Zasady zapisane"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr "Zasady: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "Uruchomione"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr "Uruchomione i kształtuje"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr "Uruchomione — nie kształtuje"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+"Takie same opcje jak w przypadku klasy — nadaje istniejącej klasie drugą "
+"nazwę."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+"Opcje są takie same jak w przypadku interfejsu, ale nazwa to netdev. "
+"Domyślna wartość nat wynosi tutaj 0, a dla interfejsów to 1."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr "Zapisz i zastosuj"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr "Zapisywanie nie powiodło się: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr "Zapisywanie"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr "Zapisywanie ustawień i stosowanie..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr "Sekcja %s już istnieje."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+"Nazwa sekcji to nazwa klasy, do której odnoszą się zasady i wartości dscp_*. "
+"Wartość ustawia razem wejście i wyjście."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+"Wybierz pliki i kliknij „Zapisz i zastosuj”, aby nadpisać i ponownie "
+"uruchomić qosify."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr "Wysyłanie %s do qosify..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr "Sterowanie usługą"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "Status usługi"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr "Nieudana czynność usługi: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr "Ustawienia zapisano"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "Uruchom"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "Status"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "Zatrzymaj"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr "Kształtowanie ruchu i klasyfikacja DSCP za pomocą qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr "Konfiguracja UCI — klasy, interfejsy, wartości domyślne."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr "Nieznana klasa/cel DSCP: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr "Prześlij pliki konfiguracyjne"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr ""
+"Czy przesłać i nadpisać pliki konfiguracyjne? qosify zostanie przeładowany."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr "Błąd przesyłania: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr "Nie udało się przesłać: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr "Przesyłanie"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr "Prawidłowe"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+"Ostrzeżenie: %s, ale qosify nie kształtuje ruchu — sprawdź kartę „Status”."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr "Pracuje"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr "Zapisywanie konfiguracji i przeładowanie qosify..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr "Zapisywanie zasad i przeładowanie qosify..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr "Masz niezapisane zmiany. Opuścić tę kartę?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr "wzorzec dns"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr "wyrażenie regularne dns"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr "wzorzec dns_c"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr "wyrażenie regularne dns_c"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr "wejście i wyjście są równe 0 — nic nie jest kształtowane"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+"nazwa nie jest ustawiona — qosify.init wysyła pustą nazwę urządzenia, a ta "
+"sekcja nigdy nie jest stosowana"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+"name jest interfejsem netifd. bandwidth ma zastosowanie tylko wtedy, gdy "
+"bandwidth_up/bandwidth_down nie są ustawione. overhead i overhead_encap mają "
+"zastosowanie tylko wtedy, gdy overhead_type jest manualny."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+"nat nie jest wysyłany: qosify emituje nat/nonat tylko w gałęzi host_isolate. "
+"CAKE akceptuje przepływy i nat — umieść nat w opcjach, aby go zastosować"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+"nat dociera tylko do %s — umieść go w opcjach lub w ingress_options i "
+"egress_options"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr "brak bandwidth_down lub bandwidth — CAKE działa bez ograniczeń"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr "brak bandwidth_up lub bandwidth — wyjście CAKE działa bez ograniczeń"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr "tylko jeśli nieustawione (+)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+"overhead i overhead_encap są ignorowane, chyba że overhead_type jest manualny"
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr "qosify jest nieuruchomiony. Rozpocznij od karty „Przegląd”."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+"qosify przekazuje to tylko do CAKE razem z izolowaniem hosta — dodaj nat do "
+"„Opcji”, aby to wymusić"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr "qosify-status"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr "qosify-status nie zwrócił żadnego wyniku."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr "wymagane — qosify pomija sekcje bez nazwy"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr "zasady"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr "port tcp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr "port tcp+udp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr "port udp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr "używane tylko wtedy, gdy „Typ narzutu” jest manualny"
diff --git a/luci-app-qosify/po/pt/qosify.po b/luci-app-qosify/po/pt/qosify.po
new file mode 100644
index 000000000..10922719f
--- /dev/null
+++ b/luci-app-qosify/po/pt/qosify.po
@@ -0,0 +1,729 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: pt\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr ""
diff --git a/luci-app-qosify/po/pt_BR/qosify.po b/luci-app-qosify/po/pt_BR/qosify.po
new file mode 100644
index 000000000..43aa63776
--- /dev/null
+++ b/luci-app-qosify/po/pt_BR/qosify.po
@@ -0,0 +1,732 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: pt_BR\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Martin Olmer \n"
+"Language-Team: Portuguese (Brazil) \n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"PO-Revision-Date: 2026-09-02 07:51+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "Ativo"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "Adicionar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "Avançada"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "Limpar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "Status"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "Parar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr ""
diff --git a/luci-app-qosify/po/ru/qosify.po b/luci-app-qosify/po/ru/qosify.po
new file mode 100644
index 000000000..e2d14de44
--- /dev/null
+++ b/luci-app-qosify/po/ru/qosify.po
@@ -0,0 +1,785 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: ru\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: SnIPeRSnIPeR "
+"\n"
+"Language-Team: Russian \n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"PO-Revision-Date: 2026-08-28 22:28+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr "%s содержит кавычку — qosify отклоняет всё значение"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr "%s загружен, qosify перезапущен."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr "%s применено (QoS отключён)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr "%s применено."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr "(псевдоним)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr "(секция без названия)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr "Секция defaults конфигурации уже существует."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "Активно"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "Добавить"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "Дополнительно"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr "Автоподстройка входящего"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr "Автозапуск"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "Доступно"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr "Доступные классы"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr "Резервное копирование текущих файлов"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr "Входящая скорость"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr "Исходящая скорость"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr "Двоичное содержимое отклонено"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr "Правила классификации"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "Очистить"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+"Очистить редактор конфигурации? Содержимое не будет сохранено, пока вы не "
+"нажмёте «Сохранить»."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+"Очистить редактор правил? Содержимое не будет сохранено, пока вы не нажмёте "
+"«Сохранить»."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr "Нажмите, чтобы отключить автозапуск"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr "Нажмите, чтобы включить автозапуск"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr "Общие настройки шейпинга — записываются напрямую в %s, секция %s."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "Конфигурация"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr "Справка по конфигурации"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr "Конфигурация очищена, qosify остановлен."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr "Конфигурация сохранена"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr "Конфигурация: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr "Файлы конфигурации"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+"Кодовые точки DSCP: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Любое значение "
+"dscp_* также может задавать имя класса. Добавьте префикс «+», чтобы "
+"переопределять только когда поле DSCP равно нулю."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr "Правила сопоставления DSCP, загружаемые qosify при запуске."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+"Значения по умолчанию, которые qosify применяет при отсутствии ключа — "
+"interface: mode diffserv4, ingress 1, egress 1, nat 1, host_isolate 1, "
+"autorate_ingress 0. device: идентично, кроме nat 0. defaults: timeout 3600, "
+"dscp_default_tcp/udp CS0, dscp_prio/dscp_bulk/dscp_icmp не заданы, "
+"bulk_trigger_pps/bulk_trigger_timeout/prio_max_avg_pkt_len 0 (отключено)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "Отключено"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "Скачать"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr "Скачайте текущие файлы конфигурации перед внесением изменений."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+"Редакторы перезагружены с диска — несохранённые изменения в редакторе были "
+"отброшены."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr "Исходящий"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr "Параметры исходящего трафика"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr "Пустая конфигурация остановит qosify. Продолжить?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr "Пустой файл"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "Включено"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr "Включено — не запущено"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr "Включено — шейпинг не выполняется (проверьте конфигурацию)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr "Введите имя секции (буквы, цифры, подчёркивание)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr "Введите один IPv4-адрес (qosify не принимает CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr "Введите один IPv6-адрес (qosify не принимает CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr "Введите значение."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr "Ошибка: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr "Ошибка: не найдено допустимых секций конфигурации."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr "Ошибка: bandwidth_down должен выглядеть как 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr "Ошибка: bandwidth_up должен выглядеть как 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+"Ошибка: недопустимые символы в полях параметров. Используйте только буквы, "
+"цифры, пробелы, дефисы, точки и двоеточия."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr "Ошибка: имя должно быть именем устройства или интерфейса"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr "Ошибка: overhead должен быть целым числом байт"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr "Ошибки:"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr "Файл слишком большой (макс. 64 КБ)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr "Найден (пустой или некорректный)"
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr "Предоставить доступ к LuCI-приложению Qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr "Изоляция хостов"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr "IPv4-адрес"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr "IPv6-адрес"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr "Входящий"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr "Параметры входящего трафика"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr "Входящий: %s / Исходящий: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr "Скрипт инициализации"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr "Установлено"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr "Имя интерфейса"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr "Некорректная строка правила: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr "Отсутствует"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr "Имя сетевого устройства"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr "Классы не определены в /etc/config/qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+"Классы не определены. Сначала добавьте классы на вкладке «Конфигурация»."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr "Не задана секция defaults конфигурации"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr "Файлы не выбраны."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr "В шаблонах или адресах не допускаются пробелы и символ #."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr "Нет допустимых секций конфигурации UCI"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr "Не запущено"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr "Не установлено"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+"Примечание: полоса пропускания не задана — CAKE будет работать без "
+"ограничений в этом направлении."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr "Параметры"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr "Overhead (байты)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr "Тип overhead"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "Обзор"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr "Пакет"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr "Порт должен быть в диапазоне 1–65534 (qosify отклоняет 65535)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr "Порт должен быть числом или диапазоном (например, 4500 или 5060–5061)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+"Добавьте префикс «+» для переопределения только при нулевом поле DSCP. "
+"Порты: tcp:443, udp:3074, диапазоны: tcp:5060–5061 (1–65534). DNS: "
+"dns:*teams*, regex: dns:/zoom[0-9]+, только CNAME: dns_c:. IP: 1.1.1.1, "
+"ff01::1"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr "QoS включён"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr "Режим очереди"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr "Быстрое добавление конфигурации"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr "Быстрое добавление правила"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr "Быстрые настройки"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr "Начало диапазона не должно превышать конец."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr "Ошибка чтения"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr "Чтение и проверка файлов..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr "Перезагрузить"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+"Заменяет оба файла конфигурации значениями по умолчанию qosify; qosify будет "
+"отключён."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr "Ошибка сброса: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr "Сбросить конфигурацию qosify к значениям по умолчанию?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr "Сбросить к значениям по умолчанию"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr "Сброс к значениям по умолчанию выполнен."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr "Сброс к значениям по умолчанию qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr "Сброс"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "Перезапустить"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr "Восстановление значений по умолчанию..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr "Правила сохранены"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr "Правила: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "Запущено"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr "Работает и выполняет шейпинг"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr "Работает — шейпинг не выполняется"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+"Те же параметры, что и у класса — даёт существующему классу второе имя."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+"Те же параметры, что и у интерфейса, но имя — это netdev. Значение nat по "
+"умолчанию 0 здесь и 1 для интерфейсов."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr "Применить"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr "Ошибка сохранения: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr "Сохранение"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr "Сохранение настроек и применение..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr "Секция %s уже существует."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+"Имя секции — это имя класса, на которое ссылаются правила и значения dscp_*. "
+"Значение value задаёт ingress и egress вместе."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+"Выберите файлы и нажмите «Применить», чтобы перезаписать файлы и "
+"перезапустить qosify."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr "Отправка %s в qosify..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr "Управление службой"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "Состояние службы"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr "Действие со службой не удалось: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr "Настройки сохранены"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "Запустить"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "Статус"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "Остановить"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr "Шейпинг трафика и классификация DSCP с помощью qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr "Конфигурация UCI — классы, интерфейсы, значения по умолчанию."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr "Неизвестный класс/цель DSCP: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr "Загрузить файлы конфигурации"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr ""
+"Загрузить и перезаписать файлы конфигурации? Qosify будет перезагружен."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr "Ошибка загрузки: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr "Загрузка не удалась: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr "Загрузка"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr "Действителен"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+"Предупреждение: %s, но qosify не выполняет шейпинг трафика — проверьте "
+"вкладку «Состояние»."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr "Работает"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr "Запись конфигурации и перезагрузка qosify..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr "Запись правил и перезагрузка qosify..."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr "Есть несохранённые изменения. Покинуть эту вкладку?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr "шаблон dns"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr "регулярное выражение dns"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr "шаблон dns_c"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr "регулярное выражение dns_c"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr "ingress и egress равны 0 — шейпинг не выполняется"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+"name не задан — qosify.init отправляет пустое имя устройства, и эта секция "
+"никогда не применяется"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+"name — это интерфейс netifd. bandwidth применяется только там, где не заданы "
+"bandwidth_up/bandwidth_down. overhead и overhead_encap применяются только "
+"при overhead_type = manual."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+"nat не отправляется: qosify выдаёт nat/nonat только внутри ветки "
+"host_isolate. CAKE принимает flows вместе с nat — добавьте nat в параметры "
+"(options), чтобы применить его"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+"nat достигает только %s — поместите его в options или в оба параметра "
+"ingress_options и egress_options"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr ""
+"не задан bandwidth_down или bandwidth — входящий CAKE работает без "
+"ограничений"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr ""
+"не задан bandwidth_up или bandwidth — исходящий CAKE работает без ограничений"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr "только если не задано (+)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+"overhead и overhead_encap игнорируются, пока overhead_type не равен manual"
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr "Qosify не запущен. Запустите его на вкладке «Обзор»."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+"Qosify передаёт это в CAKE только вместе с Host Isolate — добавьте nat в "
+"параметры (options) для принудительного применения"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr "qosify-status"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr "qosify-status не вернул никаких данных."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr "обязательно — qosify пропускает секции без имени"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr "правила"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr "порт tcp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr "порт tcp+udp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr "порт udp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr "используется только при overhead_type = manual"
diff --git a/luci-app-qosify/po/sv/qosify.po b/luci-app-qosify/po/sv/qosify.po
new file mode 100644
index 000000000..12a3096f9
--- /dev/null
+++ b/luci-app-qosify/po/sv/qosify.po
@@ -0,0 +1,772 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: sv\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Daniel Nylander \n"
+"Language-Team: Swedish \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"PO-Revision-Date: 2026-09-10 20:09+0000\n"
+"X-Generator: Weblate 2026.9.1\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr "%s innehåller ett citattecken – qosify avvisar hela värdet"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr "%s har laddats upp, qosify har lästs in igen."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr "%s har tillämpats (QoS inaktiverat)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr "%s har tillämpats."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr "(alias)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr "(namnlöst avsnitt)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr "Ett konfigurationsavsnitt med standardvärden finns redan."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "Aktiv"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "Lägg till"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "Avancerat"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr "Automatisk hastighetsanpassning, ingående"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr "Starta automatiskt"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "Tillgänglig"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr "Tillgängliga klasser"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr "Säkerhetskopiera aktuella filer"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr "Bandbredd ned"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr "Bandbredd upp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr "Binärt innehåll avvisades"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr "Klassificeringsregler"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "Rensa"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+"Rensa konfigurationsredigeraren? Innehållet sparas inte förrän du klickar på "
+"Spara."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+"Rensa regelredigeraren? Innehållet sparas inte förrän du klickar på Spara."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr "Klicka för att inaktivera automatisk start"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr "Klicka för att aktivera automatisk start"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr ""
+"Vanliga inställningar för trafikstyrning – skrivs direkt till %s, avsnitt %s."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "Konfiguration"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr "Konfigurationsreferens"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr "Konfigurationen rensades, qosify stoppades."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr "Konfigurationen har sparats"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr "Konfiguration: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr "Konfigurationsfiler"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+"DSCP-kodpunkter: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Varje dscp_*-värde "
+"kan också namnge en klass. Inled med + för att endast åsidosätta när DSCP-"
+"fältet är noll."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr "Regler för DSCP-mappning som qosify läser in vid start."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+"Standardvärden som qosify använder när en nyckel saknas – interface: mode "
+"diffserv4, ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. "
+"device: samma utom nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp inte satta, bulk_trigger_pps/"
+"bulk_trigger_timeout/prio_max_avg_pkt_len 0 (inaktiverat)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "Inaktiverad"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "Hämta"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr "Hämta aktuella konfigurationsfiler innan du gör ändringar."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr "Redigerarna lästes in igen från disk – osparade ändringar förkastades."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr "Utgående"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr "Alternativ för utgående trafik"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr "Tom konfiguration stoppar qosify. Vill du fortsätta?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr "Tom fil"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "Aktiverad"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr "Aktiverad – körs inte"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr "Aktiverad – trafikstyr inte (kontrollera konfigurationen)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr "Ange ett avsnittsnamn (alfanumeriskt/understreck)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr "Ange en enskild IPv4-adress (qosify accepterar inte CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr "Ange en enskild IPv6-adress (qosify accepterar inte CIDR)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr "Ange ett värde."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr "Fel: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr "Fel: Inga giltiga konfigurationsblock hittades."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr "Fel: bandwidth_down måste se ut som 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr "Fel: bandwidth_up måste se ut som 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+"Fel: ogiltiga tecken i alternativfälten. Använd endast alfanumeriska tecken, "
+"blanksteg, bindestreck, punkter och kolon."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr "Fel: name måste vara ett enhets- eller gränssnittsnamn"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr "Fel: overhead måste vara ett helt antal byte"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr "Fel:"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr "Filen är för stor (högst 64 kB)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr "Hittades (tom eller ogiltig)"
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr "Ge åtkomst till LuCI-appen qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr "Isolera värdar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr "IPv4-adress"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr "IPv6-adress"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr "Ingress"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr "Alternativ för ingående trafik"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr "Ingående: %s / utgående: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr "Initieringsskript"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr "Installerad"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr "Gränssnittsnamn"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr "Ogiltig regelrad: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr "Saknas"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr "Netdev-namn"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr "Inga klasser definierade i /etc/config/qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+"Inga klasser definierade. Lägg först till klasser på fliken Konfiguration."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr "Inget konfigurationsavsnitt för standardvärden definierat"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr "Inga filer valda."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr "Blanksteg och # är inte tillåtna i mönster eller adresser."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr "Inga giltiga UCI-konfigurationsblock"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr "Körs inte"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr "Inte installerad"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+"Obs: bandbredd är inte angiven – CAKE körs obegränsat i den riktningen."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr "Alternativ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr "Overhead i byte"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr "Overhead-typ"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "Överblick"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr "Packet"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr "Porten måste vara 1–65534 (qosify avvisar 65535)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr ""
+"Porten måste vara ett tal eller intervall (t.ex. 4500 eller 5060–5061)."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+"Inled med + för att endast åsidosätta när DSCP-fältet är noll. Portar: "
+"tcp:443, udp:3074, intervall: tcp:5060–5061 (1–65534). DNS: dns:*teams*, "
+"reguljärt uttryck: dns:/zoom[0-9]+, endast CNAME: dns_c:. IP: 1.1.1.1, "
+"ff01::1"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr "QoS aktiverat"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr "Köläge"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr "Lägg snabbt till konfiguration"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr "Lägg snabbt till regel"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr "Snabbinställningar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr "Intervallets början får inte överstiga slutet."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr "Läsfel"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr "Läser och validerar filer …"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr "Ladda om"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+"Ersätter båda konfigurationsfilerna med qosifys standardvärden; qosify "
+"inaktiveras."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr "Återställningen misslyckades: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr "Vill du återställa qosify-konfigurationen till standardvärden?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr "Återställ till standardvärden"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr "Återställd till standardvärden, tillämpad."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr "Återställ till qosifys standardvärden"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr "Återställer"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "Starta om"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr "Återställer standardvärden …"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr "Reglerna har sparats"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr "Regler: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "Körs"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr "Kör och trafikstyr"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr "Kör – trafikstyr inte"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr "Samma alternativ som klass – ger en befintlig klass ett andra namn."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+"Samma alternativ som gränssnitt, men name är en netdev. nat är som standard "
+"0 här och 1 för gränssnitt."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr "Spara och tillämpa"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr "Det gick inte att spara: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr "Sparar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr "Sparar inställningarna och tillämpar …"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr "Avsnittet %s finns redan."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+"Avsnittsnamnet är klassnamnet som regler och dscp_*-värden refererar till. "
+"value anger ingående och utgående trafik tillsammans."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+"Välj filer och klicka på Spara och tillämpa för att skriva över och starta "
+"om qosify."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr "Skickar %s till qosify …"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr "Tjänstkontroller"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "Status för tjänsten"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr "Tjänståtgärden misslyckades: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr "Inställningarna har sparats"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "Starta"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "Status"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "Stoppa"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr "Trafikstyrning och DSCP-klassificering via qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr "UCI-konfiguration – klasser, gränssnitt, standardvärden."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr "Okänd klass/DSCP-mål: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr "Ladda upp konfigurationsfiler"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr "Ladda upp och skriv över konfigurationsfiler? qosify läses in igen."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr "Uppladdningsfel: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr "Uppladdningen misslyckades: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr "Laddar upp"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr "Giltig"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr "Varning: %s men qosify trafikstyr inte – kontrollera fliken Status."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr "Arbetar"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr "Skriver konfigurationen och läser in qosify igen …"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr "Skriver reglerna och läser in qosify igen …"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr "Du har osparade ändringar. Vill du lämna den här fliken?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr "dns-mönster"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr "dns-reguljärt uttryck"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr "dns_c-mönster"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr "dns_c-reguljärt uttryck"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr "ingress och egress är båda 0 – ingen trafik styrs"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+"name är inte angivet – qosify.init skickar ett tomt enhetsnamn och avsnittet "
+"tillämpas aldrig"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+"name är netifd-gränssnittet. bandwidth används endast när bandwidth_up/"
+"bandwidth_down inte är satta. overhead och overhead_encap används endast när "
+"overhead_type är manual."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+"nat skickas inte: qosify skickar endast nat/nonat inom grenen host_isolate. "
+"CAKE accepterar flows plus nat – lägg nat i options för att tillämpa det"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+"nat når endast %s – lägg det i options eller i både ingress_options och "
+"egress_options"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr "ingen bandwidth_down eller bandwidth – ingående CAKE körs obegränsat"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr "ingen bandwidth_up eller bandwidth – utgående CAKE körs obegränsat"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr "endast om inte satt (+)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr "overhead och overhead_encap ignoreras om inte overhead_type är manual"
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr "qosify körs inte. Starta från fliken Översikt."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+"qosify skickar detta till CAKE endast tillsammans med Isolera värdar – lägg "
+"till nat i Options för att tvinga det"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr "qosify-status"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr "qosify-status returnerade ingen utdata."
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr "krävs – qosify hoppar över avsnitt utan name"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr "regler"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr "port för TCP"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr "tcp+udp-port"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr "udp-port"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr "används endast när Overhead-typ är manual"
diff --git a/luci-app-qosify/po/ta/qosify.po b/luci-app-qosify/po/ta/qosify.po
new file mode 100644
index 000000000..9a185aa31
--- /dev/null
+++ b/luci-app-qosify/po/ta/qosify.po
@@ -0,0 +1,732 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: ta\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Pavel Borecki \n"
+"Language-Team: Tamil \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"PO-Revision-Date: 2026-08-23 19:31+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr "qosify-status"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr ""
diff --git a/luci-app-qosify/po/templates/qosify.pot b/luci-app-qosify/po/templates/qosify.pot
new file mode 100644
index 000000000..5b5e68bfc
--- /dev/null
+++ b/luci-app-qosify/po/templates/qosify.pot
@@ -0,0 +1,1365 @@
+msgid ""
+msgstr "Content-Type: text/plain; charset=UTF-8"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:874
+msgid "%d interface"
+msgid_plural "%d interfaces"
+msgstr[0] ""
+msgstr[1] ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:935
+msgid "%d rule"
+msgid_plural "%d rules"
+msgstr[0] ""
+msgstr[1] ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:934
+msgid "%d section"
+msgid_plural "%d sections"
+msgstr[0] ""
+msgstr[1] ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:674
+msgid "%s (current)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:692
+msgid "%s (from bandwidth)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2077
+msgid ""
+"%s and %s are replaced with the templates shipped by the qosify package, and "
+"shaping is left disabled."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1801
+msgid ""
+"%s came back empty although it is %d bytes on disk — refusing to overwrite it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2338
+msgid "%s changed on disk — your unsaved edits are still in the editor."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2241
+msgid ""
+"%s could not be read — the editor is left empty and will not be saved over "
+"it."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1825
+msgid ""
+"%s has changed since this editor was loaded. Saving now discards those "
+"changes."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1856
+msgid ""
+"%s is %d bytes on disk but was never loaded into the editor — refusing to "
+"truncate it. Reload the page first."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:356
+msgid "%s is a list in this section — edit %s directly"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1648
+msgid ""
+"%s is not in the defaults list — qosify does not load it, so the Rules tab "
+"has no effect"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:286
+msgid ""
+"%s is set to \"%s\" — qosify converts it with atoi(), so anything but a non-"
+"zero number means off"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:742
+msgid "%s quick settings"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2061
+msgid "%s uploaded, qosify reloaded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2095
+msgid "%s was not written: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2098
+msgid "%s was reset but %s was not written: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1937
+msgid "%s, applied (QoS disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1942
+msgid ""
+"%s, applied — qosify did not answer on ubus, so shaping could not be checked."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1939
+msgid "%s, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:460
+msgid "%s: %s \"%s\" is not a class, codepoint or 0-63 — qosify ignores it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:448
+msgid ""
+"%s: %s \"%s\" is not a class, codepoint or 0-63 — qosify rejects the whole "
+"config and interface changes are not applied"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:457
+msgid ""
+"%s: %s \"%s\" is not a codepoint or 0-63 — qosify drops this class and the "
+"rules using it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:441
+msgid ""
+"%s: %s cannot be checked — write it as one plain or fully quoted value, with "
+"no backslash"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:443
+msgid ""
+"%s: %s contains shell metacharacters — qosify runs the tc command with sh -c "
+"as root, so they would break it or be executed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1101
+msgid "(alias)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:427
+msgid "(this qosify build has no NQB)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2177
+msgid "A config defaults section already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2175
+msgid "A section name may only contain letters, digits and underscores."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:848
+msgid "Active"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:949
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Add"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:85
+msgid "Adds atm, noatm or ptm when overhead_type is manual"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:87
+msgid "Adds ether-vlan once per level (1 or 2)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:86
+msgid "Adds mpu "
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:84
+msgid "Adds overhead  when overhead_type is manual"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:600
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:729
+msgid "Advanced"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1862
+msgid "An empty %s stops all shaping. Continue?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:718
+msgid "Automatic download rate"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:885
+msgid "Autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:916
+msgid "Autostart state unknown — rpcd did not answer"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:875
+msgid "Available"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1188
+msgid "Backup"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:76
+msgid ""
+"Bandwidth for both directions, where bandwidth_up or bandwidth_down is not "
+"set"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:708
+msgid "Basic"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:224
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2021
+msgid "Binary content rejected"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:79
+msgid "CAKE diffserv mode"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:89
+msgid "CAKE egress options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:731
+msgid "CAKE egress options, space separated."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:88
+msgid "CAKE ingress options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:730
+msgid "CAKE ingress options, space separated."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:90
+msgid "CAKE options for ingress + egress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:83
+msgid ""
+"CAKE overhead keyword added to options; manual uses overhead and "
+"overhead_encap"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:723
+msgid "CAKE overhead keyword. Use none if unsure."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:556
+msgid "Cancel"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "Check Devices"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1185
+msgid "Choose file…"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1106
+msgid "Classes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1729
+msgid "Cleanup exited with code %d"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1982
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1988
+msgid "Clear"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1861
+msgid "Clear configuration"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1982
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1988
+msgid "Clear editor"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Click to disable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Click to enable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:732
+msgid "Common CAKE options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+msgid "Config"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1886
+msgid "Config cleared, qosify stopped."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1886
+msgid "Config cleared."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1907
+msgid "Config saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2034
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2042
+msgid "Config: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:558
+msgid "Continue"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1237
+msgid "Could not read %s: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:599
+msgid "Counters"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1396
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1619
+msgid "DNS Entries"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1356
+msgid "DNS Entries (%d of %d)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1356
+msgid "DNS Entries (%d)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1013
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF (NQB needs qosify "
+"from June 2026; 24.10 and 25.12 do not have it). A raw value from 0 to 63 is "
+"accepted too, and any dscp_* value may also name a class. Prefix with + to "
+"override only when the DSCP field is zero."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:63
+msgid "DSCP value for ICMP packets"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:65
+msgid "DSCP value for bulk-marked packets"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:71
+msgid "DSCP value for egress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:70
+msgid "DSCP value for ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:69
+msgid "DSCP value for ingress and egress, where they are not set"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:64
+msgid "DSCP value for priority-marked packets"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1013
+msgid "DSCP values"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:61
+msgid "Default DSCP value for TCP packets"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+msgid "Default DSCP value for UDP packets"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:60
+msgid "Default timeout for dynamically added entries"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:710
+msgid "Device"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1703
+msgid "Device check done."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1706
+msgid "Device check failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:851
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:873
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Disabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:75
+msgid "Downlink bandwidth (same format as tc)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:712
+msgid "Downlink bandwidth, same format as tc. Set just below line speed."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1190
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1239
+msgid "Download"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:712
+msgid "Download bandwidth"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:716
+msgid "Download shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1189
+msgid "Download the current files from the router."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1158
+msgid ""
+"Each line has two whitespace separated fields, match and dscp. dscp can be a "
+"raw value, a codepoint like CS0, or a class name. DNS entries are compared "
+"in the order in which they are specified, using the first matching entry."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:731
+msgid "Egress CAKE options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2012
+msgid "Empty file"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1982
+msgid ""
+"Empty the config editor? Nothing is written until you click Save & Apply."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1988
+msgid ""
+"Empty the rules editor? Nothing is written until you click Save & Apply."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:80
+msgid "Enable CAKE NAT host detection via conntrack"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:82
+msgid "Enable CAKE automatic rate estimation for ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:81
+msgid "Enable CAKE host isolation"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:78
+msgid "Enable egress shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:77
+msgid "Enable ingress shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:873
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:850
+msgid "Enabled — Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:849
+msgid "Enabled — Not Shaping (check config)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:703
+msgid "Encapsulation overhead"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2174
+msgid "Enter a section name."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2148
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2151
+msgid ""
+"Enter a single IPv6 address (qosify does not accept CIDR or a %zone suffix)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2133
+msgid "Enter a value."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:933
+msgid "Entries"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1840
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1953
+msgid "Error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1837
+msgid "Error: No valid config stanzas found."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1755
+msgid ""
+"Error: invalid characters in bandwidth or options fields. Use alphanumeric, "
+"spaces, hyphens, dots, colons only."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1790
+msgid "Error: name must be a device or interface name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1761
+msgid "Error: overhead must be a whole number of bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1760
+msgid "Error: overhead_mpu must be a whole number of bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2062
+msgid "Errors:"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:933
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1190
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1197
+msgid "File"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1824
+msgid "File changed on disk"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2013
+msgid "File too large (max 64KB)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:659
+msgid "Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:930
+msgid "Found (empty or invalid)"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:720
+msgid "Host isolation"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1161
+msgid "IPv4 address, e.g. 1.1.1.1"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1162
+msgid "IPv6 address, e.g. ff01::1"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:730
+msgid "Ingress CAKE options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:710
+msgid "Interface"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Invalid CAKE options can stop qosify starting."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1165
+msgid "Like dns:... but only matches cname entries"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:228
+msgid ""
+"Line %d is longer than 1023 characters — the rule loader reads fixed-size "
+"lines and would split it, comment included"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:59
+msgid "List of files with port/IP/host mappings"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1394
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1396
+msgid "Loading..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Maintenance"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:702
+msgid "Manual overhead"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1158
+msgid "Mapping file syntax"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1684
+msgid "Mapping files reloaded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:426
+msgid ""
+"Match not loaded by qosify on line %s — ports 1-65534, one IPv4/IPv6 address "
+"(no CIDR or %%zone), or dns:, dns_c:"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:66
+msgid "Maximum average packet length for marking a flow as priority"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:726
+msgid "Minimum packet unit (MPU)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:875
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:930
+msgid "Missing"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:933
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1190
+msgid "Modified"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:425
+msgid ""
+"More than two fields on line %s — qosify reads the rest of the line as the "
+"DSCP value and skips it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:719
+msgid "NAT awareness"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:710
+msgid "Netdev to enable QoS on. Required."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1577
+msgid "No CAKE tin statistics."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1620
+msgid "No DNS entries."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:424
+msgid "No DSCP target on line %s — qosify skips single-field lines"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No classes defined in %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2134
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1183
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1184
+msgid "No file selected"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1998
+msgid "No files selected."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1577
+msgid "No output."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1560
+msgid "No per-class counters."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2146
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2022
+msgid "No valid UCI config stanzas"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:862
+msgid "Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1763
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:67
+msgid "Number of packets per second to trigger bulk flow detection"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1153
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1166
+msgid "Only override the DSCP value if it is zero"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:696
+msgid ""
+"Only sent with host isolation on — add nat to common CAKE options to force it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1009
+msgid "Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:722
+msgid "Overhead"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:723
+msgid "Overhead preset"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:595
+msgid "Overview"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1826
+msgid "Overwrite"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1999
+msgid "Overwrite config files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1164
+msgid ""
+"POSIX.2 extended regular expression for matching hostnames. Only works if "
+"dns lookups are passed to qosify via the add_dns_host ubus call."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2142
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2138
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2141
+msgid "Port must be a number or a range (4500, 5060-5061)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:709
+msgid "QoS Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:713
+msgid "Queueing mode"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1155
+msgid "Quick Add"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1008
+msgid "Quick Add: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2145
+msgid "Range start must not exceed end."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Re-check devices"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1699
+msgid "Re-checking devices..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Re-read the mapping files only"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1682
+msgid "Re-reading the mapping files..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1207
+msgid ""
+"Re-runs the daemon's own device pass: every shaped section is looked up "
+"again, one whose device now exists is started and one whose device has gone "
+"is stopped. Nothing is reported back by the call — the result shows in the "
+"Service table on the Overview tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2016
+msgid "Read error"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2027
+msgid "Reading and validating files..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1673
+msgid "Reading tc output..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1011
+msgid "Reference"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:757
+msgid "Reload"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:757
+msgid "Reload Rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1687
+msgid "Reload failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1577
+msgid "Requires write access."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2078
+msgid "Reset"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2112
+msgid "Reset failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2076
+msgid "Reset to defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2108
+msgid "Reset to defaults, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2087
+msgid "Resetting"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:756
+msgid "Restart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1195
+msgid "Restore"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1210
+msgid "Restore qosify defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2087
+msgid "Restoring defaults..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+msgid "Rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1968
+msgid "Rules saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2045
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2048
+msgid "Rules: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:860
+msgid "Running & Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:861
+msgid "Running — Not Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:859
+msgid "Running — Shaping Unknown"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:745
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Save & Apply"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1817
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1889
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1916
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1977
+msgid "Save failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1795
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1898
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1964
+msgid "Saving"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1795
+msgid "Saving settings and applying..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2178
+msgid "Section %s already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1003
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. qosify."
+"init reads config alias the same way as config class."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1716
+msgid "Sending %s to qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:703
+msgid "Sent to CAKE as atm, noatm or ptm."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:727
+msgid "Sent to CAKE as ether-vlan, once per tag."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:726
+msgid "Sent to CAKE as mpu, in bytes."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:702
+msgid "Sent to CAKE as overhead, in bytes."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:655
+msgid "Service"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1737
+msgid "Service action failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1809
+msgid "Settings saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:715
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:886
+msgid "Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:933
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1190
+msgid "Size"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:73
+msgid "Skip this section"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:756
+msgid "Start"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:598
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:883
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:933
+msgid "Status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:847
+msgid "Status Unknown"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:758
+msgid "Stop"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1159
+msgid "TCP single port, or range from  to "
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1672
+msgid "The detailed tc output needs write access to this page."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:582
+msgid ""
+"The qosify UCI configuration could not be loaded — class and interface lists "
+"may be incomplete."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1196
+msgid ""
+"The selected files replace the ones on the router and qosify is reloaded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2000
+msgid ""
+"The selected files replace the ones on the router and qosify is reloaded. "
+"Download a backup from this tab first if you need one."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Time below bulk_trigger_pps threshold until a bulk flow mark is removed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1394
+msgid "Traffic by CAKE Tin"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1393
+msgid "Traffic by Class"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:588
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1160
+msgid "UDP single port, or range from  to "
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:856
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:858
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid "Unknown"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:427
+msgid "Unknown class/DSCP target: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:709
+msgid "Unticked sets disabled 1 and qosify skips this section."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:74
+msgid "Uplink bandwidth (same format as tc)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:711
+msgid "Uplink bandwidth, same format as tc. Set just below line speed."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1197
+msgid "Upload"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1202
+msgid "Upload & Apply"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2001
+msgid "Upload and apply"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:711
+msgid "Upload bandwidth"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2053
+msgid "Upload error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2070
+msgid "Upload failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:717
+msgid "Upload shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2027
+msgid "Uploading"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:884
+msgid "Uptime"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:727
+msgid "VLAN tags"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:930
+msgid "Valid"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1943
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1682
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1699
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1716
+msgid "Working"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1862
+msgid "Write empty file"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1898
+msgid "Writing config and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1964
+msgid "Writing rules and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:625
+msgid ""
+"You have read-only access to this page, so editing and service control are "
+"disabled."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:893
+msgid "active"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1759
+msgid ""
+"bandwidth_down does not look like a tc rate (100mbit, 12MBps, unlimited) — "
+"passing it through anyway"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1758
+msgid ""
+"bandwidth_up does not look like a tc rate (100mbit, 12MBps, unlimited) — "
+"passing it through anyway"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1004
+msgid ""
+"config device takes the same options, with name set to a netdev. nat "
+"defaults to 1 for interfaces and 0 for devices."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:892
+msgid "device %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:894
+msgid "device: %s, ingress: %s, egress: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:289
+msgid ""
+"disabled is set to \"%s\" — config_get_bool does not recognise that, so the "
+"section stays enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:692
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:710
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:730
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:731
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:732
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1152
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:2121
+msgid "e.g. %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1163
+msgid "fnmatch() pattern supporting * and ? as wildcard characters"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1605
+msgid "get_stats did not answer."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:893
+msgid "inactive"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1101
+msgid "ingress %s, egress %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:282
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:892
+msgid "interface %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1147
+msgid "match"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:274
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:278
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:279
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:72
+msgid ""
+"netifd interface (config interface) or netdev (config device) to enable QoS "
+"on"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:710
+msgid "netifd interface to enable QoS on. Required."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:894
+msgid "no"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:284
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:283
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:874
+msgid "none"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:281
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:587
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:839
+msgid ""
+"qosify did not appear on ubus, so its config was not applied — press Reload"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:825
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1721
+msgid "qosify did not come up — check the system log"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1602
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1664
+msgid "qosify is not running. Start from the Overview tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1727
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1879
+msgid "qosify is still running — leaving the qdiscs alone"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "qosify-status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1674
+msgid "qosify-status returned no output."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:867
+msgid ""
+"rpcd is not answering for qosify — check the ACL in /usr/share/rpcd/acl.d "
+"and restart rpcd"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:824
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1601
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1663
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1720
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1726
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1878
+msgid "rpcd is not answering for qosify, so the service state is unknown."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:947
+msgid "section name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:945
+msgid "section type"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1521
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1525
+msgid "share"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1533
+msgid "total"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1152
+msgid "value"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:894
+msgid "yes"
+msgstr ""
diff --git a/luci-app-qosify/po/tr/qosify.po b/luci-app-qosify/po/tr/qosify.po
new file mode 100644
index 000000000..cbe8b4097
--- /dev/null
+++ b/luci-app-qosify/po/tr/qosify.po
@@ -0,0 +1,732 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: tr\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Martin Olmer \n"
+"Language-Team: Turkish \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"PO-Revision-Date: 2026-08-31 20:56+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "Gelişmiş"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "Kullanılabilir"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "Temizle"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "Devre dışı"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "İndir"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "Etkinleştirildi"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "Genel bakış"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "Yeniden Başlat"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "Çalışıyor"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "Hizmet Durumu"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "Başlat"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "Durum"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "Durdur"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr ""
diff --git a/luci-app-qosify/po/uk/qosify.po b/luci-app-qosify/po/uk/qosify.po
new file mode 100644
index 000000000..49b084971
--- /dev/null
+++ b/luci-app-qosify/po/uk/qosify.po
@@ -0,0 +1,733 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: uk\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Martin Olmer \n"
+"Language-Team: Ukrainian \n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"PO-Revision-Date: 2026-08-31 06:59+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "Активний"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "Додати"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "Розширені"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "Доступно"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "Очистити"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "Конфігурація"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "Вимкнено"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "Завантажити"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr "Вихід"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "Увімкнено"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr "Інстальовано"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr "Опції"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "Огляд"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr "пакет"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr "Перезавантажити"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "Перезапустити"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "Запущено"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "Стан служби"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "Почати"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "Стан"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "Зупинити"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr ""
diff --git a/luci-app-qosify/po/zh-cn b/luci-app-qosify/po/zh-cn
new file mode 120000
index 000000000..8d69574dd
--- /dev/null
+++ b/luci-app-qosify/po/zh-cn
@@ -0,0 +1 @@
+zh_Hans
\ No newline at end of file
diff --git a/luci-app-qosify/po/zh_Hans/qosify.po b/luci-app-qosify/po/zh_Hans/qosify.po
new file mode 100644
index 000000000..4b8abba4d
--- /dev/null
+++ b/luci-app-qosify/po/zh_Hans/qosify.po
@@ -0,0 +1,750 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: zh_Hans\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Hosted Weblate user 54392 "
+"\n"
+"Language-Team: Chinese (Simplified Han script) \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"PO-Revision-Date: 2026-08-17 17:59+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr "%s 包含一个引号 — qosify 拒绝整个值"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr "%s 已上传, qosify 已重载。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr "%s, 已应用 (已禁用 QoS)。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr "%s, 已应用。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr "(别名)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr "(未命名部分)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr "已存在一个配置默认部分。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "活动"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "添加"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "高级"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr "入站自动速率"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr "自启动"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "可用"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr "可用类"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr "备份当前文件"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr "下行带宽"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr "上行带宽"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr "拒绝了二进制内容"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr "分类规则"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "清除"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr "清空配置编辑器?单击保存前不会保存内容。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr "清空规则编辑器?单击保存前不会保存内容。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr "单击以禁用自启动"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr "单击启用自启动"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr "常见流量整形设置 — 直接写入 %s,%s 部分。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "配置"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr "配置参考"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr "已清空配置,qosify 已停止。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr "配置已保存"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr "配置:%s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr "配置文件"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+"DSCP 代码点:CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF。任何 dscp_* 值都可命名"
+"一个类。仅在 DSCP 字段为 0 时在前面加 + 符号使其失效。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr "qosify 启动时加载的 DSCP 映射规则。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+"没有密钥时 qosify 应用的默认设置 — 接口:mode diffserv4, ingress 1, egress "
+"1, nat 1, host_isolate 1, autorate_ingress 0。设备:相同,除了 nat 0。默认: "
+"timeout 3600, dscp_default_tcp/udp CS0, dscp_prio/dscp_bulk/dscp_icmp unset, "
+"bulk_trigger_pps/bulk_trigger_timeout/prio_max_avg_pkt_len 0(禁用)。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "已禁用"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "下载"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr "做改动前下载当前配置文件。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr "从磁盘重新加载了编辑器 — 未保存的编辑器更改被丢弃。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr "出站"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr "出站选项"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr "空的配置会让 qosify 停止。继续吗?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr "空文件"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "已启用"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr "已启用 — 未运行"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr "已启用 — 未在整形流量(检查配置)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr "输入部分名称(字母数字/下划线)。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr "输入单个 IPv4 地址(qosify 不接受 CIDR)。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr "输入单个 IPv6 地址(qosify 不接受 CIDR)。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr "输入一个值。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr "错误:%s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr "错误:未找到哟小的配置段。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr "错误:bandwidth_down 看起来必须像是 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr "错误:bandwidth_up 必须看起来像是 100mbit"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr "错误:选项字段中有无效字符。只使用字母数字、空格、连字符、点、冒号。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr "错误:名称必须是设备或接口名"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr "错误:开销必须是整数字节"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr "错误:"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr "文件太大(最大 64KB)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr "已找到(空或无效)"
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr "授予对 LuCI 应用 qosify 的访问权"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr "主机隔离"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr "IPv4 地址"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr "IPv6 地址"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr "入站"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr "入站选项"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr "入站:%s / 出站:%s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr "初始化脚本"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr "已安装"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr "接口名"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr "无效的规则行:%s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr "缺失"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr "Netdev 名称"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr "/etc/config/qosify 中未定义类"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr "未定义类。先在配置标签页中添加类。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr "未定义配置默认值部分"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr "未选中文件。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr "模式或地址中不允许空格或 # 。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr "无有效的 UCI 配置段"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr "未在运行"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr "未安装"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr "注:未设置带宽 — CAKE 在那个方向上运行不受限。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr "选项"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr "开销字节"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr "开销类型"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "概览"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Package"
+msgstr "软件包"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr "端口必须是 1-65534(qosify 拒绝 65535)。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr "端口须为一个数字或范围(如 4500 或 5060-5061)。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+"只在 DSCP 字段为 0 时在前面加 + 符号来让值失效。端口: tcp:443, udp:3074, 范"
+"围: tcp:5060-5061 (1-65534)。DNS: dns:*teams*, regex: dns:/zoom[0-9]+, "
+"CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr "QoS 已启用"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr "队列模式"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr "快速添加配置"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr "快速添加规则"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr "快速设置"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr "范围开头不能超过结尾。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr "读取错误"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr "正在读取并验证文件…"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr "重新加载"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr "用 qosify 默认设置替换两个配置文件,qosify 将被禁用。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr "重置失败:%s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr "重置 qosify 配置为默认值?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr "重置为默认值"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr "重置为默认值,已应用。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr "重置为 qosify 默认值"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr "正在重置"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "重启"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr "正在恢复默认值…"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr "规则已保存"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr "规则:%s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "运行中"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr "正在运行和流量整形"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr "运行中 — 未在整形流量"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr "和类的选项相同 — 给现有类第二个名称。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+"和接口选项相同,但名称是一个 netdev. nat (此处默认值 0,接口默认值为 1)。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr "保存并应用"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr "保存失败:%s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr "正在保存"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr "正在保存设置并应用…"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr "%s 部分已存在。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr "部分名称是规则和 dscp_* 值指向的类名。值同时设置入站和出站。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr "选择文件并单击“保存并应用”以覆盖并重启 qosify。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr "正在发送 %s 到 qosify…"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr "服务控制"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "服务状态"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr "服务操作失败:%s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr "已保存设置"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "启动"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "状态"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "停止"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr "通过 qosify 进行流量整形和 DSCP 分类"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr "UCI 配置 — 类、接口、默认值。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr "未知类/DSCP 目标:%s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr "上传配置文件"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr "上传并覆盖配置文件?qosify 将重新加载。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr "上传错误:%s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr "上传失败了:%s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr "上传中"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr "有效"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr "警告:%s 但 qosify 未在整形流量 — 检查状态标签页。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr "工作中"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr "正在写入配置并重新加载 qosify…"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr "正在写入规则并重新加载 qosify…"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr "有未保存的更改。离开此标签页?"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr "dns 模式"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr "dns 正则表达式"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr "dns_c 模式"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr "dns_c 正则表达式"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr "出入站均为 0 — 没有流量被整形"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr "名称未设置 — qosify.init 发送空的设备名,此部分从未应用"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+"名称是 netfid 接口。带宽只应用于未设置 bandwidth_up/bandwidth_down 值的地方。"
+"overhead 和 overhead_encap 只在 overhead_type 为手动时才应用。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+"nat 未发送:qosify 只在 host_isolate 分支内发出 nat/nonat。CAKE 不接受流加 "
+"nat — 将 nat 放在 options 中来应用它"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+"nat 只达到 %s — 将它放在 options 中,或者放在 ingress_options 和 "
+"egress_options 中"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr "没有 bandwidth_down 或 bandwidth — 入站 CAKE 运行不受限"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr "没有 bandwidth_up 或 bandwidth — 出站 CAKE 运行不受限"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr "仅未设置(+)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr "除非 overhead_type 为手动,否则忽略 overhead 和 overhead_encap"
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr "qosify 未在运行。从概况标签页启动。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+"qosify 仅将这个和 Host Isolate 一起传递给 CAKE — 添加 nat 到 Options 来强制它"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr "qosify-status"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr "qosify-status 未返回输出。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr "必需 — qosify 跳过没有名称的部分"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr "规则"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr "tcp 端口"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr "tcp+udp 端口"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr "udp 端口"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr "只在开销类型是手动时使用"
diff --git a/luci-app-qosify/po/zh_Hant/qosify.po b/luci-app-qosify/po/zh_Hant/qosify.po
new file mode 100644
index 000000000..e18bd9fc3
--- /dev/null
+++ b/luci-app-qosify/po/zh_Hant/qosify.po
@@ -0,0 +1,733 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: zh_Hant\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: 為什麼不加空格 \n"
+"Language-Team: Chinese (Traditional Han script) \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"PO-Revision-Date: 2026-09-02 20:59+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:121
+msgid "%s contains a quote — qosify rejects the whole value"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1138
+msgid "%s uploaded, qosify reloaded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1038
+msgid "%s, applied (QoS disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1040
+msgid "%s, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:43
+msgid "(alias)"
+msgstr "(別名)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "(unnamed section)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1244
+msgid "A config defaults section already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:451
+msgid "Active"
+msgstr "活動"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:512
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:773
+msgid "Add"
+msgstr "新增"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Advanced"
+msgstr "進階"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:392
+msgid "Autorate Ingress"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Available"
+msgstr "可用"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:740
+msgid "Available Classes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:799
+msgid "Backup Current Files"
+msgstr "備份當前檔案"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:372
+msgid "Bandwidth Down"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:371
+msgid "Bandwidth Up"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:62
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1098
+msgid "Binary content rejected"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:733
+msgid "Classification Rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:596
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:785
+msgid "Clear"
+msgstr "清除"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1072
+msgid "Clear config editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1076
+msgid "Clear rules editor? Content will not be saved until you click Save."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to disable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:408
+msgid "Click to enable autostart"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:338
+msgid "Common shaping settings — written straight to %s, section %s."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:497
+msgid "Config"
+msgstr "配置"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:567
+msgid "Config Reference"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:995
+msgid "Config cleared, qosify stopped."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1014
+msgid "Config saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1110
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1118
+msgid "Config: %s"
+msgstr "配置: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:326
+msgid "Configuration Files"
+msgstr "配置檔案"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:581
+msgid ""
+"DSCP codepoints: CS0–CS7, AF11–AF43, EF, VA, NQB, LE, DF. Any dscp_* value "
+"may also name a class. Prefix with + to override only when the DSCP field is "
+"zero."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:734
+msgid "DSCP mapping rules loaded by qosify on startup."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:583
+msgid ""
+"Defaults qosify applies when a key is absent — interface: mode diffserv4, "
+"ingress 1, egress 1, nat 1, host_isolate 1, autorate_ingress 0. device: "
+"identical except nat 0. defaults: timeout 3600, dscp_default_tcp/udp CS0, "
+"dscp_prio/dscp_bulk/dscp_icmp unset, bulk_trigger_pps/bulk_trigger_timeout/"
+"prio_max_avg_pkt_len 0 (disabled)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:454
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Disabled"
+msgstr "已停用"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:846
+msgid "Download"
+msgstr "下載"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:800
+msgid "Download current config files before making changes."
+msgstr "在做出變更前下載當前配置檔案。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1352
+msgid "Editors reloaded from disk — unsaved editor changes were discarded."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:378
+msgid "Egress"
+msgstr "傳出"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:394
+msgid "Egress Options"
+msgstr "傳出選項"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:982
+msgid "Empty config will stop qosify. Continue?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1089
+msgid "Empty file"
+msgstr "空檔案"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:410
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:465
+msgid "Enabled"
+msgstr "已啟用"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:453
+msgid "Enabled — Not Running"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:452
+msgid "Enabled — Not Shaping (check config)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1242
+msgid "Enter a section name (alphanumeric/underscore)."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1209
+msgid "Enter a single IPv4 address (qosify does not accept CIDR)."
+msgstr "輸入單個 IPv4 位址 (qosify 不接受 CIDR)。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1211
+msgid "Enter a single IPv6 address (qosify does not accept CIDR)."
+msgstr "輸入單個 IPv6 位址 (qosify 不接受 CIDR)。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1198
+msgid "Enter a value."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1051
+msgid "Error: %s"
+msgstr "錯誤: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1002
+msgid "Error: No valid config stanzas found."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:921
+msgid "Error: bandwidth_down must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:920
+msgid "Error: bandwidth_up must look like 100mbit"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:917
+msgid ""
+"Error: invalid characters in options fields. Use alphanumeric, spaces, "
+"hyphens, dots, colons only."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:948
+msgid "Error: name must be a device or interface name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:922
+msgid "Error: overhead must be a whole number of bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1139
+msgid "Errors:"
+msgstr "錯誤:"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1090
+msgid "File too large (max 64KB)"
+msgstr "檔案過大 (最大 64KB)"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:483
+msgid "Found (empty or invalid)"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json:3
+msgid "Grant access to LuCI app qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:391
+msgid "Host Isolate"
+msgstr "主機隔離"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv4 address"
+msgstr "IPv4 位址"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "IPv6 address"
+msgstr "IPv6 位址"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:377
+msgid "Ingress"
+msgstr "傳入"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:393
+msgid "Ingress Options"
+msgstr "傳入選項"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:44
+msgid "Ingress: %s / Egress: %s"
+msgstr "傳入: %s / 傳出: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+msgid "Init Script"
+msgstr "初始化指令碼"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Installed"
+msgstr "已安裝"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Interface Name"
+msgstr "介面名稱"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:68
+msgid "Invalid rule line: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:464
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:484
+msgid "Missing"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:390
+msgid "NAT"
+msgstr "NAT"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:368
+msgid "Netdev Name"
+msgstr "Netdev 名稱"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:714
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:751
+msgid "No classes defined in /etc/config/qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1199
+msgid "No classes defined. Add classes in the Config tab first."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:654
+msgid "No config defaults section defined"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1084
+msgid "No files selected."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1206
+msgid "No spaces or # allowed in patterns or addresses."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1099
+msgid "No valid UCI config stanzas"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:469
+msgid "Not Running"
+msgstr "未執行"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+msgid "Not installed"
+msgstr "未安裝"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:924
+msgid "Note: bandwidth not set — CAKE will run unlimited on that direction."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:395
+msgid "Options"
+msgstr "選項"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:374
+msgid "Overhead Bytes"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:373
+msgid "Overhead Type"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Overview"
+msgstr "概覽"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:463
+#, fuzzy
+msgid "Package"
+msgstr "套件"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1204
+msgid "Port must be 1-65534 (qosify rejects 65535)."
+msgstr "連接埠必須為 1-65534 (qosify 不接受 65535)。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1201
+msgid "Port must be a number or range (e.g. 4500 or 5060-5061)."
+msgstr "連接埠必須為數字或範圍 (例如 4500 或 5060-5061)。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:755
+msgid ""
+"Prefix with + to override only when the DSCP field is zero. Ports: tcp:443, "
+"udp:3074, ranges: tcp:5060-5061 (1-65534). DNS: dns:*teams*, regex: dns:/"
+"zoom[0-9]+, CNAME-only: dns_c:. IP: 1.1.1.1, ff01::1"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:358
+msgid "QoS Enabled"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:376
+msgid "Queue Mode"
+msgstr "佇列模式"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:505
+msgid "Quick Add Config"
+msgstr "快速新增配置"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:760
+msgid "Quick Add Rule"
+msgstr "快速新增規則"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:336
+msgid "Quick Settings"
+msgstr "快速設定"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1205
+msgid "Range start must not exceed end."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1093
+msgid "Read error"
+msgstr "讀取錯誤"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Reading and validating files..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Reload"
+msgstr "重新載入"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:829
+msgid ""
+"Replaces both config files with qosify defaults, qosify will be disabled."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1178
+msgid "Reset failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1154
+msgid "Reset qosify config to defaults?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:831
+msgid "Reset to Defaults"
+msgstr "重設為預設值"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1174
+msgid "Reset to defaults, applied."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:828
+msgid "Reset to qosify Defaults"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Resetting"
+msgstr "重設中"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Restart"
+msgstr "重新啟動"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1156
+msgid "Restoring defaults..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1058
+msgid "Rules saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1121
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1125
+msgid "Rules: %s"
+msgstr "規則: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:470
+msgid "Running"
+msgstr "執行中"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:467
+msgid "Running & Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:468
+msgid "Running — Not Shaping"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:607
+msgid "Same options as class — gives an existing class a second name."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:609
+msgid ""
+"Same options as interface, but name is a netdev. nat defaults to 0 here and "
+"to 1 for interfaces."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:398
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:597
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:786
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:822
+msgid "Save & Apply"
+msgstr "儲存並套用"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:972
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:998
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1023
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1067
+msgid "Save failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Saving"
+msgstr "儲存中"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:953
+msgid "Saving settings and applying..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1245
+msgid "Section %s already exists."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:606
+msgid ""
+"Section name is the class name that rules and dscp_* values refer to. value "
+"sets ingress and egress together."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:809
+msgid "Select files and click Save & Apply to overwrite and restart qosify."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Sending %s to qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:404
+msgid "Service Controls"
+msgstr "服務控制"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:322
+msgid "Service Status"
+msgstr "服務狀態"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:900
+msgid "Service action failed: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:964
+msgid "Settings saved"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Start"
+msgstr "啟動"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:241
+msgid "Status"
+msgstr "狀態"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:415
+msgid "Stop"
+msgstr "停止"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:238
+msgid "Traffic shaping and DSCP classification via qosify"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:498
+msgid "UCI configuration — classes, interfaces, defaults."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:184
+msgid "Unknown class/DSCP target: %s"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:808
+msgid "Upload Config Files"
+msgstr "上傳配置檔案"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1085
+msgid "Upload and overwrite config files? qosify will reload."
+msgstr "上傳並覆寫配置檔案?qosify 將會重新載入。"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1130
+msgid "Upload error: %s"
+msgstr "上傳錯誤: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1148
+msgid "Upload failed: %s"
+msgstr "上傳失敗: %s"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1103
+msgid "Uploading"
+msgstr "上傳中"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:482
+msgid "Valid"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1041
+msgid "Warning: %s but qosify is not shaping traffic — check the Status tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:889
+msgid "Working"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1005
+msgid "Writing config and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:1054
+msgid "Writing rules and reloading qosify..."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:273
+msgid "You have unsaved changes. Leave this tab?"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c pattern"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "dns_c regex"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:117
+msgid "ingress and egress are both 0 — nothing is shaped"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:109
+msgid ""
+"name is not set — qosify.init sends an empty device name and this section is "
+"never applied"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:608
+msgid ""
+"name is the netifd interface. bandwidth applies only where bandwidth_up/"
+"bandwidth_down are unset. overhead and overhead_encap apply only when "
+"overhead_type is manual."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:113
+msgid ""
+"nat is not sent: qosify only emits nat/nonat inside the host_isolate branch. "
+"CAKE does accept flows plus nat — put nat in options to apply it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:114
+msgid ""
+"nat only reaches %s — put it in options, or in both ingress_options and "
+"egress_options"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:119
+msgid "no bandwidth_down or bandwidth — ingress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:118
+msgid "no bandwidth_up or bandwidth — egress CAKE runs unlimited"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:772
+msgid "only if unset (+)"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:116
+msgid "overhead and overhead_encap are ignored unless overhead_type is manual"
+msgstr ""
+
+#: applications/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json:3
+msgid "qosify"
+msgstr "qosify"
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:876
+msgid "qosify is not running. Start from the Overview tab."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:384
+msgid ""
+"qosify only passes this to CAKE together with Host Isolate — add nat to "
+"Options to force it"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:852
+msgid "qosify-status"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:878
+msgid "qosify-status returned no output."
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:370
+msgid "required — qosify skips sections with no name"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:489
+msgid "rules"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "tcp+udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:763
+msgid "udp port"
+msgstr ""
+
+#: applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js:375
+msgid "used only when Overhead Type is manual"
+msgstr ""
diff --git a/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json b/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json
new file mode 100644
index 000000000..988f80e95
--- /dev/null
+++ b/luci-app-qosify/root/usr/share/luci/menu.d/luci-app-qosify.json
@@ -0,0 +1,14 @@
+{
+	"admin/network/qosify": {
+		"title": "qosify",
+		"order": 90,
+		"action": {
+			"type": "view",
+			"path": "qosify/main"
+		},
+		"depends": {
+			"acl": [ "luci-app-qosify" ],
+			"fs": { "/usr/sbin/qosify": "executable" }
+		}
+	}
+}
diff --git a/luci-app-qosify/root/usr/share/qosify-luci/00-defaults.conf b/luci-app-qosify/root/usr/share/qosify-luci/00-defaults.conf
new file mode 100644
index 000000000..ff8a0bef5
--- /dev/null
+++ b/luci-app-qosify/root/usr/share/qosify-luci/00-defaults.conf
@@ -0,0 +1,17 @@
+# DNS
+tcp:53		voice
+tcp:5353	voice
+udp:53		voice
+udp:5353	voice
+
+# NTP
+udp:123		voice
+
+# SSH
+tcp:22		+video
+
+# HTTP/QUIC
+tcp:80		+besteffort
+tcp:443		+besteffort
+udp:80		+besteffort
+udp:443		+besteffort
diff --git a/luci-app-qosify/root/usr/share/qosify-luci/cleanup b/luci-app-qosify/root/usr/share/qosify-luci/cleanup
new file mode 100755
index 000000000..39e8b3fc5
--- /dev/null
+++ b/luci-app-qosify/root/usr/share/qosify-luci/cleanup
@@ -0,0 +1,137 @@
+#!/bin/sh
+# SPDX-License-Identifier: MIT
+#
+# Remove qosify qdiscs and ifb devices left behind after qosify exits.
+#
+# A clean stop only does part of this: interface_clear_qdisc() removes the root
+# qdisc, the bpf filters and the ifb device but not clsact, and main() never
+# calls qosify_dns_stop(), so ifb-dns stays up. A crash, a SIGKILL or a respawn
+# loop leaves the rest too. This script deliberately only touches the devices of
+# enabled qosify sections, the ifb names qosify derives from them and qosify's
+# own ifb-dns -- never a qdisc or ifb qosify did not create.
+#
+# `cleanup wait` is for a package removal: qosify is stopped by its own prerm,
+# which may run after ours, so the device names are read now and the sweep waits
+# up to 30 s for qosify to exit. If it is still running, nothing is touched.
+
+. /lib/functions.sh
+. /lib/functions/network.sh
+
+# The lock is an flock on an open fd, not a directory plus an EXIT trap: rpcd
+# SIGKILLs this script at its exec timeout (rpc_file_exec_timeout_cb() in
+# file.c), the trap never runs, and the leftover directory would then make every
+# later run exit 0 without doing anything. The kernel drops an flock on process
+# exit however the process dies. The path differs from the old directory so a
+# stale one left by an earlier version cannot break the redirect. Without
+# busybox flock, run unlocked rather than not at all -- the worst a concurrent
+# run can do is repeat a tc delete.
+LOCK="/var/lock/qosify-luci-cleanup.lock"
+
+mkdir -p /var/lock
+exec 9>"$LOCK"
+command -v flock >/dev/null && { flock -n 9 || exit 0; }
+
+# Mirrors interface_ifb_name() in qosify: "ifb-" while strlen(dev) + 4 is
+# below IFNAMSIZ. Longer names take a different branch upstream which we do not
+# try to reproduce here.
+ifb_name() {
+	[ "${#1}" -lt 12 ] || return 1
+	echo "ifb-$1"
+}
+
+qosify_filter() {
+	tc filter show dev "$1" "$2" 2>/dev/null | grep -q ' qosify_'
+}
+
+# qosify's filters sit at QOSIFY_PRIO_BASE (0x110 = 272): its bpf classifier on
+# egress, and on ingress the classifier plus the DNS and ifb redirects up to 277.
+# A clean exit removes them and the root qdisc, so they only survive a crash, and
+# then the root cake is qosify's too. Another shaper's root qdisc, and a clsact
+# carrying anyone else's filters, are left alone.
+clear_dev() {
+	local dev="$1"
+	local ifb p
+
+	[ -n "$dev" ] || return 0
+
+	if [ -e "/sys/class/net/$dev" ]; then
+		if qosify_filter "$dev" egress; then
+			tc qdisc del dev "$dev" root 2>/dev/null
+			tc filter del dev "$dev" egress pref 272 2>/dev/null
+		fi
+		if qosify_filter "$dev" ingress; then
+			for p in 272 273 274 275 276 277; do
+				tc filter del dev "$dev" ingress pref "$p" 2>/dev/null
+			done
+		fi
+		[ -n "$(tc filter show dev "$dev" ingress 2>/dev/null)$(tc filter show dev "$dev" egress 2>/dev/null)" ] ||
+			tc qdisc del dev "$dev" clsact 2>/dev/null
+	fi
+
+	# An ifb outlives its parent, so this is not gated on $dev still existing.
+	# That only helps a `config device`, whose name survives the netdev.
+	ifb="$(ifb_name "$dev")" || return 0
+	[ -e "/sys/class/net/$ifb" ] || return 0
+
+	ip link set "$ifb" down 2>/dev/null
+	ip link del "$ifb" 2>/dev/null
+}
+
+# add_interface() in qosify.init returns before reading anything else when
+# disabled is set, so qosify never touched those devices.
+section_enabled() {
+	local disabled
+
+	config_get_bool disabled "$1" disabled 0
+	[ "$disabled" -eq 0 ]
+}
+
+# `config interface` names a netifd interface, not a device; qosify resolves it
+# to .l3_device before touching tc, so resolve it the same way. netifd drops
+# .l3_device when the interface goes down, so a vanished pppoe device leaves no
+# name to derive the ifb from here. qosify clears that one itself: on the next
+# up, interface_start() runs interface_clear_qdisc(), which deletes ifb-
+# before cmd_add_ingress() creates it again.
+collect_interface() {
+	local cfg="$1"
+	local name dev
+
+	section_enabled "$cfg" || return 0
+	config_get name "$cfg" name
+	[ -n "$name" ] || return 0
+
+	network_get_device dev "$name" && DEVS="$DEVS $dev"
+}
+
+# `config device` names a netdev directly.
+collect_device() {
+	local cfg="$1"
+	local name
+
+	section_enabled "$cfg" || return 0
+	config_get name "$cfg" name
+	[ -n "$name" ] && DEVS="$DEVS $name"
+}
+
+DEVS=""
+config_load qosify
+config_foreach collect_interface interface
+config_foreach collect_device device
+
+if [ "$1" = wait ]; then
+	n=30
+	while pidof qosify >/dev/null; do
+		[ "$n" -gt 0 ] || exit 0
+		n=$((n - 1))
+		sleep 1
+	done
+fi
+
+for dev in $DEVS; do clear_dev "$dev"; done
+
+# ifb-dns is qosify's fixed DNS ifb, not derived from any section, and a stop
+# leaves it up under the kernel's default fq_codel. Deleting its root qdisc only
+# re-attaches the default, so drop the device -- only once qosify has exited.
+pidof qosify >/dev/null || ip link del ifb-dns 2>/dev/null
+
+exit 0
diff --git a/luci-app-qosify/root/usr/share/qosify-luci/qosify b/luci-app-qosify/root/usr/share/qosify-luci/qosify
new file mode 100644
index 000000000..7a3d0d068
--- /dev/null
+++ b/luci-app-qosify/root/usr/share/qosify-luci/qosify
@@ -0,0 +1,48 @@
+config defaults
+	list defaults /etc/qosify/*.conf
+	option dscp_prio video
+	option dscp_icmp +besteffort
+	option dscp_default_udp besteffort
+	option prio_max_avg_pkt_len 500
+
+config class besteffort
+	option ingress CS0
+	option egress CS0
+
+config class bulk
+	option ingress LE
+	option egress LE
+
+config class video
+	option ingress AF41
+	option egress AF41
+
+config class voice
+	option ingress CS6
+	option egress CS6
+	option bulk_trigger_pps 100
+	option bulk_trigger_timeout 5
+	option dscp_bulk CS0
+
+config interface wan
+	option name wan
+	option disabled 1
+	option bandwidth_up 100mbit
+	option bandwidth_down 100mbit
+	option overhead_type none
+	# defaults:
+	option ingress 1
+	option egress 1
+	option mode diffserv4
+	option nat 1
+	option host_isolate 1
+	option autorate_ingress 0
+	option ingress_options ""
+	option egress_options ""
+	option options ""
+
+config device wandev
+	option disabled 1
+	option name wan
+	option bandwidth 100mbit
+
diff --git a/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json b/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json
new file mode 100644
index 000000000..9179b20f0
--- /dev/null
+++ b/luci-app-qosify/root/usr/share/rpcd/acl.d/luci-app-qosify.json
@@ -0,0 +1,37 @@
+{
+	"luci-app-qosify": {
+		"description": "Grant access to LuCI app qosify",
+		"read": {
+			"ubus": {
+				"qosify": [ "status", "get_stats", "dump" ],
+				"rc": [ "list" ],
+				"service": [ "list" ],
+				"file": [ "read", "stat" ]
+			},
+			"uci": [ "qosify" ],
+			"file": {
+				"/etc/config/qosify": [ "read", "list" ],
+				"/etc/qosify/00-defaults.conf": [ "read", "list" ],
+				"/usr/share/qosify-luci/qosify": [ "read" ],
+				"/usr/share/qosify-luci/00-defaults.conf": [ "read" ],
+				"/proc/uptime": [ "read" ],
+				"/proc/[0-9]*/stat": [ "read" ]
+			}
+		},
+		"write": {
+			"ubus": {
+				"qosify": [ "reload", "check_devices" ],
+				"rc": [ "init" ],
+				"uci": [ "revert" ],
+				"file": [ "write", "exec" ]
+			},
+			"uci": [ "qosify" ],
+			"file": {
+				"/etc/config/qosify": [ "write" ],
+				"/etc/qosify/00-defaults.conf": [ "write" ],
+				"/usr/sbin/qosify-status": [ "exec" ],
+				"/usr/share/qosify-luci/cleanup": [ "exec" ]
+			}
+		}
+	}
+}
diff --git a/luci-app-sfp-info/Makefile b/luci-app-sfp-info/Makefile
new file mode 100644
index 000000000..c0efc025e
--- /dev/null
+++ b/luci-app-sfp-info/Makefile
@@ -0,0 +1,17 @@
+# Copyright (C) 2026 OpenWrt.org
+# This is free software, licensed under the Apache License, Version 2.0.
+
+include $(TOPDIR)/rules.mk
+
+LUCI_TITLE:=SFP/QSFP Transceiver Diagnostics
+LUCI_DEPENDS:=+luci-base +ethtool-full +ucode-mod-math
+LUCI_PKGARCH:=all
+
+PKG_LICENSE:=Apache-2.0
+PKG_VERSION:=1.0
+PKG_RELEASE:=1
+PKG_MAINTAINER:=Meng Zhuo 
+
+include $(TOPDIR)/feeds/luci/luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
diff --git a/luci-app-sfp-info/README.md b/luci-app-sfp-info/README.md
new file mode 100644
index 000000000..096ae354a
--- /dev/null
+++ b/luci-app-sfp-info/README.md
@@ -0,0 +1,26 @@
+# luci-app-sfp-info
+
+Visual dashboard for SFP/QSFP optical transceiver diagnostics via `ethtool --json -m`.
+
+## Features
+
+- Auto-detection of all SFP/SFP+/QSFP+/QSFP28 modules on network interfaces
+- Real-time display of temperature, voltage, bias current, TX/RX power
+- Alarm and warning status with inline labels
+- Auto-refresh every 10 seconds
+- Supports SFF-8472 (SFP), SFF-8636 (QSFP), and CMIS (QSFP-DD/OSFP)
+
+## Dependencies
+
+- `luci-base`
+- `ethtool-full` (>= 7.0 for JSON output support)
+- `ucode-mod-math` (mW to dBm conversion in the rpcd backend)
+
+## RPC API
+
+The backend provides one ubus method under `luci.sfp-info`:
+
+**list** -- Returns full diagnostics for all detected SFP interfaces:
+```
+ubus call luci.sfp-info list
+```
diff --git a/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js b/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js
new file mode 100644
index 000000000..c36ca2a16
--- /dev/null
+++ b/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js
@@ -0,0 +1,206 @@
+'use strict';
+'require view';
+'require rpc';
+'require poll';
+
+var callList = rpc.declare({
+	object: 'luci.sfp-info',
+	method: 'list',
+	expect: { 'result': [] }
+});
+
+function mwToDbm(mw) {
+	if (mw == null || mw <= 0)
+		return null;
+
+	return 10 * Math.log10(mw);
+}
+
+function fmtDate(code) {
+	if (!code)
+		return '-';
+
+	var m = code.match(/^(\d{2})(\d{2})(\d{2})/);
+
+	if (!m)
+		return code;
+
+	return '20' + m[1] + '-' + m[2] + '-' + m[3];
+}
+
+function fmtVal(val, unit, dec) {
+	if (val == null)
+		return '-';
+
+	return val.toFixed(dec || 1) + ' ' + unit;
+}
+
+function rangeTip(lo, hi, unit, dec) {
+	if (lo == null && hi == null)
+		return '';
+
+	dec = dec || 1;
+	var l = lo != null ? lo.toFixed(dec) : '?';
+	var h = hi != null ? hi.toFixed(dec) : '?';
+
+	return _('Range') + ': ' + l + ' - ' + h + ' ' + unit;
+}
+
+return view.extend({
+	load: function() {
+		return callList();
+	},
+
+	render: function(data) {
+		var modules = Array.isArray(data) ?
+			data :
+			((data && data.result) ? data.result : []);
+
+		var tbl = E('table', { 'class': 'table cbi-section-table' }, [
+			E('tr', { 'class': 'tr cbi-section-table-titles' }, [
+				E('th', { 'class': 'th' }, _('Interface')),
+				E('th', { 'class': 'th' }, _('Vendor')),
+				E('th', { 'class': 'th' }, _('Type')),
+				E('th', { 'class': 'th' }, _('Part Number')),
+				E('th', { 'class': 'th' }, _('Date Code')),
+				E('th', { 'class': 'th' }, _('Temperature')),
+				E('th', { 'class': 'th' }, _('Voltage')),
+				E('th', { 'class': 'th' }, _('Bias Current')),
+				E('th', { 'class': 'th' }, _('TX Power')),
+				E('th', { 'class': 'th' }, _('RX Power')),
+				E('th', { 'class': 'th' }, _('Status')),
+			])
+		]);
+
+		this._populateTable(tbl, modules);
+
+		var view = E('div', {}, [
+			E('h2', {}, _('SFP / QSFP Transceiver Information')),
+			E('div', { 'class': 'cbi-section' }, [ tbl ])
+		]);
+
+		var self = this;
+		poll.add(function() {
+			return callList().then(function(res) {
+				var mods = Array.isArray(res) ?
+					res :
+					((res && res.result) ? res.result : []);
+				self._populateTable(tbl, mods);
+			});
+		}, 10);
+
+		return view;
+	},
+
+	_populateTable: function(tbl, modules) {
+		/* remove old data rows, keep header */
+		var rows = tbl.querySelectorAll('tr:not(.cbi-section-table-titles)');
+		rows.forEach(function(r) {
+			r.remove();
+		});
+
+		if (!modules.length) {
+			tbl.appendChild(E('tr', { 'class': 'tr placeholder' }, [
+				E('td', { 'class': 'td', 'colspan': '11' },
+					E('em', {},
+						_('No SFP/QSFP modules detected.')))
+			]));
+			return;
+		}
+
+		for (var i = 0; i < modules.length; i++) {
+			var row = this._renderRow(modules[i]);
+			row.classList.add('cbi-rowstyle-' + ((i % 2) ? 2 : 1));
+			tbl.appendChild(row);
+		}
+	},
+
+	_renderRow: function(mod) {
+		var d = mod.diagnostics || {};
+		var thr = mod.thresholds || {};
+
+		var type = (mod.identifier || '-');
+		if (mod.connector)
+			type += ' ' + mod.connector;
+		if (mod.transceiver_type && mod.transceiver_type.length)
+			type += ' \u00b7 ' + mod.transceiver_type.join(', ');
+
+		var statusText = statusLabel(mod.alarms, mod.warnings);
+
+		var tempTitle  = rangeTip(
+			thr.temp_low_c, thr.temp_high_c, '\u00b0C');
+		var voltTitle  = rangeTip(
+			thr.voltage_low_v, thr.voltage_high_v, 'V');
+		var biasTitle  = rangeTip(
+			thr.tx_bias_low_ma, thr.tx_bias_high_ma, 'mA');
+		var txTitle    = rangeTip(
+			mwToDbm(thr.tx_power_low_mw),
+			mwToDbm(thr.tx_power_high_mw),
+			'dBm', 2);
+		var rxTitle    = rangeTip(
+			mwToDbm(thr.rx_power_low_mw),
+			mwToDbm(thr.rx_power_high_mw),
+			'dBm', 2);
+
+		return E('tr', { 'class': 'tr' }, [
+			E('td', { 'class': 'td' }, mod.iface),
+			E('td', { 'class': 'td' }, mod.vendor_name || '-'),
+			E('td', { 'class': 'td' }, type),
+			E('td', { 'class': 'td' }, mod.vendor_pn || '-'),
+			E('td', { 'class': 'td' }, fmtDate(mod.date_code)),
+			E('td', { 'class': 'td', 'title': tempTitle },
+				fmtVal(d.temperature_c, '\u00b0C', 1)),
+			E('td', { 'class': 'td', 'title': voltTitle },
+				fmtVal(d.voltage_v, 'V', 3)),
+			E('td', { 'class': 'td', 'title': biasTitle },
+				fmtVal(d.bias_current_ma, 'mA', 1)),
+			E('td', { 'class': 'td', 'title': txTitle },
+				fmtVal(d.tx_power_dbm, 'dBm', 2)),
+			E('td', { 'class': 'td', 'title': rxTitle },
+				fmtVal(d.rx_power_dbm, 'dBm', 2)),
+			E('td', { 'class': 'td' }, statusText),
+		]);
+	},
+
+	handleSave: null,
+	handleSaveApply: null,
+	handleReset: null
+});
+
+function statusLabel(alarms, warnings) {
+	var items = [];
+
+	var L = {
+		'temp_high':     _('Temperature high'),
+		'temp_low':      _('Temperature low'),
+		'voltage_high':  _('Voltage high'),
+		'voltage_low':   _('Voltage low'),
+		'tx_bias_high':  _('TX bias high'),
+		'tx_bias_low':   _('TX bias low'),
+		'tx_power_high': _('TX power high'),
+		'tx_power_low':  _('TX power low'),
+		'rx_power_high': _('RX power high'),
+		'rx_power_low':  _('RX power low')
+	};
+
+	if (alarms) {
+		for (var k in alarms) {
+			if (alarms[k])
+				items.push(E('span', { 'class': 'label notice' },
+					L[k] || k.replace(/_/g, ' ')));
+		}
+	}
+
+	if (!items.length && warnings) {
+		for (var w in warnings) {
+			if (warnings[w])
+				items.push(E('span', { 'class': 'label warning' },
+					L[w] || w.replace(/_/g, ' ')));
+		}
+	}
+
+	if (items.length)
+		return E('div', {}, items);
+
+	return E('span', { 'class': 'label success' }, _('Normal'));
+}
diff --git a/luci-app-sfp-info/po/cs/sfp-info.po b/luci-app-sfp-info/po/cs/sfp-info.po
new file mode 100644
index 000000000..f1fd4f98b
--- /dev/null
+++ b/luci-app-sfp-info/po/cs/sfp-info.po
@@ -0,0 +1,125 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: cs\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Pavel Borecki \n"
+"Language-Team: Czech \n"
+"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n"
+"PO-Revision-Date: 2026-08-14 18:35+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr "Proud polarizace"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr "Kód data výroby"
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr "Udělit ubus přístup k SFP Info"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "Rozhraní"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr "Nezjištěny žádné SFP/QSFP moduly."
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr "Normální"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr "Katalogové číslo součástky"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr "Přijímací výkon"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr "Maximální přijímaný výkon"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr "Minimální přijímaný výkon"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr "Rozsah"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr "Informace o SFP / QSFP vysílači"
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr "Informace o SFP modulech"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "Stav"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr "TX výkon"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr "TX maximální proud"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr "TX minimální proud"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr "TX maximální výkon"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr "TX minimální výkon"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "Teplota"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr "Nejvyšší teplota"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr "Nejnižší teplota"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "Typ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "Výrobce"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr "Napětí"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr "Nejvyšší napětí"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr "Nejnižší napětí"
+
+#~ msgid "Alarm"
+#~ msgstr "Alarm"
+
+#~ msgid "Warning"
+#~ msgstr "Varování"
diff --git a/luci-app-sfp-info/po/de/sfp-info.po b/luci-app-sfp-info/po/de/sfp-info.po
new file mode 100644
index 000000000..45135d017
--- /dev/null
+++ b/luci-app-sfp-info/po/de/sfp-info.po
@@ -0,0 +1,119 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: de\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: HerbJul <19JulianHerbst95@web.de>\n"
+"Language-Team: German \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"PO-Revision-Date: 2026-09-08 12:39+0000\n"
+"X-Generator: Weblate 2026.9.1\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "Interface"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "Status"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr ""
diff --git a/luci-app-sfp-info/po/es/sfp-info.po b/luci-app-sfp-info/po/es/sfp-info.po
new file mode 100644
index 000000000..e31d8d223
--- /dev/null
+++ b/luci-app-sfp-info/po/es/sfp-info.po
@@ -0,0 +1,125 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: es\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Franco Castillo \n"
+"Language-Team: Spanish \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"PO-Revision-Date: 2026-08-24 16:48+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr "Corriente de polarización"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr "Código de fecha"
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr "Otorgar acceso a ubus a la información de SFP"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "Interfaz"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr "No se detectaron módulos SFP/QSFP."
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr "Normal"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr "Número de pieza"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr "Potencia RX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr "Potencia RX alta"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr "Potencia RX baja"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr "Rango"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr "Información del transceptor SFP/QSFP"
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr "Información de SFP"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "Estado"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr "Potencia TX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr "Corriente de polarización TX alta"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr "Corriente de polarización TX baja"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr "Potencia TX alta"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr "Potencia TX baja"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "Temperatura"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr "Temperatura alta"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr "Temperatura baja"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "Tipo"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "Proveedor"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr "Voltaje"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr "Voltaje alto"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr "Voltaje bajo"
+
+#~ msgid "Alarm"
+#~ msgstr "Alarma"
+
+#~ msgid "Warning"
+#~ msgstr "Advertencia"
diff --git a/luci-app-sfp-info/po/ga/sfp-info.po b/luci-app-sfp-info/po/ga/sfp-info.po
new file mode 100644
index 000000000..7f8d7dace
--- /dev/null
+++ b/luci-app-sfp-info/po/ga/sfp-info.po
@@ -0,0 +1,126 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: ga\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Aindriú Mac Giolla Eoin \n"
+"Language-Team: Irish \n"
+"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :(n>"
+"6 && n<11) ? 3 : 4;\n"
+"PO-Revision-Date: 2026-08-16 17:38+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr "Sruth Claonta"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr "Cód Dáta"
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr "Deonaigh rochtain do ubus ar Eolas SFP"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "Comhéadan"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr "Níor braitheadh aon mhodúil SFP/QSFP."
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr "Gnáth"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr "Uimhir Chuid"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr "Cumhacht RX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr "Ardchumhacht RX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr "Cumhacht RX íseal"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr "Raon"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr "Faisnéis faoi Tharchuradóir SFP / QSFP"
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr "Eolas SFP"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "Stádas"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr "Cumhacht TX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr "Claonadh TX ard"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr "Claonadh TX íseal"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr "Cumhacht ard TX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr "Cumhacht íseal TX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "Teocht"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr "Teocht ard"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr "Teocht íseal"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "Cineál"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "Díoltóir"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr "Voltas"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr "Voltas ard"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr "Voltas íseal"
+
+#~ msgid "Alarm"
+#~ msgstr "Aláram"
+
+#~ msgid "Warning"
+#~ msgstr "Rabhadh"
diff --git a/luci-app-sfp-info/po/ko/sfp-info.po b/luci-app-sfp-info/po/ko/sfp-info.po
new file mode 100644
index 000000000..242c22d1c
--- /dev/null
+++ b/luci-app-sfp-info/po/ko/sfp-info.po
@@ -0,0 +1,119 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: ko\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Hyeonjeong Lee \n"
+"Language-Team: Korean \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"PO-Revision-Date: 2026-08-17 17:59+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr "바이어스 전류"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr "날짜 코드"
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr "SFP 정보에 ubus 접근 권한 부여"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "인터페이스"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr "SFP/QSFP 모듈이 감지되지 않음."
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr "정상"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr "부품 번호"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr "RX 전력"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr "RX 전력 높음"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr "RX 전력 낮음"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr "범위"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr "SFP / QSFP 트랜시버 정보"
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr "SFP 정보"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "상태"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr "TX 전력"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr "TX 바이어스 높음"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr "TX 바이어스 낮음"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr "TX 전력 높음"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr "TX 전력 낮음"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "온도"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr "온도 높음"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr "온도 낮음"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "유형"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "벤더"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr "전압"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr "전압 높음"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr "전압 낮음"
diff --git a/luci-app-sfp-info/po/lo/sfp-info.po b/luci-app-sfp-info/po/lo/sfp-info.po
new file mode 100644
index 000000000..52a6474ed
--- /dev/null
+++ b/luci-app-sfp-info/po/lo/sfp-info.po
@@ -0,0 +1,125 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: lo\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Bone NI \n"
+"Language-Team: Lao \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"PO-Revision-Date: 2026-08-15 13:06+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr "ກະແສໄຟ Bias"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr "ລະຫັດວັນທີ"
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr "ອະນຸຍາດໃຫ້ ubus ເຂົ້າເຖິງຂໍ້ມູນ SFP"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "ອິນເຕີເຟສ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr "ບໍ່ພົບໂມດູນ SFP/QSFP."
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr "ປົກກະຕິ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr "ເລກລະຫັດຊິ້ນສ່ວນ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr "ກຳລັງການຮັບ (RX)"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr "ກຳລັງການຮັບ (RX) ສູງ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr "ກຳລັງການຮັບ (RX) ຕ່ຳ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr "ໄລຍະ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr "ຂໍ້ມູນຕົວຮັບ-ສົ່ງສັນຍານ SFP / QSFP"
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr "ຂໍ້ມູນ SFP"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "ສະຖານະ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr "ກຳລັງການສົ່ງ (TX)"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr "ຄ່າ Bias ການສົ່ງ (TX) ສູງ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr "ຄ່າ Bias ການສົ່ງ (TX) ຕ່ຳ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr "ກຳລັງການສົ່ງ (TX) ສູງ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr "ກຳລັງການສົ່ງ (TX) ຕ່ຳ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "ອຸນຫະພູມ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr "ອຸນຫະພູມສູງ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr "ອຸນຫະພູມຕ່ຳ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "ປະເພດ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "ຜູ້ຜະລິດ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr "ແຮງດັນໄຟຟ້າ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr "ແຮງດັນໄຟຟ້າສູງ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr "ແຮງດັນໄຟຟ້າຕ່ຳ"
+
+#~ msgid "Alarm"
+#~ msgstr "ແຈ້ງເຕືອນໄພ"
+
+#~ msgid "Warning"
+#~ msgstr "ຄຳເຕືອນ"
diff --git a/luci-app-sfp-info/po/lt/sfp-info.po b/luci-app-sfp-info/po/lt/sfp-info.po
new file mode 100644
index 000000000..38f3aff8f
--- /dev/null
+++ b/luci-app-sfp-info/po/lt/sfp-info.po
@@ -0,0 +1,120 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: lt\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Džiugas Januševičius \n"
+"Language-Team: Lithuanian \n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"(n%100<10 || n%100>=20) ? 1 : 2);\n"
+"PO-Revision-Date: 2026-08-27 11:45+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr "Nuokrypio srovė"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr "Datos/Pagaminimo kodas"
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr "Suteikti „ubus“ prieigą prie – „SFP“ informacijos"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "Sąsaja ir/arba Sietuvas"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr "Neaptikti jokie „SFP/QSFP“ moduliai."
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr "Normalus"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr "Gaminio numeris"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr "Atsiųsta/Gauta reaktyviai galia"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr "Atsiųsta/Gauta reaktyviai aukšta galia"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr "Atsiųsta/Gauta reaktyviai žema galia"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr "Diapazonas"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr "„SFP/QSFP“ siųstuvo-imtuvo informacija"
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr "„SFP“ informacija"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "Būklė/Būsena"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr "Nusiųsto/Įkelto reaktyviai galia"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr "Nusiųsta/Įkelta reaktyviai aukštas nuokrypis"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr "Nusiųsta/Įkelta reaktyviai žemas nuokrypis"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr "Nusiųsta/Įkelta reaktyviai aukšta galia"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr "Nusiųsta/Įkelta reaktyviai žema galia"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "Temperatūra"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr "Aukšta temperatūra"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr "Žema temperatūra"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "Tipas"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "Pardavėjas/Tiekėjas"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr "Įtampa"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr "Aukšta įtampa"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr "Žema įtampa"
diff --git a/luci-app-sfp-info/po/pl/sfp-info.po b/luci-app-sfp-info/po/pl/sfp-info.po
new file mode 100644
index 000000000..6a4df4004
--- /dev/null
+++ b/luci-app-sfp-info/po/pl/sfp-info.po
@@ -0,0 +1,126 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: pl\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Matthaiks \n"
+"Language-Team: Polish \n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2);\n"
+"PO-Revision-Date: 2026-08-13 05:21+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr "Prąd polaryzacji"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr "Kod daty"
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr "Udziel ubus dostępu do informacji o SFP"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "Interfejs"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr "Nie wykryto modułów SFP/QSFP."
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr "Normalny"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr "Numer części"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr "Moc RX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr "Wysoka moc RX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr "Niska moc RX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr "Zakres"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr "Informacje o nadajniku-odbiorniku SFP/QSFP"
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr "Informacje o SFP"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "Status"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr "Moc TX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr "Wysoki prąd TX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr "Niski prąd TX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr "Wysoka moc TX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr "Niska moc TX"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "Temperatura"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr "Wysoka temperatura"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr "Niska temperatura"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "Typ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "Producent"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr "Napięcie"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr "Wysokie napięcie"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr "Niske napięcie"
+
+#~ msgid "Alarm"
+#~ msgstr "Alarm"
+
+#~ msgid "Warning"
+#~ msgstr "Ostrzeżenie"
diff --git a/luci-app-sfp-info/po/pt/sfp-info.po b/luci-app-sfp-info/po/pt/sfp-info.po
new file mode 100644
index 000000000..1348ea9a7
--- /dev/null
+++ b/luci-app-sfp-info/po/pt/sfp-info.po
@@ -0,0 +1,116 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: pt\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Automatically generated\n"
+"Language-Team: none\n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr ""
diff --git a/luci-app-sfp-info/po/pt_BR/sfp-info.po b/luci-app-sfp-info/po/pt_BR/sfp-info.po
new file mode 100644
index 000000000..5d4116ede
--- /dev/null
+++ b/luci-app-sfp-info/po/pt_BR/sfp-info.po
@@ -0,0 +1,119 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: pt_BR\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Martin Olmer \n"
+"Language-Team: Portuguese (Brazil) \n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"PO-Revision-Date: 2026-09-02 07:51+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "Interface"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "Status"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "Tipo"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "Fabricante"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr ""
diff --git a/luci-app-sfp-info/po/ru/sfp-info.po b/luci-app-sfp-info/po/ru/sfp-info.po
new file mode 100644
index 000000000..515e75bf2
--- /dev/null
+++ b/luci-app-sfp-info/po/ru/sfp-info.po
@@ -0,0 +1,127 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: ru\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: SnIPeRSnIPeR "
+"\n"
+"Language-Team: Russian \n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"PO-Revision-Date: 2026-08-16 22:03+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr "Ток смещения"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr "Дата изготовления"
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr "Предоставить доступ ubus к информации SFP Info"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "Интерфейс"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr "Модули SFP/QSFP не обнаружены."
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr "Норма"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr "Номер изделия"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr "Мощность приёма"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr "Высокая мощность приёма"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr "Низкая мощность приёма"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr "Диапазон"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr "Информация о трансивере SFP / QSFP"
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr "SFP Info"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "Статус"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr "Мощность передачи"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr "Высокий ток смещения передачи"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr "Низкий ток смещения передачи"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr "Высокая мощность передачи"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr "Низкая мощность передачи"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "Температура"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr "Высокая температура"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr "Низкая температура"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "Тип"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "Производитель"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr "Напряжение"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr "Высокое напряжение"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr "Низкое напряжение"
+
+#~ msgid "Alarm"
+#~ msgstr "Авария"
+
+#~ msgid "Warning"
+#~ msgstr "Предупреждение"
diff --git a/luci-app-sfp-info/po/sv/sfp-info.po b/luci-app-sfp-info/po/sv/sfp-info.po
new file mode 100644
index 000000000..cf0762f2a
--- /dev/null
+++ b/luci-app-sfp-info/po/sv/sfp-info.po
@@ -0,0 +1,119 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: sv\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Daniel Nylander \n"
+"Language-Team: Swedish \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"PO-Revision-Date: 2026-09-16 17:42+0000\n"
+"X-Generator: Weblate 2026.10.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr "Förspänningsström"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr "Datumkod"
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr "Ge ubus åtkomst till SFP-information"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "Gränssnitt"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr "Inga SFP-/QSFP-moduler hittades."
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr "Normal"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr "Artikelnummer"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr "RX-effekt"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr "Hög RX-effekt"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr "Låg RX-effekt"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr "Intervall"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr "Information om SFP-/QSFP-sändtagare"
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr "SFP-information"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "Status"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr "TX-effekt"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr "Hög TX-förspänning"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr "Låg TX-förspänning"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr "Hög TX-effekt"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr "Låg TX-effekt"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "Temperatur"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr "Hög temperatur"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr "Låg temperatur"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "Typ"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "Tillverkare"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr "Spänning"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr "Hög spänning"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr "Låg spänning"
diff --git a/luci-app-sfp-info/po/ta/sfp-info.po b/luci-app-sfp-info/po/ta/sfp-info.po
new file mode 100644
index 000000000..0b12e85d1
--- /dev/null
+++ b/luci-app-sfp-info/po/ta/sfp-info.po
@@ -0,0 +1,119 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: ta\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Pavel Borecki \n"
+"Language-Team: Tamil \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"PO-Revision-Date: 2026-08-23 19:31+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "வெப்பநிலை"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr ""
diff --git a/luci-app-sfp-info/po/templates/sfp-info.pot b/luci-app-sfp-info/po/templates/sfp-info.pot
new file mode 100644
index 000000000..225ad6373
--- /dev/null
+++ b/luci-app-sfp-info/po/templates/sfp-info.pot
@@ -0,0 +1,110 @@
+msgid ""
+msgstr "Content-Type: text/plain; charset=UTF-8"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr ""
diff --git a/luci-app-sfp-info/po/tr/sfp-info.po b/luci-app-sfp-info/po/tr/sfp-info.po
new file mode 100644
index 000000000..bf46dbf06
--- /dev/null
+++ b/luci-app-sfp-info/po/tr/sfp-info.po
@@ -0,0 +1,119 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: tr\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Martin Olmer \n"
+"Language-Team: Turkish \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"PO-Revision-Date: 2026-08-31 18:52+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "Arayüz"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "Durum"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "Sıcaklık"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "Tür"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "Satıcı"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr "Gerilim"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr ""
diff --git a/luci-app-sfp-info/po/uk/sfp-info.po b/luci-app-sfp-info/po/uk/sfp-info.po
new file mode 100644
index 000000000..fb478d66c
--- /dev/null
+++ b/luci-app-sfp-info/po/uk/sfp-info.po
@@ -0,0 +1,120 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Language: uk\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Last-Translator: Martin Olmer \n"
+"Language-Team: Ukrainian \n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"PO-Revision-Date: 2026-08-28 19:55+0000\n"
+"X-Generator: Weblate 2026.9.dev0\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "Інтерфейс"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr "Нормальна"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr ""
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "Стан"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "Тип"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "Постачальник"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr ""
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr ""
diff --git a/luci-app-sfp-info/po/zh-cn b/luci-app-sfp-info/po/zh-cn
new file mode 120000
index 000000000..8d69574dd
--- /dev/null
+++ b/luci-app-sfp-info/po/zh-cn
@@ -0,0 +1 @@
+zh_Hans
\ No newline at end of file
diff --git a/luci-app-sfp-info/po/zh_Hans/sfp-info.po b/luci-app-sfp-info/po/zh_Hans/sfp-info.po
new file mode 100644
index 000000000..7f19052e5
--- /dev/null
+++ b/luci-app-sfp-info/po/zh_Hans/sfp-info.po
@@ -0,0 +1,128 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: 2026-05-22 19:27+0800\n"
+"Last-Translator: Meng Zhuo \n"
+"Language-Team: Chinese (Simplified Han script) \n"
+"Language: zh_Hans\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Weblate\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr "偏置电流"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr "生产日期"
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr "授予 ubus 访问 SFP 收发器信息的权限"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "接口"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr "未检测到 SFP/QSFP 收发器。"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr "正常"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr "型号"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr "接收功率"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr "接收功率过高"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr "接收功率过低"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr "范围"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr "SFP / QSFP 收发器信息"
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr "SFP 收发器信息"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "状态"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr "发送功率"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr "偏置电流过高"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr "偏置电流过低"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr "发送功率过高"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr "发送功率过低"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "温度"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr "温度过高"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr "温度过低"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "类型"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "厂商"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr "电压"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr "电压过高"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr "电压过低"
+
+#~ msgid "Alarm"
+#~ msgstr "告警"
+
+#~ msgid "Warning"
+#~ msgstr "警告"
diff --git a/luci-app-sfp-info/po/zh_Hant/sfp-info.po b/luci-app-sfp-info/po/zh_Hant/sfp-info.po
new file mode 100644
index 000000000..aa7489e0a
--- /dev/null
+++ b/luci-app-sfp-info/po/zh_Hant/sfp-info.po
@@ -0,0 +1,128 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: 2026-05-22 19:27+0800\n"
+"Last-Translator: Meng Zhuo \n"
+"Language-Team: Chinese (Traditional Han script) \n"
+"Language: zh_Hant\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Weblate\n"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:68
+msgid "Bias Current"
+msgstr "偏壓電流"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:65
+msgid "Date Code"
+msgstr "生產日期"
+
+#: applications/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json:3
+msgid "Grant ubus access to SFP Info"
+msgstr "授予 ubus 存取 SFP 收發器資訊的權限"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:61
+msgid "Interface"
+msgstr "介面"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:106
+msgid "No SFP/QSFP modules detected."
+msgstr "未檢測到 SFP/QSFP 收發器。"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:205
+msgid "Normal"
+msgstr "正常"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:64
+msgid "Part Number"
+msgstr "型號"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:70
+msgid "RX Power"
+msgstr "接收功率"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:182
+msgid "RX power high"
+msgstr "接收功率過高"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:183
+msgid "RX power low"
+msgstr "接收功率過低"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:46
+msgid "Range"
+msgstr "範圍"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:78
+msgid "SFP / QSFP Transceiver Information"
+msgstr "SFP / QSFP 收發器資訊"
+
+#: applications/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json:3
+msgid "SFP Info"
+msgstr "SFP 收發器資訊"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:71
+msgid "Status"
+msgstr "狀態"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:69
+msgid "TX Power"
+msgstr "發送功率"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:178
+msgid "TX bias high"
+msgstr "偏壓電流過高"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:179
+msgid "TX bias low"
+msgstr "偏壓電流過低"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:180
+msgid "TX power high"
+msgstr "發送功率過高"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:181
+msgid "TX power low"
+msgstr "發送功率過低"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:66
+msgid "Temperature"
+msgstr "溫度"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:174
+msgid "Temperature high"
+msgstr "溫度過高"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:175
+msgid "Temperature low"
+msgstr "溫度過低"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:63
+msgid "Type"
+msgstr "類型"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:62
+msgid "Vendor"
+msgstr "廠商"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:67
+msgid "Voltage"
+msgstr "電壓"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:176
+msgid "Voltage high"
+msgstr "電壓過高"
+
+#: applications/luci-app-sfp-info/htdocs/luci-static/resources/view/sfp-info/main.js:177
+msgid "Voltage low"
+msgstr "電壓過低"
+
+#~ msgid "Alarm"
+#~ msgstr "警報"
+
+#~ msgid "Warning"
+#~ msgstr "警告"
diff --git a/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json b/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json
new file mode 100644
index 000000000..5c68ed8ae
--- /dev/null
+++ b/luci-app-sfp-info/root/usr/share/luci/menu.d/luci-app-sfp-info.json
@@ -0,0 +1,13 @@
+{
+	"admin/status/sfp-info": {
+		"title": "SFP Info",
+		"order": 50,
+		"action": {
+			"type": "view",
+			"path": "sfp-info/main"
+		},
+		"depends": {
+			"acl": ["luci-app-sfp-info"]
+		}
+	}
+}
diff --git a/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json b/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json
new file mode 100644
index 000000000..6b653e86b
--- /dev/null
+++ b/luci-app-sfp-info/root/usr/share/rpcd/acl.d/luci-app-sfp-info.json
@@ -0,0 +1,10 @@
+{
+	"luci-app-sfp-info": {
+		"description": "Grant ubus access to SFP Info",
+		"read": {
+			"ubus": {
+				"luci.sfp-info": ["list"]
+			}
+		}
+	}
+}
diff --git a/luci-app-sfp-info/root/usr/share/rpcd/ucode/sfp-info.uc b/luci-app-sfp-info/root/usr/share/rpcd/ucode/sfp-info.uc
new file mode 100755
index 000000000..96fbaaf38
--- /dev/null
+++ b/luci-app-sfp-info/root/usr/share/rpcd/ucode/sfp-info.uc
@@ -0,0 +1,286 @@
+#!/usr/bin/env ucode
+'use strict';
+
+import { lsdir, popen, stat } from 'fs';
+import { log } from 'math';
+
+function run_json(cmd) {
+	let proc = popen(cmd, 'r');
+
+	if (!proc)
+		return null;
+
+	let out = proc.read('all');
+
+	proc.close();
+
+	if (!out)
+		return null;
+
+	try {
+		return json(trim(out));
+	} catch (e) {
+		return null;
+	}
+}
+
+function shellquote(s) {
+	return `'${replace(s, "'", "'\\''")}'`;
+}
+
+function mw_to_dbm(mw) {
+	if (mw == null || mw <= 0)
+		return null;
+
+	return 10 * log(mw) / log(10);
+}
+
+function parse_ethtool_json(eth_data, iface) {
+	/* ethtool --json -m outputs a JSON array with one element */
+	let data = null;
+
+	if (type(eth_data) == 'array' && length(eth_data) > 0)
+		data = eth_data[0];
+	else if (type(eth_data) == 'object')
+		data = eth_data;
+	else
+		return null;
+
+	/* must have an identifier to be a valid transceiver */
+	let identifier = data.identifier_description || null;
+
+	if (!identifier)
+		return null;
+
+	/* connector */
+	let connector = data.connector_description || null;
+
+	/* wavelength */
+	let wavelength = data.laser_wavelength != null ?
+		+data.laser_wavelength : null;
+
+	/* vendor OUI -- ethtool JSON always [byte0, byte1, byte2] */
+	let vendor_oui = type(data.vendor_oui) == 'array' ?
+		sprintf('%02x:%02x:%02x',
+			data.vendor_oui[0],
+			data.vendor_oui[1],
+			data.vendor_oui[2]) : null;
+
+	/* transceiver types -- ethtool may output a single string */
+	let transceiver_types = [];
+
+	if (data.transceiver_type)
+		push(transceiver_types, data.transceiver_type);
+
+	/* detect multi-channel (QSFP/CMIS) by array bias current */
+	let bias = data.laser_tx_bias_current;
+	let is_multi_ch = type(bias) == 'array';
+	let ch_count = is_multi_ch ? min(length(bias), 4) : 1;
+
+	let diag = {};
+
+	diag.temperature_c = data.module_temperature_measurement != null ?
+		+data.module_temperature_measurement : null;
+	diag.voltage_v = data.module_voltage_measurement != null ?
+		+data.module_voltage_measurement : null;
+
+	if (is_multi_ch) {
+		for (let ch = 0; ch < ch_count; ch++) {
+			let ci = ch + 1;
+
+			/* bias current per channel */
+			diag[`bias_current_ma_ch${ci}`] =
+				bias[ch] != null ? +bias[ch] : null;
+
+			/* TX power per channel */
+			let tx_arr = data.transmit_avg_optical_power;
+			let tx_mw = (type(tx_arr) == 'array' &&
+				tx_arr[ch] != null) ? +tx_arr[ch] : null;
+
+			diag[`tx_power_mw_ch${ci}`] = tx_mw;
+			diag[`tx_power_dbm_ch${ci}`] = mw_to_dbm(tx_mw);
+
+			/* RX power per channel -- QSFP: rx_power.values array */
+			let rx_obj = data.rx_power;
+			let rx_arr = (type(rx_obj) == 'object' &&
+				type(rx_obj.values) == 'array') ?
+				rx_obj.values : null;
+
+			let rx_mw = (rx_arr != null && rx_arr[ch] != null) ?
+				+rx_arr[ch] : null;
+
+			diag[`rx_power_mw_ch${ci}`] = rx_mw;
+			diag[`rx_power_dbm_ch${ci}`] = mw_to_dbm(rx_mw);
+		}
+
+		/* plain keys from channel 1 for table view */
+		diag.bias_current_ma = diag.bias_current_ma_ch1;
+		diag.tx_power_mw     = diag.tx_power_mw_ch1;
+		diag.tx_power_dbm    = diag.tx_power_dbm_ch1;
+		diag.rx_power_mw     = diag.rx_power_mw_ch1;
+		diag.rx_power_dbm    = diag.rx_power_dbm_ch1;
+	} else {
+		/* SFP: single-channel values */
+		diag.bias_current_ma = bias != null ? +bias : null;
+
+		let tx_val = data.transmit_avg_optical_power;
+
+		diag.tx_power_mw  = tx_val != null ? +tx_val : null;
+		diag.tx_power_dbm = mw_to_dbm(tx_val != null ? +tx_val : null);
+
+		/* SFP: rx_power is { value, type } */
+		let rx_obj = data.rx_power;
+		let rx_val = (type(rx_obj) == 'object') ? rx_obj.value : null;
+
+		diag.rx_power_mw  = rx_val != null ? +rx_val : null;
+		diag.rx_power_dbm = mw_to_dbm(rx_val != null ? +rx_val : null);
+	}
+
+	/*
+	 * Alarm / warning flags.
+	 * Ethtool JSON keys are display-names lowercased with underscores.
+	 * We map them to the short keys the frontend expects.
+	 * For QSFP, channel-level flags are arrays -- check if any channel is true.
+	 */
+	const alarm_map = {
+		'laser_bias_current_high_alarm':  'tx_bias_high',
+		'laser_output_power_high_alarm': 'tx_power_high',
+		'laser_tx_power_high_alarm':     'tx_power_high',
+		'laser_bias_current_low_alarm':   'tx_bias_low',
+		'laser_output_power_low_alarm':  'tx_power_low',
+		'laser_tx_power_low_alarm':      'tx_power_low',
+		'module_temperature_high_alarm':  'temp_high',
+		'module_temperature_low_alarm':   'temp_low',
+		'module_voltage_high_alarm':      'voltage_high',
+		'module_voltage_low_alarm':       'voltage_low',
+		'laser_rx_power_high_alarm':      'rx_power_high',
+		'laser_rx_power_low_alarm':       'rx_power_low',
+	};
+
+	const warning_map = {
+		'laser_bias_current_high_warning':  'tx_bias_high',
+		'laser_output_power_high_warning': 'tx_power_high',
+		'laser_tx_power_high_warning':     'tx_power_high',
+		'laser_bias_current_low_warning':   'tx_bias_low',
+		'laser_output_power_low_warning':  'tx_power_low',
+		'laser_tx_power_low_warning':      'tx_power_low',
+		'module_temperature_high_warning':  'temp_high',
+		'module_temperature_low_warning':   'temp_low',
+		'module_voltage_high_warning':      'voltage_high',
+		'module_voltage_low_warning':       'voltage_low',
+		'laser_rx_power_high_warning':      'rx_power_high',
+		'laser_rx_power_low_warning':       'rx_power_low',
+	};
+
+	function map_flags(src, map) {
+		let out = {};
+
+		for (let json_key, out_key in map) {
+			let val = src[json_key];
+
+			if (val == null)
+				continue;
+
+			/* arrays: any channel set -> true (QSFP channel-level) */
+			if (type(val) == 'array') {
+				let has = false;
+
+				for (let v in val) {
+					if (v) {
+						has = true;
+						break;
+					}
+				}
+
+				out[out_key] = has;
+			} else {
+				out[out_key] = !!val;
+			}
+		}
+
+		return out;
+	}
+
+	let alarms   = map_flags(data, alarm_map);
+	let warnings = map_flags(data, warning_map);
+
+	/*
+	 * Thresholds.
+	 * Ethtool JSON nests thresholds as sub-objects:
+	 * { module_temperature: { high_alarm_threshold: ... }, ... }
+	 */
+	function get_thr(obj, key) {
+		if (type(obj) != 'object')
+			return null;
+
+		return obj[key] != null ? +obj[key] : null;
+	}
+
+	let thresholds = {};
+	const thr_map = {
+		'module_temperature': { h: 'temp_high_c',       l: 'temp_low_c'       },
+		'module_voltage':     { h: 'voltage_high_v',     l: 'voltage_low_v'    },
+		'laser_bias_current': { h: 'tx_bias_high_ma',    l: 'tx_bias_low_ma'   },
+		'laser_output_power': { h: 'tx_power_high_mw',   l: 'tx_power_low_mw'  },
+		'laser_rx_power':     { h: 'rx_power_high_mw',   l: 'rx_power_low_mw'  },
+	};
+
+	for (let src_key, out_keys in thr_map) {
+		let obj = data[src_key];
+
+		if (type(obj) == 'object') {
+			thresholds[out_keys.h] = get_thr(obj, 'high_alarm_threshold');
+			thresholds[out_keys.l] = get_thr(obj, 'low_alarm_threshold');
+		}
+	}
+
+	return {
+		iface, present: true,
+		identifier, connector, wavelength,
+		vendor_name:  data.vendor_name || null,
+		vendor_oui,
+		vendor_pn:    data.vendor_pn  || null,
+		vendor_rev:   data.vendor_rev || null,
+		vendor_sn:    data.vendor_sn  || null,
+		date_code:    data.date_code  || null,
+		transceiver_type: transceiver_types,
+		diagnostics:  diag,
+		alarms, warnings, thresholds,
+	};
+}
+
+function rpc_list() {
+	let result = [];
+	let names  = lsdir('/sys/class/net/');
+
+	if (!names)
+		return { result };
+
+	for (let name in names) {
+		/* skip virtual interfaces: only physical NICs
+		 * have a /sys/class/net//device symlink */
+		if (!stat('/sys/class/net/' + name + '/device'))
+			continue;
+
+		let out = run_json(
+			'/usr/sbin/ethtool --json -m ' +
+			shellquote(name) +
+			' 2>/dev/null');
+
+		if (out) {
+			try {
+				let parsed = parse_ethtool_json(out, name);
+				if (parsed)
+					push(result, parsed);
+			} catch (e) {}
+		}
+	}
+
+	return { result };
+}
+
+const methods = {
+	list: { call: () => rpc_list() },
+};
+
+return { 'luci.sfp-info': methods };
diff --git a/luci-app-shunt/Makefile b/luci-app-shunt/Makefile
new file mode 100644
index 000000000..1d92135c5
--- /dev/null
+++ b/luci-app-shunt/Makefile
@@ -0,0 +1,16 @@
+# Copyright 2026 Dirk Brenken (dev@brenken.org)
+# This is free software, licensed under the Apache License, Version 2.0
+
+include $(TOPDIR)/rules.mk
+
+LUCI_TITLE:=LuCI support for shunt
+LUCI_DEPENDS:=+luci-base +shunt
+
+PKG_VERSION:=0.1.10
+PKG_RELEASE:=3
+PKG_LICENSE:=Apache-2.0
+PKG_MAINTAINER:=Dirk Brenken 
+
+include $(TOPDIR)/feeds/luci/luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
diff --git a/luci-app-shunt/htdocs/luci-static/resources/view/shunt/logread.js b/luci-app-shunt/htdocs/luci-static/resources/view/shunt/logread.js
new file mode 100644
index 000000000..6f714d78a
--- /dev/null
+++ b/luci-app-shunt/htdocs/luci-static/resources/view/shunt/logread.js
@@ -0,0 +1,4 @@
+'use strict';
+'require view.shunt.logtemplate as LogTemplate';
+
+return LogTemplate.Logview(/\bshunt(\[\d+\])?:/, 'shunt');
diff --git a/luci-app-shunt/htdocs/luci-static/resources/view/shunt/logtemplate.js b/luci-app-shunt/htdocs/luci-static/resources/view/shunt/logtemplate.js
new file mode 100644
index 000000000..70e876c61
--- /dev/null
+++ b/luci-app-shunt/htdocs/luci-static/resources/view/shunt/logtemplate.js
@@ -0,0 +1,68 @@
+'use strict';
+'require rpc';
+
+const callLogRead = rpc.declare({
+	object: 'log',
+	method: 'read',
+	params: ['lines', 'stream', 'oneshot'],
+	expect: {}
+});
+
+function Logview(logtag, name) {
+	return L.view.extend({
+		load: () => Promise.resolve(),
+
+		render: function () {
+			const pollFn = () => {
+				return callLogRead(1000, false, true).then(res => {
+					const logEl = document.getElementById('logfile');
+					if (!logEl) return;
+					const filtered = (res?.log ?? [])
+						.filter(entry => !logtag || (logtag instanceof RegExp
+							? logtag.test(entry.msg)
+							: entry.msg.includes(logtag)))
+						.map(entry => {
+							const d = new Date(entry.time);
+							const pad = n => String(n).padStart(2, '0');
+							const date = `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()}`;
+							const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+							return `[${date}-${time}] ${entry.msg}`;
+						});
+					logEl.value = filtered.length > 0
+						? filtered.join('\n')
+						: _('No %s related logs yet!').format(name);
+					logEl.scrollTop = logEl.scrollHeight;
+				});
+			};
+
+			this._pollFn = pollFn;
+			L.Poll.add(pollFn);
+
+			return E('div', { class: 'cbi-map' }, [
+				E('div', { class: 'cbi-section' }, [
+					E('div', { class: 'cbi-section-descr' },
+						_('The syslog output, pre-filtered for messages related to: %s').format(name)),
+					E('textarea', {
+						id: 'logfile',
+						style: 'min-height: 500px; max-height: 90vh; width: 100%; padding: 5px; font-family: monospace; resize: vertical;',
+						readonly: 'readonly',
+						wrap: 'off'
+					})
+				])
+			]);
+		},
+
+		unload: function () {
+			if (this._pollFn) {
+				L.Poll.remove(this._pollFn);
+				this._pollFn = null;
+			}
+		},
+
+		handleSaveApply: null,
+		handleSave: null,
+		handleReset: null
+	});
+}
+
+return L.Class.extend({ Logview });
diff --git a/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js b/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js
new file mode 100644
index 000000000..61270ebee
--- /dev/null
+++ b/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js
@@ -0,0 +1,480 @@
+'use strict';
+'require dom';
+'require view';
+'require poll';
+'require fs';
+'require ui';
+'require uci';
+'require rpc';
+'require form';
+'require tools.widgets as widgets';
+
+const getStatus = rpc.declare({
+	object: 'luci.shunt',
+	method: 'status'
+});
+
+const getPackages = rpc.declare({
+	object: 'rpc-sys',
+	method: 'packagelist',
+	params: ['all'],
+	expect: { packages: {} }
+});
+
+function handleAction(ev) {
+	if (ev === 'restart') {
+		const map = document.querySelector('.cbi-map');
+
+		return dom.callClassMethod(map, 'save')
+			.then(function () {
+				return Promise.all([
+					uci.changes(),
+					fs.exec('/etc/init.d/shunt', ['running'])
+				]);
+			})
+			.then(function (res) {
+				const pending = res[0] && Object.keys(res[0]).length;
+				const running = res[1] && res[1].code === 0;
+
+				if (pending && running) {
+					L.bind(ui.changes.apply, ui.changes)();
+					return null;
+				}
+
+				if (pending) {
+					return uci.apply().then(function () {
+						if (ui.changes.setIndicator) {
+							ui.changes.setIndicator(0);
+						}
+						return fs.exec_direct('/etc/init.d/shunt', [ev]);
+					});
+				}
+				return fs.exec_direct('/etc/init.d/shunt', [ev]);
+			})
+			.catch(function (e) {
+				ui.addNotification(null,
+					E('p', {}, [_('Apply failed: %s').format(e)]), 'error');
+			});
+	}
+	return fs.exec_direct('/etc/init.d/shunt', [ev]);
+}
+
+function fmtMark(mark) {
+	if (mark == null) {
+		return '-';
+	}
+	return '0x%08x'.format(mark);
+}
+
+function fmtCount(n) {
+	if (n == null) {
+		return '?';
+	}
+	return '%d'.format(n);
+}
+
+function card(label, value, sub, cls) {
+	return E('div', { 'class': 'shunt-card' }, [
+		E('div', { 'class': 'shunt-label' }, label),
+		E('div', { 'class': 'shunt-value ' + (cls || '') }, value),
+		sub ? E('div', { 'class': 'shunt-sub' }, sub) : ''
+	]);
+}
+
+function dot(state) {
+	return E('span', { 'class': 'shunt-dot shunt-dot-' + state });
+}
+
+function renderState(st) {
+	if (!st || st.running == null) {
+		return E('div', { 'class': 'shunt-state' }, [
+			dot('off'), E('span', {}, _('no answer from the backend'))
+		]);
+	}
+
+	// The firewall's stop action deletes every nftables table, shunt's
+	// included; the daemon puts its own back. The counter is the only trace
+	// that is left of it, so it is shown where the state is.
+	const reapplied = (st.service && st.service.reapplied) || 0;
+
+	if (st.running && st.applied) {
+		return E('div', { 'class': 'shunt-state' }, [
+			dot('ok'), E('span', {}, _('running, policy applied')),
+			reapplied ? E('span', { 'class': 'shunt-key' },
+				_('ruleset re-applied %d time(s)').format(reapplied)) : ''
+		]);
+	}
+
+	if (st.running && !st.applied) {
+		return E('div', { 'class': 'shunt-state' }, [
+			dot('warn'), E('span', {}, _('running, but no ruleset in the kernel'))
+		]);
+	}
+
+	if (!st.running && st.applied) {
+		return E('div', { 'class': 'shunt-state' }, [
+			dot('warn'), E('span', {}, _('ruleset present, service not running'))
+		]);
+	}
+
+	return E('div', { 'class': 'shunt-state' }, [
+		dot('off'), E('span', {}, _('stopped'))
+	]);
+}
+
+// The observer's verdict identifiers are module contract - translated for
+// display only, with the raw key kept beside each label.
+const DROP_LABEL = {
+	'nomatch': _('Domain is in no policy'),
+	'qtype': _('Not an address query'),
+	'noaddr': _('Answer carried no usable address'),
+
+	'dns:E_RCODE': _('Error reply, e.g. NXDOMAIN'),
+	'dns:E_NOTRESP': _('Not a response'),
+	'dns:E_TRUNC': _('Truncated response'),
+	'dns:E_QDCOUNT': _('Not exactly one question'),
+	'dns:E_SHORT': _('Message ends mid-structure'),
+	'dns:E_MSGLEN': _('Message too long'),
+	'dns:E_QPTR': _('Compression pointer in the question'),
+	'dns:E_LABEL': _('Reserved label type'),
+	'dns:E_NAMELEN': _('Name too long'),
+	'dns:E_CHARSET': _('Name has a byte outside a-z 0-9 - _'),
+	'dns:E_RDLEN': _('Record length does not fit the type'),
+	'dns:E_ANSMAX': _('Too many answers'),
+
+	'frame:E_SHORT': _('Frame ends mid-structure'),
+	'frame:E_ETHER': _('Neither IPv4 nor IPv6'),
+	'frame:E_VLAN': _('Too many stacked VLAN tags'),
+	'frame:E_IPLEN': _('Inconsistent IP header length'),
+	'frame:E_FRAG': _('IP fragment'),
+	'frame:E_EXTHDR': _('Extension header chain too long'),
+	'frame:E_PROTO': _('Not UDP'),
+	'frame:E_UDPLEN': _('Inconsistent UDP length')
+};
+
+function dropLabel(key) {
+	return DROP_LABEL[key] || key;
+}
+
+function renderSnoop(svc) {
+	if (!svc || !svc.snoop) {
+		return '';
+	}
+
+	const drops = svc.snoop.drops || {};
+	const matched = svc.snoop.matched || 0;
+	const keys = Object.keys(drops).sort(function (a, b) {
+		return drops[b] - drops[a] || a.localeCompare(b);
+	});
+
+	let total = 0;
+
+	keys.forEach(function (k) {
+		total += drops[k];
+	});
+
+	const rows = keys.map(function (k) {
+		return E('tr', { 'class': 'tr' }, [
+			E('td', { 'class': 'td left' }, [
+				dropLabel(k),
+				E('span', { 'class': 'shunt-key' }, k)
+			]),
+			E('td', { 'class': 'td right' }, ['%d'.format(drops[k])])
+		]);
+	});
+
+	return E('div', { 'class': 'shunt-block' }, [
+		E('div', { 'class': 'shunt-label' },
+			[_('DNS responses observed on %s since the service started')
+				.format((svc.snoop.devices || []).join(', ') || '-')]),
+		E('table', { 'class': 'table' }, [
+			E('tr', { 'class': 'tr' }, [
+				E('td', { 'class': 'td left' },
+					E('strong', {}, _('Answers used for a policy'))),
+				E('td', { 'class': 'td right' },
+					E('strong', { 'class': 'shunt-hit' }, '%d'.format(matched)))
+			])
+		].concat(rows)),
+		E('div', { 'class': 'shunt-sub' }, _('%d of %d observed responses were not used. That is normal: the observer sees every answer on the network, and only the ones for a domain you routed are of any interest.')
+			.format(total, total + matched))
+	]);
+}
+
+function renderPolicies(st) {
+	if (!st || !st.policies || !st.policies.length) {
+		return E('div', { 'class': 'shunt-sub' }, _('No policy is active.'));
+	}
+
+	const rows = [
+		E('tr', { 'class': 'tr table-titles' }, [
+			E('th', { 'class': 'th' }, _('Policy')),
+			E('th', { 'class': 'th' }, _('Action')),
+			E('th', { 'class': 'th' }, _('Interface')),
+			E('th', { 'class': 'th' }, _('Mark')),
+			E('th', { 'class': 'th' }, _('Table')),
+			E('th', { 'class': 'th' }, _('Rules')),
+			E('th', { 'class': 'th' }, _('Routes')),
+			E('th', { 'class': 'th' }, _('Fallback'))
+		])
+	];
+
+	st.policies.forEach(function (p) {
+		// A bypass policy has no mark, no table and no rule of its own, so
+		// those cells are empty rather than unknown.
+		const bypass = (p.action === 'bypass');
+
+		rows.push(E('tr', { 'class': 'tr' }, [
+			E('td', { 'class': 'td' }, [p.name]),
+			E('td', { 'class': 'td' }, [p.action || 'route']),
+			E('td', { 'class': 'td' }, [p.interface || '-']),
+			E('td', { 'class': 'td' }, [fmtMark(p.mark)]),
+			E('td', { 'class': 'td' }, [bypass ? '-' : fmtCount(p.rt_table)]),
+			E('td', { 'class': 'td' }, [bypass ? '-' : fmtCount(p.rules)]),
+			E('td', { 'class': 'td' }, [bypass ? '-' : fmtCount(p.routes)]),
+			E('td', { 'class': 'td' }, [bypass ? '-' : (p.fallback || 'main')])
+		]));
+	});
+
+	return E('table', { 'class': 'table' }, rows);
+}
+
+function renderIssues(st) {
+	if (!st || !st.issues || !st.issues.length) {
+		return '';
+	}
+
+	const items = st.issues.map(function (i) {
+		const where = i.entry ? '%s: %s'.format(i.policy, i.entry) : i.policy;
+		return E('li', {}, ['%s - %s'.format(where, i.reason)]);
+	});
+
+	return E('div', { 'class': 'shunt-block' }, [
+		E('div', { 'class': 'shunt-label' }, _('Rejected settings')),
+		E('ul', { 'class': 'shunt-issues' }, items),
+		E('div', { 'class': 'shunt-sub' }, _('These entries were skipped. Everything else was applied - a rejected entry never takes the service down.'))
+	]);
+}
+
+return view.extend({
+	load: function () {
+		return Promise.all([
+			L.resolveDefault(getStatus(), {}),
+			uci.load('shunt').catch(() => 0),
+			L.resolveDefault(getPackages(true), {})
+		]);
+	},
+
+	render: function (result) {
+		const pkgs = result[2] || {};
+
+		if (!uci.sections('shunt').length) {
+			ui.addNotification(null, E('p', _('No shunt config found!')), 'error');
+			return;
+		}
+
+		let m, s, o;
+
+		m = new form.Map('shunt', 'shunt',
+			_('Policy based routing by mac, source, destination and domain. For further information please check the %s.')
+				.format(`${_('online documentation')}`));
+		const style = E('style', { 'type': 'text/css' },
+			'#shunt-status {' +
+			'--shunt-card-bg: rgba(128,128,128,.07);' +
+			'--shunt-card-border: rgba(128,128,128,.28);' +
+			'--shunt-muted: GrayText;' +
+			'--shunt-ok: #1f8a5f;' +
+			'--shunt-warn: #b8860b;' +
+			'--shunt-off: #808080;' +
+			'}' +
+			'@media (prefers-color-scheme: dark) {' +
+			'#shunt-status {' +
+			'--shunt-ok: #63c79b;' +
+			'--shunt-warn: #e0b458;' +
+			'}}' +
+			'#shunt-status .shunt-grid { display: grid; gap: .75em; ' +
+			'grid-template-columns: repeat(auto-fit, minmax(min(12em, 100%), 1fr)); ' +
+			'margin-bottom: .75em; }' +
+			'#shunt-status .shunt-card { background: var(--shunt-card-bg); ' +
+			'border: 1px solid var(--shunt-card-border); border-radius: 8px; ' +
+			'padding: .7em .9em; min-width: 0; overflow-wrap: break-word; }' +
+			'#shunt-status .shunt-block { margin-bottom: .75em; }' +
+			'#shunt-status .shunt-label { font-size: .85em; ' +
+			'color: var(--shunt-muted); margin-bottom: .3em; }' +
+			'#shunt-status .shunt-sub { font-size: .8em; ' +
+			'color: var(--shunt-muted); margin-top: .3em; }' +
+			'#shunt-status .shunt-value { font-size: 1.5em; line-height: 1.3; ' +
+			'font-variant-numeric: tabular-nums; }' +
+			'#shunt-status .shunt-state { display: flex; align-items: center; gap: .5em; }' +
+			'#shunt-status .shunt-dot { width: .6em; height: .6em; border-radius: 50%; ' +
+			'flex: 0 0 auto; background: var(--shunt-muted); }' +
+			'#shunt-status .shunt-dot-ok { background: var(--shunt-ok); }' +
+			'#shunt-status .shunt-dot-warn { background: var(--shunt-warn); }' +
+			'#shunt-status .shunt-dot-off { background: var(--shunt-off); }' +
+			'#shunt-status .shunt-issues { margin: 0; padding-left: 1.2em; }' +
+			'#shunt-status .shunt-key { color: var(--shunt-muted); ' +
+			'font-family: monospace; font-size: .8em; margin-left: .6em; }' +
+			'#shunt-status .shunt-hit { color: var(--shunt-ok); }');
+
+		const setNodes = (id, nodes) => {
+			const el = document.getElementById(id);
+			if (el) {
+				dom.content(el, nodes);
+			}
+		};
+
+		// Shown once, not on every poll tick. rp_filter is a box-wide security
+		// setting shunt does not change; a strict value silently drops its
+		// traffic, so the UI names it where the log would otherwise be the
+		// only place. Points at the docs rather than offering a button,
+		// because the change belongs to the administrator.
+		let rpWarned = false;
+
+		const update = (st) => {
+			const svc = st ? st.service : null;
+			// Devices the kernel would drop marked traffic on, on the status
+			// root. Empty when all is loose, when each policy device is loose
+			// itself, or when rp_filter_manage has set them - the daemon reads
+			// the live value, so an enabled switch simply yields an empty list.
+			const blocked = (st && st.rp_filter_blocked) || [];
+
+			if (blocked.length && !rpWarned) {
+				rpWarned = true;
+				ui.addNotification(
+					_('Reverse path filtering is strict'),
+					E('p', {}, [
+						_('Strict rp_filter will drop shunt\'s marked traffic on %s. Set rp_filter to 2 on the policy interface, enable rp_filter_manage to have shunt do it, or loosen it box-wide; see the README.').format(
+							blocked.join(', '))
+					]),
+					'warning');
+			}
+
+			setNodes('shunt-state', renderState(st));
+			setNodes('shunt-version', E('span', {}, ['%s / %s'.format(
+				pkgs['luci-app-shunt'] || _('n/a'), pkgs['shunt'] || _('n/a'))]));
+			setNodes('shunt-learned', E('span', {}, [
+				svc ? fmtCount(svc.dedupe) : '-']));
+			setNodes('shunt-poll', E('span', {}, [svc?.poll
+				? (svc.poll.resolv
+					? _('%d name(s) every %ds').format(svc.poll.names, svc.poll.interval)
+					: _('unavailable - ucode-mod-resolv missing'))
+				: '-']));
+			setNodes('shunt-policies', renderPolicies(st));
+			setNodes('shunt-snoop', renderSnoop(svc));
+			setNodes('shunt-issues', renderIssues(st));
+		};
+
+		// TypedSection: `config global` is anonymous, so there is no section
+		// named 'global' to bind to.
+		o = m.section(form.TypedSection, 'global');
+		o.anonymous = true;
+		o.addremove = false;
+		o.render = L.bind(function () {
+			return E('div', { 'id': 'shunt-status' }, [
+				style,
+				E('div', { 'class': 'shunt-grid' }, [
+					card(_('Service'), E('span', { 'id': 'shunt-state' }, '-'),
+						E('span', {}, [
+							_('Version'), ': ',
+							E('span', { 'id': 'shunt-version' }, '-')
+						])),
+					card(_('Learned addresses'),
+						E('span', { 'id': 'shunt-learned' }, '-'),
+						_('across all policies')),
+					card(_('Poll'), E('span', { 'id': 'shunt-poll' }, '-'),
+						_('wildcards are covered by the observer only'))
+				]),
+				E('div', { 'id': 'shunt-policies' }, ''),
+				E('div', { 'id': 'shunt-snoop' }, ''),
+				E('div', { 'id': 'shunt-issues' }, '')
+			]);
+		}, this);
+
+		poll.add(function () {
+			return L.resolveDefault(getStatus(), null).then(update);
+		}, 2);
+
+		// The status subtree only exists once m.render() has resolved and
+		// View.__init__ has attached the nodes; a direct call here would find
+		// no ids and leave the cards on '-' until the first poll tick.
+		requestAnimationFrame(function () {
+			update(result[0]);
+		});
+
+		s = m.section(form.TypedSection, 'global', _('Settings'));
+		s.anonymous = true;
+		s.addremove = false;
+		s.tab('general', _('General Settings'));
+		s.tab('snoop', _('DNS Observer Settings'));
+
+		o = s.taboption('general', form.Flag, 'enabled', _('Enabled'),
+			_('Enable the shunt service.'));
+		o.rmempty = false;
+
+		o = s.taboption('general', form.Flag, 'debug', _('Debug Logging'),
+			_('Log every observed DNS answer and every set write. Useful for a bug report, noisy in normal operation - on a router running adblock roughly half of all answers are error replies, and each one gets a line.'));
+		o.rmempty = false;
+
+		o = s.taboption('general', form.Flag, 'rp_filter_manage', _('Manage rp_filter'),
+			_('Set rp_filter to 2 on shunt\'s own policy interfaces, at start and when one comes up.'));
+		o.rmempty = false;
+
+		o = s.taboption('general', form.Value, 'poll_interval', _('Poll Interval'),
+			_('Seconds between poll cycles.'));
+		o.datatype = 'and(uinteger,min(30))';
+		o.placeholder = '300';
+
+		o = s.taboption('general', form.Value, 'entry_ttl', _('Maximum Entry Lifetime'),
+			_('Ceiling in seconds for a learned address in its Set. An address learned from a DNS answer expires with the TTL of that answer, clamped to the range 60 to this value; a polled address uses this value. Keep it well above the poll interval.'));
+		o.datatype = 'and(uinteger,min(60))';
+		o.placeholder = '1200';
+		o.validate = function (section_id, value) {
+			const iv = this.map.lookupOption('poll_interval', section_id);
+			const interval = (iv && iv[0]) ? (iv[0].formvalue(section_id) || 300) : 300;
+
+			if (value && +value < 2 * +interval) {
+				return _('Should be at least twice the poll interval (%d), otherwise entries expire between cycles.').format(2 * interval);
+			}
+
+			return true;
+		};
+
+		o = s.taboption('snoop', form.Flag, 'snoop', _('Passive DNS Observer'),
+			_('Read DNS answers as they pass the LAN device, whichever resolver produced them. Required for wildcard domains, which cannot be resolved ahead of time.'));
+		o.rmempty = false;
+
+		o = s.taboption('snoop', widgets.DeviceSelect, 'snoop_device',
+			_('Observed Devices'),
+			_('The LAN device the DNS answers cross on their way to the clients, normally br-lan. One entry per network segment, see the README.'));
+		o.default = 'br-lan';
+		o.multiple = true;
+		o.noaliases = true;
+
+		s = m.section(form.TypedSection, 'global');
+		s.anonymous = true;
+		s.addremove = false;
+		s.render = L.bind(function () {
+			return E('div', { 'class': 'cbi-page-actions' }, [
+				E('button', {
+					'class': 'btn cbi-button cbi-button-negative important',
+					'style': 'float:none;margin-right:.4em;',
+					'click': ui.createHandlerFn(this, function () {
+						return handleAction('stop');
+					})
+				}, [_('Stop')]),
+				E('button', {
+					'class': 'btn cbi-button cbi-button-positive important',
+					'style': 'float:none',
+					'click': ui.createHandlerFn(this, function () {
+						return handleAction('restart');
+					})
+				}, [_('Save & Restart')])
+			]);
+		});
+
+		return m.render();
+	},
+
+	handleSaveApply: null,
+	handleSave: null,
+	handleReset: null
+});
diff --git a/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js b/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js
new file mode 100644
index 000000000..0953a07c0
--- /dev/null
+++ b/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js
@@ -0,0 +1,223 @@
+'use strict';
+'require dom';
+'require view';
+'require fs';
+'require ui';
+'require uci';
+'require form';
+
+function handleAction(ev) {
+	if (ev === 'restart') {
+		const map = document.querySelector('.cbi-map');
+
+		return dom.callClassMethod(map, 'save')
+			.then(function () {
+				return Promise.all([
+					uci.changes(),
+					fs.exec('/etc/init.d/shunt', ['running'])
+				]);
+			})
+			.then(function (res) {
+				const pending = res[0] && Object.keys(res[0]).length;
+				const running = res[1] && res[1].code === 0;
+
+				if (pending && running) {
+					L.bind(ui.changes.apply, ui.changes)();
+					return null;
+				}
+
+				if (pending) {
+					return uci.apply().then(function () {
+						if (ui.changes.setIndicator) {
+							ui.changes.setIndicator(0);
+						}
+						return fs.exec_direct('/etc/init.d/shunt', [ev]);
+					});
+				}
+				return fs.exec_direct('/etc/init.d/shunt', [ev]);
+			})
+			.catch(function (e) {
+				ui.addNotification(null,
+					E('p', {}, [_('Apply failed: %s').format(e)]), 'error');
+			});
+	}
+	return fs.exec_direct('/etc/init.d/shunt', [ev]);
+}
+
+// `shunt refresh` re-reads the domain files in the running daemon; its
+// output is the count and the issues, worth showing as is.
+function handleRefresh() {
+	return fs.exec('/usr/sbin/shunt', ['refresh'])
+		.then(function (res) {
+			const text = ((res.stdout || '') + (res.stderr || '')).trim();
+
+			ui.addNotification(null, E('pre', {}, [text || _('No output')]),
+				res.code === 0 ? 'info' : 'warning');
+		})
+		.catch(function (e) {
+			ui.addNotification(null,
+				E('p', {}, [_('Refresh failed: %s').format(e)]), 'error');
+		});
+}
+
+return view.extend({
+	load: function () {
+		return Promise.all([
+			uci.load('shunt').catch(() => 0),
+			uci.load('network').catch(() => 0)
+		]);
+	},
+
+	render: function () {
+		if (!uci.sections('shunt').length) {
+			ui.addNotification(null, E('p', _('No shunt config found!')), 'error');
+			return;
+		}
+
+		let m, s, o;
+
+		m = new form.Map('shunt', _('Policies'),
+			_('Evaluated top to bottom - the first policy a packet matches \
+				wins. Within one policy the selectors are ANDed: source plus domain means only that client, and only to those domains.'));
+		s = m.section(form.GridSection, 'policy');
+		s.addremove = true;
+		s.anonymous = false;
+		s.sortable = true;
+		s.nodescriptions = true;
+		s.addbtntitle = _('Add policy');
+
+		// The section name becomes an nftables identifier, so it is validated
+		// where it is typed rather than silently skipped later.
+		s.renderSectionAdd = function (extra_class) {
+			const el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments);
+			const nameEl = el.querySelector('.cbi-section-create-name');
+
+			if (nameEl) {
+				ui.addValidator(nameEl, 'and(uciname,maxlength(24))', true);
+			}
+
+			return el;
+		};
+
+		o = s.option(form.Flag, 'enabled', _('Enabled'));
+		o.rmempty = false;
+		o.default = '1';
+		o.editable = true;
+
+		o = s.option(form.ListValue, 'action', _('Action'),
+			_('Route the selected traffic into the interface below, or \
+				bypass it - a bypass policy marks nothing and exempts the traffic from every policy after it.'));
+		o.value('route', _('Route into a policy interface'));
+		o.value('bypass', _('Bypass - leave the traffic alone'));
+		o.default = 'route';
+		o.editable = true;
+
+		o = s.option(form.Value, 'interface', _('Interface'),
+			_('The device or logical interface this policy routes into. A \
+				netifd name is resolved to its device; any other device name is used as entered.'));
+		o.rmempty = false;
+		o.depends('action', 'route');
+
+		uci.sections('network', 'interface').forEach(function (n) {
+			if (n['.name'] !== 'loopback') {
+				o.value(n['.name'], '%s (%s)'.format(n['.name'], _('interface')));
+			}
+		});
+
+		o = s.option(form.ListValue, 'fallback', _('Fallback Behavior'));
+		o.value('main', _('Fall through to the normal uplink'));
+		o.value('block', _('Block the traffic (killswitch)'));
+		o.default = 'main';
+		o.depends('action', 'route');
+
+		o = s.option(form.Flag, 'keep_local', _('Keep Local Traffic'),
+			_('Traffic to networks the routing table already has a route for \
+				- your other subnets, your static routes - stays on the normal path instead of entering the policy interface. Turn this off only for a hermetic killswitch.'));
+		o.default = '1';
+		o.rmempty = false;
+		o.modalonly = true;
+		o.depends('action', 'route');
+
+		o = s.option(form.DynamicList, 'src', _('Source Addresses'),
+			_('Client addresses or prefixes this policy applies to. Leave empty to apply to every client.'));
+		o.datatype = 'ipaddr';
+		o.modalonly = true;
+
+		o = s.option(form.DynamicList, 'src_mac', _('Source MAC Addresses'),
+			_('Client MACs this policy applies to, ORed with the addresses above.'));
+		o.datatype = 'macaddr';
+		o.modalonly = true;
+
+		o = s.option(form.MultiValue, 'proto', _('Protocols'),
+			_('Restrict to tcp, udp or both. A port without a protocol covers both.'));
+		o.value('tcp', 'tcp');
+		o.value('udp', 'udp');
+		o.modalonly = true;
+
+		o = s.option(form.DynamicList, 'dport', _('Destination Ports'),
+			_('Single ports or ranges like 8000-8080, ANDed with the addresses below.'));
+		o.datatype = 'or(port, portrange)';
+		o.modalonly = true;
+
+		o = s.option(form.DynamicList, 'dst', _('Destination Addresses'),
+			_('Destination addresses or prefixes to route into this policy.'));
+		o.datatype = 'ipaddr';
+		o.modalonly = true;
+
+		o = s.option(form.DynamicList, 'domain', _('Domains'),
+			_('example.com matches that name only, *.example.com matches its \
+				subdomains but not the apex - list both to cover both.'));
+		o.modalonly = true;
+
+		o = s.option(form.DynamicList, 'domain_file', _('Domain Files'),
+			_('Files with one domain pattern per line, for large or \
+				community-maintained lists. A bare name lives under /tmp/shunt, an absolute path is used as given. Entries from a file are learned by the DNS observer only, never polled; after the file changed, refresh below.'));
+		o.placeholder = 'community.txt';
+		o.modalonly = true;
+		o.validate = function (section_id, value) {
+			if (value && (/\s/.test(value) || (value.charAt(0) !== '/' && value.indexOf('/') !== -1))) {
+				return _('Expecting a bare file name or an absolute path, without whitespace');
+			}
+			return true;
+		};
+
+		o = s.option(form.Value, 'gw4', _('IPv4 Gateway Override'),
+			_('Only needed when the gateway discovered from netifd is wrong. \
+				Point to point interfaces need no gateway at all.'));
+		o.datatype = 'ip4addr';
+		o.modalonly = true;
+
+		o = s.option(form.Value, 'gw6', _('IPv6 Gateway Override'));
+		o.datatype = 'ip6addr';
+		o.modalonly = true;
+
+		s = m.section(form.TypedSection, 'global');
+		s.anonymous = true;
+		s.addremove = false;
+		s.render = L.bind(function () {
+			return E('div', { 'class': 'cbi-page-actions' }, [
+				E('button', {
+					'class': 'btn cbi-button cbi-button-positive important',
+					'style': 'float:none',
+					'click': ui.createHandlerFn(this, function () {
+						return handleAction('restart');
+					})
+				}, [_('Save & Restart')]),
+				' ',
+				E('button', {
+					'class': 'btn cbi-button cbi-button-action',
+					'style': 'float:none',
+					'click': ui.createHandlerFn(this, function () {
+						return handleRefresh();
+					})
+				}, [_('Refresh Domain Files')])
+			]);
+		});
+
+		return m.render();
+	},
+
+	handleSaveApply: null,
+	handleSave: null,
+	handleReset: null
+});
diff --git a/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js b/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js
new file mode 100644
index 000000000..f4422361e
--- /dev/null
+++ b/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js
@@ -0,0 +1,263 @@
+'use strict';
+'require view';
+'require dom';
+'require ui';
+'require uci';
+'require rpc';
+
+const getSets = rpc.declare({
+	object: 'luci.shunt',
+	method: 'sets',
+	params: ['policy']
+});
+
+function setKind(name) {
+	return name.substring(0, 1);
+}
+
+function setPolicy(name) {
+	return name.substring(name.substring(0, 1) === 'm' ? 2 : 3);
+}
+
+function fmtExpiry(sec) {
+	if (sec == null) {
+		return '-';
+	}
+	if (sec >= 3600) {
+		return _('%dh %dm').format(Math.floor(sec / 3600),
+			Math.floor((sec % 3600) / 60));
+	}
+	if (sec >= 60) {
+		return _('%dm %ds').format(Math.floor(sec / 60), sec % 60);
+	}
+	return _('%ds').format(sec);
+}
+
+function fmtNum(n) {
+	if (n == null) {
+		return '-';
+	}
+	return '%d'.format(n);
+}
+
+function renderCards(sets, kinds, title, empty_hint) {
+	const byPolicy = {};
+
+	Object.keys(sets).forEach(function (name) {
+		if (kinds.indexOf(setKind(name)) < 0) {
+			return;
+		}
+
+		const policy = setPolicy(name);
+
+		if (!byPolicy[policy]) {
+			byPolicy[policy] = [];
+		}
+
+		sets[name].forEach(function (e) {
+			byPolicy[policy].push(e);
+		});
+	});
+
+	const policies = Object.keys(byPolicy).filter(function (p) {
+		return byPolicy[p].length > 0;
+	}).sort();
+
+	if (!policies.length) {
+		return empty_hint
+			? E('div', { 'class': 'shunt-block' }, [
+				E('div', { 'class': 'shunt-label' }, title),
+				E('div', { 'class': 'shunt-sub' }, empty_hint)
+			])
+			: '';
+	}
+
+	const cards = policies.map(function (policy) {
+		return E('div', { 'class': 'shunt-card' }, [
+			E('div', { 'class': 'shunt-card-title' }, [policy]),
+			E('div', { 'class': 'shunt-addrs' },
+				byPolicy[policy].sort(function (a, b) {
+					return String(a.addr).localeCompare(String(b.addr));
+				}).map(function (e) {
+					return E('div', {}, [
+						e.addr,
+						e.packets ? E('span', { 'class': 'shunt-hits' },
+							_('%d pkt matched').format(e.packets)) : ''
+					]);
+				}))
+		]);
+	});
+
+	return E('div', { 'class': 'shunt-block' }, [
+		E('div', { 'class': 'shunt-label' }, title),
+		E('div', { 'class': 'shunt-grid' }, cards)
+	]);
+}
+
+function renderLearned(sets) {
+	const rows = [];
+
+	Object.keys(sets).forEach(function (name) {
+		if (setKind(name) !== 'd') {
+			return;
+		}
+
+		const policy = setPolicy(name);
+
+		sets[name].forEach(function (e) {
+			rows.push({
+				policy: policy,
+				addr: e.addr,
+				expires: e.expires,
+				packets: e.packets,
+				bytes: e.bytes
+			});
+		});
+	});
+
+	rows.sort(function (a, b) {
+		return a.policy.localeCompare(b.policy)
+			|| (b.packets || 0) - (a.packets || 0)
+			|| String(a.addr).localeCompare(String(b.addr));
+	});
+
+	if (!rows.length) {
+		return E('div', { 'class': 'shunt-block' }, [
+			E('div', { 'class': 'shunt-label' }, _('Learned addresses')),
+			E('div', { 'class': 'shunt-sub' }, _('Nothing learned yet. The service fills these from its poll cycle and from observed DNS answers.'))
+		]);
+	}
+
+	const tbl = E('table', { 'class': 'table shunt-table' }, [
+		E('tr', { 'class': 'tr table-titles' }, [
+			E('th', { 'class': 'th' }, _('Policy')),
+			E('th', { 'class': 'th' }, _('Address')),
+			E('th', { 'class': 'th' }, _('Expires')),
+			E('th', { 'class': 'th' }, _('Packets routed')),
+			E('th', { 'class': 'th' }, _('Bytes routed'))
+		])
+	]);
+
+	cbi_update_table(tbl, rows.map(function (r) {
+		return [
+			r.policy,
+			r.addr,
+			fmtExpiry(r.expires),
+			fmtNum(r.packets),
+			fmtNum(r.bytes)
+		];
+	}));
+
+	return E('div', { 'class': 'shunt-block' }, [
+		E('div', { 'class': 'shunt-label' },
+			_('Learned addresses (%d)').format(rows.length)),
+		tbl
+	]);
+}
+
+return view.extend({
+	load: function () {
+		return Promise.all([
+			uci.load('shunt').catch(() => 0),
+			L.resolveDefault(getSets(''), {})
+		]);
+	},
+
+	render: function (result) {
+		const self = this;
+
+		const render_sets = function (data) {
+			const sets = (data && data.sets) || {};
+			const target = document.getElementById('shunt-sets');
+
+			if (!target) {
+				return;
+			}
+
+			dom.content(target, [
+				renderCards(sets, ['c', 'm'], _('Client Selectors'),
+					_('No client is selected, so every client is covered.')),
+				renderCards(sets, ['s'], _('Static Destinations'), null),
+				renderLearned(sets)
+			]);
+		};
+
+		const reload = function () {
+			const sel = document.getElementById('shunt-policy');
+
+			return L.resolveDefault(getSets(sel ? sel.value : ''), {})
+				.then(render_sets);
+		};
+
+		const options = [E('option', { 'value': '' }, _('all policies'))]
+			.concat(uci.sections('shunt', 'policy').map(function (p) {
+				return E('option', { 'value': p['.name'] }, [p['.name']]);
+			}));
+
+		const style = E('style', { 'type': 'text/css' },
+			'#shunt-sets {' +
+			'--shunt-card-bg: rgba(128,128,128,.07);' +
+			'--shunt-card-border: rgba(128,128,128,.28);' +
+			'--shunt-muted: GrayText;' +
+			'}' +
+			'#shunt-sets .shunt-block { margin-bottom: 1.2em; }' +
+			'#shunt-sets .shunt-label { font-size: .85em; ' +
+			'color: var(--shunt-muted); margin-bottom: .4em; }' +
+			'#shunt-sets .shunt-sub { font-size: .85em; color: var(--shunt-muted); }' +
+			'#shunt-sets .shunt-grid { display: grid; gap: .75em; ' +
+			'grid-template-columns: repeat(auto-fit, minmax(min(16em, 100%), 1fr)); }' +
+			'#shunt-sets .shunt-card { background: var(--shunt-card-bg); ' +
+			'border: 1px solid var(--shunt-card-border); border-radius: 8px; ' +
+			'padding: .7em .9em; min-width: 0; }' +
+			'#shunt-sets .shunt-card-title { font-weight: bold; margin-bottom: .3em; }' +
+			'#shunt-sets .shunt-addrs { font-family: monospace; font-size: .9em; ' +
+			'overflow-wrap: anywhere; }' +
+			'#shunt-sets .shunt-hits { color: var(--shunt-muted); ' +
+			'font-size: .85em; margin-left: .6em; }' +
+			'#shunt-sets .shunt-table { table-layout: fixed; width: 100%; }' +
+			'#shunt-sets .shunt-table th:nth-child(1),' +
+			'#shunt-sets .shunt-table td:nth-child(1) { width: 15%; }' +
+			'#shunt-sets .shunt-table th:nth-child(2),' +
+			'#shunt-sets .shunt-table td:nth-child(2) { width: 37%; ' +
+			'overflow-wrap: anywhere; }' +
+			'#shunt-sets .shunt-table th:nth-child(3),' +
+			'#shunt-sets .shunt-table td:nth-child(3) { width: 16%; }' +
+			'#shunt-sets .shunt-table th:nth-child(4),' +
+			'#shunt-sets .shunt-table td:nth-child(4) { width: 16%; }' +
+			'#shunt-sets .shunt-table th:nth-child(5),' +
+			'#shunt-sets .shunt-table td:nth-child(5) { width: 16%; }');
+
+		const page = E('div', { 'class': 'cbi-map' }, [
+			style,
+			E('h2', {}, _('Set Reporting')),
+			E('div', { 'class': 'cbi-section' }, [
+				E('div', { 'class': 'cbi-section-descr' },
+					_('What the nftables Sets currently hold. Counters are reset whenever an entry is refreshed, so they show recent activity, not a lifetime total.'))
+			]),
+			E('div', { 'id': 'shunt-sets' }, ''),
+			E('div', { 'class': 'cbi-page-actions' }, [
+				E('select', {
+					'id': 'shunt-policy',
+					'class': 'cbi-input-select',
+					'style': 'float:none;margin-right:.4em;width:auto;',
+					'change': ui.createHandlerFn(self, reload)
+				}, options),
+				E('button', {
+					'class': 'btn cbi-button cbi-button-action important',
+					'style': 'float:none',
+					'click': ui.createHandlerFn(self, reload)
+				}, [_('Refresh')])
+			])
+		]);
+
+		requestAnimationFrame(function () {
+			render_sets(result[1]);
+		});
+
+		return page;
+	},
+
+	handleSaveApply: null,
+	handleSave: null,
+	handleReset: null
+});
diff --git a/luci-app-shunt/po/templates/shunt.pot b/luci-app-shunt/po/templates/shunt.pot
new file mode 100644
index 000000000..dc6ceb086
--- /dev/null
+++ b/luci-app-shunt/po/templates/shunt.pot
@@ -0,0 +1,601 @@
+msgid ""
+msgstr "Content-Type: text/plain; charset=UTF-8"
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:358
+msgid "%d name(s) every %ds"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:198
+msgid ""
+"%d of %d observed responses were not used. That is normal: the observer sees "
+"every answer on the network, and only the ones for a domain you routed are "
+"of any interest."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:85
+msgid "%d pkt matched"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:27
+msgid "%dh %dm"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:31
+msgid "%dm %ds"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:33
+msgid "%ds"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:211
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:107
+msgid "Action"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:87
+msgid "Add policy"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:134
+msgid "Address"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:130
+msgid "Answer carried no usable address"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:193
+msgid "Answers used for a policy"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:56
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:41
+msgid "Apply failed: %s"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:129
+msgid "Block the traffic (killswitch)"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:111
+msgid "Bypass - leave the traffic alone"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:137
+msgid "Bytes routed"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:427
+msgid ""
+"Ceiling in seconds for a learned address in its Set. An address learned from "
+"a DNS answer expires with the TTL of that answer, clamped to the range 60 to "
+"this value; a polled address uses this value. Keep it well above the poll "
+"interval."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:147
+msgid "Client MACs this policy applies to, ORed with the addresses above."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:178
+msgid "Client Selectors"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:142
+msgid ""
+"Client addresses or prefixes this policy applies to. Leave empty to apply to "
+"every client."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:138
+msgid "Compression pointer in the question"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:407
+msgid "DNS Observer Settings"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:188
+msgid "DNS responses observed on %s since the service started"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:413
+msgid "Debug Logging"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:162
+msgid "Destination Addresses"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:157
+msgid "Destination Ports"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:163
+msgid "Destination addresses or prefixes to route into this policy."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:172
+msgid "Domain Files"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:128
+msgid "Domain is in no policy"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:167
+msgid "Domains"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:410
+msgid "Enable the shunt service."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:409
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:102
+msgid "Enabled"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:132
+msgid "Error reply, e.g. NXDOMAIN"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:80
+msgid ""
+"Evaluated top to bottom - the first policy a packet matches wins. Within one "
+"policy the selectors are ANDed: source plus domain means only that client, "
+"and only to those domains."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:179
+msgid "Expecting a bare file name or an absolute path, without whitespace"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:135
+msgid "Expires"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:150
+msgid "Extension header chain too long"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:128
+msgid "Fall through to the normal uplink"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:217
+msgid "Fallback"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:127
+msgid "Fallback Behavior"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:173
+msgid ""
+"Files with one domain pattern per line, for large or community-maintained "
+"lists. A bare name lives under /tmp/shunt, an absolute path is used as "
+"given. Entries from a file are learned by the DNS observer only, never "
+"polled; after the file changed, refresh below."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:145
+msgid "Frame ends mid-structure"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:406
+msgid "General Settings"
+msgstr ""
+
+#: applications/luci-app-shunt/root/usr/share/rpcd/acl.d/luci-app-shunt.json:3
+msgid "Grant access to LuCI app shunt"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:149
+msgid "IP fragment"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:184
+msgid "IPv4 Gateway Override"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:190
+msgid "IPv6 Gateway Override"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:148
+msgid "Inconsistent IP header length"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:152
+msgid "Inconsistent UDP length"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:212
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:115
+msgid "Interface"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:133
+msgid "Keep Local Traffic"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:380
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:126
+msgid "Learned addresses"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:153
+msgid "Learned addresses (%d)"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:414
+msgid ""
+"Log every observed DNS answer and every set write. Useful for a bug report, "
+"noisy in normal operation - on a router running adblock roughly half of all "
+"answers are error replies, and each one gets a line."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:417
+msgid "Manage rp_filter"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:213
+msgid "Mark"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:426
+msgid "Maximum Entry Lifetime"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:136
+msgid "Message ends mid-structure"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:137
+msgid "Message too long"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:141
+msgid "Name has a byte outside a-z 0-9 - _"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:140
+msgid "Name too long"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:146
+msgid "Neither IPv4 nor IPv6"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/logtemplate.js:33
+msgid "No %s related logs yet!"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:179
+msgid "No client is selected, so every client is covered."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:54
+msgid "No output"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:205
+msgid "No policy is active."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:271
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:73
+msgid "No shunt config found!"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:151
+msgid "Not UDP"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:133
+msgid "Not a response"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:129
+msgid "Not an address query"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:135
+msgid "Not exactly one question"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:127
+msgid ""
+"Nothing learned yet. The service fills these from its poll cycle and from "
+"observed DNS answers."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:446
+msgid "Observed Devices"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:185
+msgid ""
+"Only needed when the gateway discovered from netifd is wrong. Point to point "
+"interfaces need no gateway at all."
+msgstr ""
+
+#: applications/luci-app-shunt/root/usr/share/luci/menu.d/luci-app-shunt.json:23
+msgid "Overview"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:136
+msgid "Packets routed"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:441
+msgid "Passive DNS Observer"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:79
+#: applications/luci-app-shunt/root/usr/share/luci/menu.d/luci-app-shunt.json:31
+msgid "Policies"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:210
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:133
+msgid "Policy"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:278
+msgid ""
+"Policy based routing by mac, source, destination and domain. For further "
+"information please check the %s."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:383
+msgid "Poll"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:421
+msgid "Poll Interval"
+msgstr ""
+
+#: applications/luci-app-shunt/root/usr/share/luci/menu.d/luci-app-shunt.json:47
+msgid "Processing Log"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:151
+msgid "Protocols"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:442
+msgid ""
+"Read DNS answers as they pass the LAN device, whichever resolver produced "
+"them. Required for wildcard domains, which cannot be resolved ahead of time."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:142
+msgid "Record length does not fit the type"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:249
+msgid "Refresh"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:213
+msgid "Refresh Domain Files"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:59
+msgid "Refresh failed: %s"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:252
+msgid "Rejected settings"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:139
+msgid "Reserved label type"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:152
+msgid "Restrict to tcp, udp or both. A port without a protocol covers both."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:343
+msgid "Reverse path filtering is strict"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:110
+msgid "Route into a policy interface"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:108
+msgid ""
+"Route the selected traffic into the interface below, or bypass it - a bypass "
+"policy marks nothing and exempts the traffic from every policy after it."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:216
+msgid "Routes"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:215
+msgid "Rules"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:470
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:205
+msgid "Save & Restart"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:422
+msgid "Seconds between poll cycles."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:375
+msgid "Service"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:232
+#: applications/luci-app-shunt/root/usr/share/luci/menu.d/luci-app-shunt.json:39
+msgid "Set Reporting"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:418
+msgid ""
+"Set rp_filter to 2 on shunt's own policy interfaces, at start and when one "
+"comes up."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:403
+msgid "Settings"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:435
+msgid ""
+"Should be at least twice the poll interval (%d), otherwise entries expire "
+"between cycles."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:158
+msgid "Single ports or ranges like 8000-8080, ANDed with the addresses below."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:141
+msgid "Source Addresses"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:146
+msgid "Source MAC Addresses"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:180
+msgid "Static Destinations"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:463
+msgid "Stop"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:345
+msgid ""
+"Strict rp_filter will drop shunt's marked traffic on %s. Set rp_filter to 2 "
+"on the policy interface, enable rp_filter_manage to have shunt do it, or "
+"loosen it box-wide; see the README."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:214
+msgid "Table"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:447
+msgid ""
+"The LAN device the DNS answers cross on their way to the clients, normally "
+"br-lan. One entry per network segment, see the README."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:116
+msgid ""
+"The device or logical interface this policy routes into. A netifd name is "
+"resolved to its device; any other device name is used as entered."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/logtemplate.js:44
+msgid "The syslog output, pre-filtered for messages related to: %s"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:254
+msgid ""
+"These entries were skipped. Everything else was applied - a rejected entry "
+"never takes the service down."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:143
+msgid "Too many answers"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:147
+msgid "Too many stacked VLAN tags"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:134
+msgid ""
+"Traffic to networks the routing table already has a route for - your other "
+"subnets, your static routes - stays on the normal path instead of entering "
+"the policy interface. Turn this off only for a hermetic killswitch."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:134
+msgid "Truncated response"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:377
+msgid "Version"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:235
+msgid ""
+"What the nftables Sets currently hold. Counters are reset whenever an entry "
+"is refreshed, so they show recent activity, not a lifetime total."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:382
+msgid "across all policies"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:192
+msgid "all policies"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:168
+msgid ""
+"example.com matches that name only, *.example.com matches its subdomains but "
+"not the apex - list both to cover both."
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:123
+msgid "interface"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:353
+msgid "n/a"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:91
+msgid "no answer from the backend"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:279
+msgid "online documentation"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:116
+msgid "ruleset present, service not running"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:104
+msgid "ruleset re-applied %d time(s)"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:110
+msgid "running, but no ruleset in the kernel"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:102
+msgid "running, policy applied"
+msgstr ""
+
+#: applications/luci-app-shunt/root/usr/share/luci/menu.d/luci-app-shunt.json:3
+msgid "shunt"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:121
+msgid "stopped"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:359
+msgid "unavailable - ucode-mod-resolv missing"
+msgstr ""
+
+#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:384
+msgid "wildcards are covered by the observer only"
+msgstr ""
diff --git a/luci-app-shunt/root/etc/uci-defaults/95-luci-app-shunt-housekeeping b/luci-app-shunt/root/etc/uci-defaults/95-luci-app-shunt-housekeeping
new file mode 100644
index 000000000..e6e19f9f2
--- /dev/null
+++ b/luci-app-shunt/root/etc/uci-defaults/95-luci-app-shunt-housekeeping
@@ -0,0 +1,4 @@
+#!/bin/sh
+rm -f /var/luci-indexcache.*.json
+[ -x "/etc/init.d/rpcd" ] && /etc/init.d/rpcd reload
+exit 0
diff --git a/luci-app-shunt/root/usr/share/luci/menu.d/luci-app-shunt.json b/luci-app-shunt/root/usr/share/luci/menu.d/luci-app-shunt.json
new file mode 100644
index 000000000..db0664cb1
--- /dev/null
+++ b/luci-app-shunt/root/usr/share/luci/menu.d/luci-app-shunt.json
@@ -0,0 +1,54 @@
+{
+	"admin/services/shunt": {
+		"title": "shunt",
+		"order": "65",
+		"action": {
+			"type": "alias",
+			"path": "admin/services/shunt/overview"
+		},
+		"depends": {
+			"acl": [
+				"luci-app-shunt"
+			],
+			"fs": {
+				"/usr/sbin/shunt": "executable",
+				"/etc/init.d/shunt": "executable"
+			},
+			"uci": {
+				"shunt": true
+			}
+		}
+	},
+	"admin/services/shunt/overview": {
+		"title": "Overview",
+		"order": 10,
+		"action": {
+			"type": "view",
+			"path": "shunt/overview"
+		}
+	},
+	"admin/services/shunt/policies": {
+		"title": "Policies",
+		"order": 20,
+		"action": {
+			"type": "view",
+			"path": "shunt/policies"
+		}
+	},
+	"admin/services/shunt/setreport": {
+		"title": "Set Reporting",
+		"order": 30,
+		"action": {
+			"type": "view",
+			"path": "shunt/setreport"
+		}
+	},
+	"admin/services/shunt/logread": {
+		"title": "Processing Log",
+		"order": 40,
+		"action": {
+			"type": "view",
+			"path": "shunt/logread"
+		}
+	}
+}
diff --git a/luci-app-shunt/root/usr/share/rpcd/acl.d/luci-app-shunt.json b/luci-app-shunt/root/usr/share/rpcd/acl.d/luci-app-shunt.json
new file mode 100644
index 000000000..c217953a2
--- /dev/null
+++ b/luci-app-shunt/root/usr/share/rpcd/acl.d/luci-app-shunt.json
@@ -0,0 +1,47 @@
+{
+	"luci-app-shunt": {
+		"description": "Grant access to LuCI app shunt",
+		"write": {
+			"uci": [
+				"shunt"
+			]
+		},
+		"read": {
+			"ubus": {
+				"luci.shunt": [
+					"status",
+					"sets"
+				],
+				"rpc-sys": [
+					"packagelist"
+				]
+			},
+			"uci": [
+				"shunt"
+			],
+			"file": {
+				"/etc/init.d/shunt stop": [
+					"exec"
+				],
+				"/etc/init.d/shunt start": [
+					"exec"
+				],
+				"/etc/init.d/shunt restart": [
+					"exec"
+				],
+				"/etc/init.d/shunt running": [
+					"exec"
+				],
+				"/usr/sbin/shunt refresh": [
+					"exec"
+				]
+			},
+			"cgi-io": [
+				"exec"
+			],
+			"log": [
+				"read"
+			]
+		}
+	}
+}
\ No newline at end of file
diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile
index ebe75e392..691226701 100644
--- a/luci-theme-footstrap/Makefile
+++ b/luci-theme-footstrap/Makefile
@@ -1,5 +1,5 @@
 #
-# Copyright (C) 2026
+# Copyright (C) 2026 Ivan Kvashonkin
 #
 # This is free software, licensed under the Apache License, Version 2.0 .
 #
@@ -8,140 +8,86 @@ include $(TOPDIR)/rules.mk
 
 PKG_NAME:=luci-theme-footstrap
 # luci.mk keys the Build/Prepare hook name on LUCI_NAME, which defaults to the checkout's
-# directory name — a differently-named checkout symlinked into the feed would silently skip
-# the CSS build. Pin it.
+# directory name — a differently-named checkout symlinked into a feed would silently skip the
+# hook below. Pin it.
 LUCI_NAME:=luci-theme-footstrap
 
-# Version is git-derived; never hardcode PKG_VERSION. luci.mk falls back to PKG_SRC_VERSION,
-# which it always defines, when nothing here sets it — see docs/package.md for why no override
-# lives here.
+PKG_MAINTAINER:=Ivan Kvashonkin 
+
 LUCI_TITLE:=Footstrap Theme
-# +luci-base is the WHOLE list, and keeping it that way is the constraint. Nothing this package ships
-# fetches anything at run time, so there is no second name to justify — and the rule that got it here
-# stands: the one convenience tool that ever wanted a dep was `curl`, which is NOT in OpenWrt's
-# default set, on an 8 MB device, for one API call. Fall back to what the base image has
-# (uclient-fetch, jsonfilter, sha256sum) or do without.
+LUCI_DESCRIPTION:=A standalone LuCI theme with a collapsible sidebar or top bar, light/dark \
+	modes, a menu search and a client-side page router
+# +luci-base is the WHOLE list, and keeping it that way is a design constraint: the theme ships no
+# framework and every page it draws is drawn by luci-base's own view JS.
 LUCI_DEPENDS:=+luci-base
 LUCI_PKGARCH:=all
-# Else luci.mk defaults these to LuCI's own, and the package claims MAINTAINER "OpenWrt LuCI
-# community" / URL openwrt/luci.
-LUCI_MAINTAINER:=Ivan Kvashonkin 
-LUCI_URL:=https://github.com/VizzleTF/luci-theme-footstrap
 
-# CSS: ship verbatim. luci.mk would run csstidy over it, which is old enough to mangle :has(),
-# color-mix() and nested calc() and break the layout. build-css.sh minifies instead.
+# csstidy is old enough to mangle :has() and color-mix(), both of which this sheet uses heavily —
+# it silently drops the declarations and the layout goes with them. The stylesheet is already
+# minified (see the note below), so there is nothing for it to win back.
 LUCI_MINIFY_CSS:=0
 #
-# JS: the released package is built by owfeed (tools/stage.sh), never by this Makefile through
-# the SDK — so LUCI_MINIFY_JS keeps luci.mk's default jsmin here. jsmin decides regex-or-division
-# from a one-character lookback, and `n` (of `return`) and `>` (of `=>`) are not on its
-# allow-list, so `return /re/` makes it eat the rest of the file AND EXIT 0 (openwrt/luci#8299).
-# eslint's `wrap-regex` bans the shape in the source; tools/jsmin-verify.mjs proves jsmin's output
-# is token-identical to it.
+# The JS keeps luci.mk's default jsmin. The catch, and why the source is written the way it is:
+# jsmin decides `/` = regex-or-division from a ONE-character lookback, and `n` (of `return`) and
+# `>` (of `=>`) are not on its allow-list — so `return /re/` makes it eat the rest of the file AND
+# EXIT 0 (openwrt/luci#8299). Every regex literal in this theme's JS is therefore parenthesised.
 
-# Apache-2.0, and not a free choice: styles/base began as a fork of luci-theme-bootstrap's
-# cascade.css, the ucode templates derive from LuCI's own, and a few JS helpers are verbatim copies.
-# The notices travel with it.
+# Apache-2.0, and not a free choice: the stylesheet began as a fork of luci-theme-bootstrap's
+# cascade.css, the ucode templates derive from LuCI's own, and a few JS helpers are verbatim
+# copies. The notices travel with it.
 #
-# No OFL-1.1: the theme ships no Font Software, so declaring it would be a false statement about the
-# package's contents.
+# The theme carries NO webfonts: --fs-font-sans and --fs-font-mono name Manrope and JetBrains Mono
+# first and the system stack after, so a machine with either installed uses it and one without
+# falls through silently. Nothing here is Font Software, so there is no OFL half to declare.
 PKG_LICENSE:=Apache-2.0
 PKG_LICENSE_FILES:=LICENSE
 
-# /etc/config/footstrap is SHIPPED as an empty stub and WRITTEN AT RUNTIME: Appearance ->
-# "Save as default" has rpcd uci-set the router-wide axes into that very file. Without this define
-# the package manager owns it as an ordinary file and REPLACES it on upgrade, so the admin's saved
-# defaults are wiped by the theme's own one-click Update, silently and reported as success.
-# OpenWrt honours the define for BOTH formats (include/package-pack.mk), so one covers 24.10's opkg
-# and 25.12's apk alike. Any future root/etc/config/* must be listed here too.
+# /etc/config/footstrap is SHIPPED as an empty stub and WRITTEN AT RUNTIME: Appearance -> "Save as
+# default" has rpcd uci-set the router-wide axes into that very file (fs-prefs.js saveAsDefault()).
+# Without this define the package manager owns it as an ordinary file and REPLACES it on upgrade,
+# so an admin's saved defaults are wiped by an ordinary package upgrade, silently, reported as
+# success. OpenWrt honours this for BOTH formats (include/package-pack.mk: KEEP_$(1) -> apk
+# .conffiles, ipk CONTROL/conffiles). Any future root/etc/config/* must be listed here too.
 define Package/luci-theme-footstrap/conffiles
 /etc/config/footstrap
 endef
 
-# NO `rpcd reload` HERE, on install or removal — it used to be, and it was the more dangerous of
-# the two rpcd calls this package could make. Measured on an owrt2512 stand:
+# NO `rpcd reload` HERE, on install or on removal. Measured on a live router: `reload` is
+# procd_send_signal -> SIGHUP -> exec_self(), a full re-exec that re-scans the plugin dirs and
+# dlopen()s every .so, and a plugin that is absent or mid-write at that instant is DROPPED past
+# one stderr line and does not come back on its own. Losing `file` that way makes System ->
+# Backup / Flash Firmware's "Reset to defaults" row silently disappear, and the same race was
+# seen on the `luci` plugin, leaving getFeatures/getTimezones answering -32000 until a reboot.
+# This package registers no rpcd object of its own - only acl.d/*.json - so the reload ran that
+# risk for every OTHER plugin and refreshed nothing of ours.
 #
-# 1. RELOAD IS A RE-EXEC, NOT A REFRESH. `/etc/init.d/rpcd reload` is `procd_send_signal rpcd` ->
-#    SIGHUP -> rpcd freezes sessions and calls exec_self(): a full re-exec that re-scans the plugin
-#    dirs and dlopen()s every .so. A plugin file that is absent or mid-write at that instant is
-#    DROPPED, past one stderr line, and does NOT come back on its own:
-#
-#      mv /usr/lib/rpcd/file.so /tmp/ ; rpcd reload   -> `ubus list` loses `file`
-#      file restored, no reload                       -> still missing
-#      reload (or restart)                             -> back
-#
-#    First measured this way on the `luci` ucode plugin (24.10/25.12: /usr/libexec/rpcd/luci) — a
-#    reload racing another package's file replacement left every luci/getFeatures,
-#    luci/getTimezones and luci/getMountPoints call answering `-32000 Object not found`, reported
-#    from the field on a SNAPSHOT router, cleared only by a reboot. The SAME race hits `file`, and
-#    losing `file` is worse: it is what makes System -> Backup/Flash Firmware's "Reset to
-#    defaults" row silently disappear. view/system/flash.js computes has_rootfs_data from
-#    /proc/mtd and /proc/mounts read through fs.trimmed(), wrapped in L.resolveDefault(..., '') —
-#    so the -32000 `file` answers with reads as "no overlay" instead of as an error (OpenWrt forum
-#    thread 251930, posts 97 and 109 — "fixed" by installing an unrelated package whose OWN
-#    postinst reload happened to re-register `file`).
-#
-# 2. THIS PACKAGE HAS NOTHING FOR A RELOAD TO REFRESH. It registers no rpcd object at all — no
-#    /usr/libexec/rpcd/* script, no /usr/lib/rpcd/*.so, only root/usr/share/rpcd/acl.d/*.json — so
-#    a reload here only runs the dlopen risk above for every OTHER plugin, for zero gain of its own.
-#
-# 3. rpcd READS acl.d/*.json AT LOGIN, not only at start or on SIGHUP. Measured: narrow
-#    rpcd.@login[0].read/write from '*' to an explicit group list, remove the ACL file with rpcd
-#    already running -> `session access` for uci footstrap write answers false; put the file back
-#    with NO reload, log in again -> true. A fresh login already sees a just-installed ACL.
-#
-# 4. On a stock router, root's rpcd login grants read='*' write='*' — blanket. `session access`
-#    answers true even for an INVENTED object (measured against `uci totally_made_up_pkg_xyz`), so
-#    the theme's ACL group is irrelevant to a root session from the start.
-#
-# The one case #3 does not cover: a session that logged in BEFORE this install, on a RESTRICTED
-# (non-'*') rpcd login — its in-memory ACL set predates the new grant, and only a fresh login
-# re-reads acl.d. Measured: that session stays false until it is forced to re-authenticate.
-# Ending the session (not reloading rpcd) fixes exactly that, with a narrower blast radius: the
-# postinst below ends a session only when a fresh login would actually gain the scope — denied
-# uci/footstrap/write right now, AND its own username's rpcd login lists this theme's ACL group —
-# so a restricted login the theme's grant was never added to (a second admin account, a monitoring
-# script's login) is left running: re-authenticating would deny it exactly the same as before.
-# Verified verbatim on the stand: a blanket-root session survived, a stale restricted session was
-# destroyed, the unauthenticated all-zero session was left untouched, and a fresh login answered
-# true. The one visible price: an admin on a restricted account WHOSE LOGIN ALREADY LISTS the
-# theme's group is logged out by their own install action and must log back in — the narrowest set
-# this can be, since a session whose login never grants the group gains nothing from re-logging in.
+# rpcd reads acl.d/*.json AT LOGIN, not only on SIGHUP, so a fresh login already sees a
+# just-installed ACL. The one case that leaves is a session opened BEFORE this install on a
+# restricted (non-'*') rpcd login: its in-memory ACL set predates the grant. That session is
+# ended below, and only when re-authenticating would actually hand it the theme's scope.
 define Package/luci-theme-footstrap/postinst
 #!/bin/sh
 [ -n "$${IPKG_INSTROOT}" ] || {
 	# uci-defaults registers the theme; drop the LuCI caches so it is seen without a reboot.
-	# default_postinst also runs it; the second pass is idempotent.
+	# Calling it ourselves is belt-and-braces: OpenWrt's default_postinst also runs (then
+	# deletes) every /etc/uci-defaults/* we ship, so it executes twice per install. It is
+	# idempotent: the second pass finds the theme already registered and changes nothing.
 	[ -f /etc/uci-defaults/30_luci-theme-footstrap ] && \
 		sh /etc/uci-defaults/30_luci-theme-footstrap >/dev/null 2>&1 || true
 	rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* >/dev/null 2>&1 || true
-	# See the note above the define: no reload. Instead, end a session only when re-authenticating
-	# would actually hand it the theme's scope: it must be denied uci/footstrap/write right now
-	# AND its own rpcd login entry must list this theme's ACL group, in either read or write — a
-	# denied session whose login never grants that group (a second admin account or a monitoring
-	# script's login the theme was never added to) stays denied after a fresh login too, so ending
-	# it would only cost someone a session for nothing. Guarded on the `session` object existing at
-	# all (a router with rpcd built without ubus session support just does nothing here).
+	# A blanket ('*') login is granted everything and answers true below, so it is never touched;
+	# neither is the unauthenticated all-zero session, nor a denied session whose own login does
+	# not list this theme's group - re-authenticating would deny it just the same. Guarded on
+	# rpcd having ubus session support at all.
 	ubus list 2>/dev/null | grep -qx session && {
 		for s in $$(ubus call session list 2>/dev/null | grep -o '"ubus_rpc_session": "[a-f0-9]*"' | cut -d'"' -f4); do
-			# skips the all-zero session: that one is the UNAUTHENTICATED default and is never
-			# denied anything by an ACL, so it is not this sweep's business.
 			case "$$s" in *[!0]*) ;; *) continue ;; esac
 			ubus call session access "{\"ubus_rpc_session\":\"$$s\",\"scope\":\"uci\",\"object\":\"footstrap\",\"function\":\"write\"}" 2>/dev/null | grep -q true && continue
-			# A '*' login already answered true above and never reaches here. What is left is a
-			# denied session: only destroy it if ITS OWN username's rpcd login actually lists the
-			# theme's ACL group, so a fresh login would gain the scope this sweep is chasing.
 			u=$$(ubus call session get "{\"ubus_rpc_session\":\"$$s\"}" 2>/dev/null | grep -o '"username": *"[^"]*"' | cut -d'"' -f4)
 			[ -n "$$u" ] || continue
-			# Enumerate every login section from `uci show`, not a `uci -q get` index count: the
-			# latter stops at the FIRST section with no username option (a login block missing that
-			# option is legal UCI) and never looks at any section past it — fails safe (a session is
-			# kept, never wrongly destroyed) but skips real sections. `while read` takes one line at
-			# a time instead of `for`'s word-splitting, because a username may itself contain a
-			# space, which would otherwise split one section's line into two and skip it. The loop
-			# runs in a subshell, so its answer cannot survive in a variable — it is echoed out and
-			# captured instead. A show line's value comes single-quoted
-			# (`rpcd.@login[0].username='root'`), stripped below.
+			# Every login section from `uci show`, not a `uci -q get` index walk: the latter stops
+			# at the first section with no username option. `while read` keeps a username with a
+			# space in one piece; the loop is a subshell, so its answer is echoed out, not stored.
 			grant=$$(uci show rpcd 2>/dev/null | grep -F .username= | while read -r kv; do
 				sect=$${kv%%.username=*}
 				un=$${kv#*.username=}
@@ -164,14 +110,15 @@ define Package/luci-theme-footstrap/postrm
 #!/bin/sh
 # opkg runs the OLD package's postrm with arg "upgrade" during a version upgrade and "remove" on a
 # real removal. On upgrade this script MUST change nothing: reverting mediaurlbase and wiping the
-# theme registration here is what flipped every updating 24.10 user back to bootstrap. apk never
-# runs this on upgrade (it uses the new package's pre/post-upgrade), so guarding on the arg is
-# correct for both managers.
+# theme registration here is what flipped every updating user back to bootstrap. apk never runs
+# this on upgrade (it uses the new package's pre/post-upgrade), so guarding on the arg is correct
+# for both managers.
 case "$$1" in *upgrade*) exit 0 ;; esac
 [ -n "$${IPKG_INSTROOT}" ] || {
 	uci -q delete luci.themes.Footstrap
-	# Don't leave the active theme pointing at the media dir we just removed: a theme needs its
-	# media dir AND its ucode template to render, so both are checked before handing the UI over.
+	# Don't leave the active theme pointing at the media dir we just removed. A theme needs its
+	# media dir AND its ucode template to render, so a one-part check could hand the UI to a
+	# half-removed bootstrap — the blank page this branch exists to avoid.
 	case "$$(uci -q get luci.main.mediaurlbase)" in
 		/luci-static/footstrap*)
 			[ -d /www/luci-static/bootstrap ] && \
@@ -179,79 +126,45 @@ case "$$1" in *upgrade*) exit 0 ;; esac
 				uci set luci.main.mediaurlbase=/luci-static/bootstrap ;;
 	esac
 	uci commit luci
-	# The admin-uploaded login background (kept out of the package on purpose, so it survives an
-	# upgrade). A real removal is the one time it should go — this branch never runs on upgrade.
+	# The admin-uploaded login background and pattern, kept out of the package on purpose so they
+	# survive an upgrade. A real removal is the one time they should go.
 	rm -rf /etc/footstrap >/dev/null 2>&1 || true
 	rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* >/dev/null 2>&1 || true
-	# No rpcd reload here either — see the postinst note. A removal drops nothing rpcd needs to
-	# be told about (no plugin object, ever); a session still holding the now-gone ACL group is
-	# simply denied uci/footstrap/write on its next call, which is the correct answer anyway.
 }
 endef
 
-# Runs right after luci.mk copies luasrc/ucode/htdocs/root/src into PKG_BUILD_DIR, so it rewrites
-# the build copy and never the source tree.
+# THE STYLESHEET IS COMMITTED, NOT GENERATED HERE, and that is a deliberate difference from the
+# theme's own repository. There, styles/ is sixteen files in four cascade layers whose ORDER is the
+# design, and cascade.css is a build artefact that is not even tracked; a shell script concatenates
+# and minifies it at package time. In this tree the other four themes each commit one cascade.css
+# and have no build step, and a theme that arrives with its own build system asks a reviewer to
+# audit that before they can read a stylesheet. So the sheet is generated upstream of this tree and
+# committed here — unmangled and readable, which the release artefact is not.
 #
-# 1. cascade.css is generated, not committed — and styles/ is not in luci.mk's copy list, so
-#    build-css.sh reads it from $(CURDIR) and writes into the build tree. cat and awk only, which
-#    is why it runs on an OpenWrt buildbot.
-# 2. Stamp the git-derived version into fs-version.js. Unconditional: with PKG_VERSION empty it
-#    falls back to PKG_SRC_VERSION, which luci.mk always defines, so only an un-built source
-#    checkout keeps the literal '0.0.0-dev'.
-#
-# The catalogue is luci.mk's business and nothing here compiles it: `po/` is the directory
-# LUCI_LANGUAGES globs, so luci.mk emits one `luci-i18n-footstrap-` package per language.
+# What is left for this hook is the two things luci.mk cannot do by itself.
 define Build/Prepare/luci-theme-footstrap
 	# luci.mk's Build/Prepare copies only src/ luasrc/ htdocs/ root/ ucode/ into
-	# $(PKG_BUILD_DIR), and PKG_LICENSE_FILES resolves against THAT — so the Apache text has to
-	# be put there by hand or the declaration points at nothing.
+	# $(PKG_BUILD_DIR), and PKG_LICENSE_FILES resolves against THAT — so the Apache text has to be
+	# put there by hand or the declaration points at nothing.
 	$(CP) $(CURDIR)/LICENSE $(PKG_BUILD_DIR)/
-	$(SHELL) $(CURDIR)/build-css.sh $(PKG_BUILD_DIR)/htdocs/luci-static/footstrap/cascade.css
-	# The private `--fs-*` names are 16% of the sheet and mean nothing to a browser — the same trade
-	# terser makes for the JS. BEFORE strip-templates.sh on purpose: the reserved set is derived by
-	# reading the JS and the templates, so it must see them whole. Over-reserving costs bytes,
-	# under-reserving breaks the theme silently.
-	# The reserved set is read from $(CURDIR), the SOURCE, never from $(PKG_BUILD_DIR): in CI the
-	# build tree's JS has already been through terser, so its comments are gone and five names that
-	# are only MENTIONED in a comment stop being reserved. That made the shipped sheet depend on WHO
-	# built it — 10 reserved names via CI, 15 via a plain SDK build, same source. Reading the source
-	# over-reserves by ~1 KB and is the safe direction.
-	$(SHELL) $(CURDIR)/mangle-tokens.sh \
-		$(PKG_BUILD_DIR)/htdocs/luci-static/footstrap/cascade.css \
-		$(CURDIR)/htdocs/luci-static/resources $(CURDIR)/ucode
-	# Templates are mostly comments and shipped them all. jsmin/terser strip the JS, build-css.sh the
-	# CSS; the .ut files were simply never included in that trade. Only `{# … #}` goes — see the
-	# script for why the ucode-code `/* … */` deliberately stays. -22 KB of 60, git keeps every word,
-	# and dev-sync.sh does NOT run this: a router you are debugging on keeps its comments.
-	$(SHELL) $(CURDIR)/strip-templates.sh $(PKG_BUILD_DIR)/ucode
-	# The gate-only exports (`/* fs:probe */`). A handful of module-private functions are also
-	# listed in a module's baseclass so a gate in the source repository can call them — nothing on a
-	# router does. The functions stay; the export line goes, so the surface a package carries is the
-	# one the theme itself uses.
-	$(SHELL) $(CURDIR)/strip-probes.sh $(PKG_BUILD_DIR)/htdocs/luci-static/resources
-	# …and the same for the shell under root/ (71% and 95% comment lines). Whole-line `#` only.
-	$(SHELL) $(CURDIR)/strip-shell.sh $(PKG_BUILD_DIR)/root
-	# The two static assets luci.mk has no step for: the SVG favicon carries a 753-byte comment and
-	# the manifest its indentation, and both are fetched by every browser over a link uhttpd does
-	# not compress. awk only, so this runs on a buildbot with no node.
-	$(SHELL) $(CURDIR)/strip-assets.sh $(PKG_BUILD_DIR)/htdocs/luci-static/footstrap
+	# The version the Appearance tab prints. luci.mk derives PKG_SRC_VERSION from the tree's git
+	# history; without this the string stays at the source literal '0.0.0-dev' and every install
+	# reports itself as a development build.
 	$(SED) "s#const FS_VERSION *= *'[^']*'#const FS_VERSION = '$(if $(PKG_VERSION),$(PKG_VERSION),$(PKG_SRC_VERSION))'#" \
 		$(PKG_BUILD_DIR)/htdocs/luci-static/resources/fs-version.js
 endef
 
-# Absolute, not a relative path: CI rsyncs this package into package/, not into the feed.
 include $(TOPDIR)/feeds/luci/luci.mk
 
-# The line below is load-bearing and is NOT a signature — do not delete it as boilerplate.
+# THE LINE BELOW IS LOAD-BEARING AND IT IS NOT A SIGNATURE — do not delete it as boilerplate.
 #
 # include/scan.mk builds the package list by GREPPING the Makefiles, not by parsing them:
 #   find -L package -name Makefile | xargs grep -aHE 'call (Build/DefaultTargets|BuildPackage|KernelPackage)'
 # A Makefile that does not match is not in the list, so it is never dumped, no CONFIG_PACKAGE_*
-# symbol is emitted for it, and `make package//compile` answers "No rule to make target" with
-# no error naming the package anywhere in the build.
+# symbol is emitted for it, and `make package//compile` answers "No rule to make target" —
+# with no error naming the package anywhere in the build.
 #
-# This theme never calls BuildPackage itself; luci.mk does, at the include above, and the grep
-# cannot see that. So the only thing that puts this package into the SDK's list is the literal text
-# on the next line. tools/scan-marker.sh re-derives the grep from scan.mk and fails the build if it
-# goes missing.
+# This theme never calls BuildPackage itself; luci.mk does, at the include above. The grep cannot
+# see that. So the only thing that puts this package into the build's list is the literal text on
+# the next line — which is why every luci-* Makefile carries it.
 # call BuildPackage - OpenWrt buildroot signature
diff --git a/luci-theme-footstrap/README.md b/luci-theme-footstrap/README.md
deleted file mode 100644
index 4090763fa..000000000
--- a/luci-theme-footstrap/README.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# luci-theme-footstrap (the package)
-
-A LuCI theme for OpenWrt **24.10 and newer** (ucode templates). Installing and using it:
-[the repository README](../README.md). Developer documentation: [`../docs/`](../docs/README.md).
-
-Internal name: `footstrap`. Media path: `/luci-static/footstrap`.
-
-## One theme, one entry
-
-`luci.themes` carries **exactly one** entry, `Footstrap` → `/luci-static/footstrap`. Layout (top bar
-by default, or the side menu), mode, palette, density, wallpaper, rounding and every colour — accent,
-the three status colours, the four surfaces — are **client** axes on the Footstrap tab of
-System → System: `localStorage` plus attributes and inline properties on `:root`, and nothing is
-written to the router until "Save as default" is pressed.
-There are no per-layout or per-mode themes; every legacy name (`FootstrapSidebar`, `FootstrapOnTop`,
-`…-dark`/`…-light`) is deleted by `root/etc/uci-defaults/30_luci-theme-footstrap`.
-
-There is one menu renderer too, `menu-footstrap.js`. The top bar is its own markup, morphed by CSS
-through `:root[data-layout]`. No second template, no second renderer, no `footstrap-top` symlink.
-
-**The theme's boundary:** it supplies the chrome and the design language; page content is drawn by
-the view JS of `luci-mod-*`. The one exception is `fs-overview.js`, which draws no content of its own
-and only re-arranges the stock overview sections.
-
-## Layout
-
-```
-Makefile                          luci.mk; LUCI_MINIFY_CSS:=0; Build/Prepare (CSS, mangle, strip,
-                                  version, po2lmo)
-styles/                           CSS SOURCE: layers tokens / base / theme / pages
-build-css.sh                      styles/ → htdocs/luci-static/footstrap/cascade.css
-mangle-tokens.sh                  shorten the private --fs-* names in a built sheet
-strip-templates.sh strip-shell.sh drop comments from .ut and from root/**.sh
-po/                               translation catalogue (Weblate translates this)
-ucode/template/themes/footstrap/  header.ut, footer.ut, sysauth.ut, partials/
-htdocs/luci-static/footstrap/     cascade.css (generated), logo.svg — no webfonts, and no
-                                  wallpaper: the Pattern is one the admin uploads, and a font
-                                  is one the admin installs (repo: fonts/set-font.sh)
-htdocs/luci-static/resources/     menu-footstrap.js (renderer), menu-footstrap-common.js,
-                                  fs-{fit,menutree,prefs,widgets,chrome,router,sheets,search,
-                                  select,appearance,version,overview}.js
-root/etc/uci-defaults/            registration and legacy-name migration
-root/usr/share/rpcd/acl.d/        ACL: uci footstrap (Save as default) + login-background upload
-```
-
-There is no update checker: the theme is installed from the package feed (`install.sh` adds it), so
-`apk upgrade` / `opkg upgrade` carries it forward like everything else on the router. The Footstrap
-tab shows the version it is running and makes no network call to do it.
-
-**Do not edit `cascade.css`** — it is generated by `build-css.sh` from `styles/` and is gitignored.
-Colours go in `styles/03-palettes.css`, scales and tokens in `styles/02-tokens.css`.
-
-## Working on a router
-
-The normal dev stand is the disposable routers `../owlab.yaml` defines, driven by `owlab` — see
-[`../docs/development.md`](../docs/development.md). `dev-sync.sh` targets a **hardware** router over
-ssh:
-
-```sh
-./dev-sync.sh          # push to `ssh router` (registers the theme but does NOT activate it)
-ssh router 'uci set luci.main.mediaurlbase=/luci-static/footstrap; uci commit luci; rm -f /tmp/luci-indexcache*'
-ssh router 'uci set luci.main.mediaurlbase=/luci-static/bootstrap; uci commit luci'   # roll back
-```
-
-Before pushing, run `npm run check` from the repository root. The rules and the traps are in
-[`../docs/conventions.md`](../docs/conventions.md); packaging is in
-[`../docs/package.md`](../docs/package.md).
diff --git a/luci-theme-footstrap/build-apk.sh b/luci-theme-footstrap/build-apk.sh
deleted file mode 100755
index 2035de056..000000000
--- a/luci-theme-footstrap/build-apk.sh
+++ /dev/null
@@ -1,120 +0,0 @@
-#!/bin/sh
-# Build luci-theme-footstrap as an OpenWrt .apk via the SDK. The theme is noarch, so the package
-# installs on any router of that release whatever its CPU architecture.
-#
-#   ./build-apk.sh            # download SDK if needed, then build
-#   BUILD_DIR=~/x ./build-apk.sh
-#
-# This is NOT how the release is built — `./tools/stage.sh && owfeed build` is, and it produces both
-# formats in seconds without a toolchain. What this exercises is the OTHER path: the Makefile,
-# luci.mk, jsmin and the SDK's own packaging, which has to keep working for anyone building the
-# theme in an SDK or a feed without owfeed.
-set -e
-
-# The point release and the compiler are both baked into the SDK's filename, so this default goes
-# stale twice over: downloads.openwrt.org keeps only the CURRENT point release of a branch, and a
-# toolchain bump changes the gcc string. Kept in step with owfeed.lock's `point:` for the 25.12
-# branch, which is what the release builds against. Override with OPENWRT_RELEASE=… and
-# OPENWRT_SDK_FILE=… for any other.
-REL="${OPENWRT_RELEASE:-25.12.5}"
-SDK_BASE="https://downloads.openwrt.org/releases/${REL}/targets/mediatek/filogic"
-SDK_FILE="${OPENWRT_SDK_FILE:-openwrt-sdk-${REL}-mediatek-filogic_gcc-14.3.0_musl.Linux-x86_64.tar.zst}"
-SDK_URL="$SDK_BASE/$SDK_FILE"
-# MUST be a case-sensitive fs (ext4/…), NOT an NTFS/9p Windows mount.
-BUILD_DIR="${BUILD_DIR:-/tmp/ow-footstrap-build}"
-# FORCE=1 overrides buildroot's host-prereq bail-outs (see step 4).
-export FORCE=1
-THEME_DIR="$(cd "$(dirname "$0")" && pwd)"          # this package
-REPO="$(cd "$THEME_DIR/.." && pwd)"                 # repo root (holds tools/, .github/)
-SDK_DIR="$BUILD_DIR/sdk"
-# The SDK channel (major.minor) the release SDK is signed under — derived from REL, never a
-# second copy: 25.12.2 -> 25.12.
-CHANNEL="${REL%.*}"
-
-mkdir -p "$BUILD_DIR"
-cd "$BUILD_DIR"
-
-# 1. SDK
-if [ ! -d "$SDK_DIR" ]; then
-	echo ">> downloading SDK $REL ..."
-	# --https-only: GNU wget follows https -> http redirects, and this tarball is a toolchain
-	# that will build a package a maintainer may hand to someone.
-	wget -q --https-only -O sdk.tar.zst "$SDK_URL"
-
-	# Verify the SDK: signature first, checksum under it. This is the least verified input in this
-	# repo and the only one that ends up INSIDE the built package (jsmin.c and i18n-scan.pl are
-	# LINTERS pinned by commit and sha256; the toolchain that compiles the artifact arrives on TLS).
-	# `sha256sums` alone is NOT a verification — it is served by the same host from the same
-	# directory, unsigned, so whoever can replace the tarball replaces the checksum beside it (see
-	# docs/conventions.md on GitHub's asset digest). What makes it one is the ed25519 signature over that file,
-	# checked with a key pinned from a DIFFERENT host (github.com/openwrt/keyring). Fails CLOSED.
-	#
-	# Every pin comes from luci-upstream.pin — the keyring commit, usign, and the branch's own
-	# signing key. The keys used to live in .github/workflows/build.yml's matrix and were read out
-	# of it here; that matrix went with the SDK build when the release moved to owfeed, so they now
-	# have one home and this script has one source.
-	. "$THEME_DIR/luci-upstream.pin"
-	[ -n "${OPENWRT_KEYRING_PIN:-}" ] || { echo "OPENWRT_KEYRING_PIN missing from luci-upstream.pin" >&2; exit 1; }
-
-	# sdk_key / sdk_key_sha256 for CHANNEL: 25.12 -> OPENWRT_SDK_KEY_2512. Indirection through
-	# eval because the pin file is plain shell assignments and /bin/sh has no associative arrays.
-	CH_VAR="$(printf '%s' "$CHANNEL" | tr -d .)"
-	eval "SDK_KEY=\${OPENWRT_SDK_KEY_$CH_VAR:-}"
-	eval "SDK_KEY_SHA256=\${OPENWRT_SDK_KEY_${CH_VAR}_SHA256:-}"
-	[ -n "$SDK_KEY" ] && [ -n "$SDK_KEY_SHA256" ] \
-		|| { echo "no SDK signing key for channel $CHANNEL in luci-upstream.pin" >&2; exit 1; }
-
-	echo ">> verifying SDK signature (key $SDK_KEY, channel $CHANNEL) ..."
-	rm -rf "$BUILD_DIR/usign"
-	U="$("$REPO/tools/build-usign.sh" "$BUILD_DIR/usign")"
-	wget -q --https-only -O sha256sums     "$SDK_BASE/sha256sums"
-	wget -q --https-only -O sha256sums.sig "$SDK_BASE/sha256sums.sig"
-	wget -q --https-only -O openwrt.pub \
-		"https://raw.githubusercontent.com/openwrt/keyring/$OPENWRT_KEYRING_PIN/usign/$SDK_KEY"
-	echo "$SDK_KEY_SHA256  openwrt.pub" | sha256sum -c - >/dev/null \
-		|| { echo "OpenWrt's $CHANNEL key does not match its pin — refusing to verify with it" >&2; exit 1; }
-	"$U" -V -m sha256sums -p openwrt.pub -x sha256sums.sig \
-		|| { echo "BAD SIGNATURE on OpenWrt's sha256sums — refusing to build with this SDK." >&2; exit 1; }
-	echo ">> signature verified; now the checksum means something."
-
-	WANT="$(grep -F " *$SDK_FILE" sha256sums | cut -d' ' -f1)"
-	[ -n "$WANT" ] || { echo "no sha256 published for $SDK_FILE" >&2; exit 1; }
-	GOT="$(sha256sum sdk.tar.zst | cut -d' ' -f1)"
-	[ "$WANT" = "$GOT" ] || { echo "SDK checksum mismatch: want $WANT, got $GOT" >&2; exit 1; }
-	rm -f sha256sums sha256sums.sig openwrt.pub
-	echo ">> SDK verified."
-
-	echo ">> extracting ..."
-	mkdir -p "$SDK_DIR"
-	tar --zstd -xf sdk.tar.zst -C "$SDK_DIR" --strip-components=1
-	rm -f sdk.tar.zst
-fi
-cd "$SDK_DIR"
-
-# 2. feeds (need luci for luci.mk + BuildPackage macros)
-if [ ! -f feeds/luci.index ] && [ ! -d feeds/luci ]; then
-	./scripts/feeds update base luci
-fi
-./scripts/feeds install -a -p luci >/dev/null 2>&1 || true
-
-# 3. drop our theme into the luci themes feed (fresh copy)
-DEST="feeds/luci/themes/luci-theme-footstrap"
-rm -rf "$DEST"
-cp -a "$THEME_DIR" "$DEST"
-rm -rf "$DEST/build-apk.sh" "$DEST/dev-sync.sh" "$DEST/.git" 2>/dev/null || true
-
-./scripts/feeds update -i luci
-./scripts/feeds install luci-theme-footstrap
-
-# 4. build. ncurses is only needed for interactive menuconfig, not for a noarch
-# theme, so satisfy the host prereq stamp to skip that check.
-mkdir -p staging_dir/host
-touch staging_dir/host/.prereq-build
-make defconfig FORCE=1
-make package/luci-theme-footstrap/clean FORCE=1 V=s >/dev/null 2>&1 || true
-make package/luci-theme-footstrap/compile FORCE=1 V=s
-
-# 5. locate the artifact
-echo
-echo ">> built packages:"
-find bin -name 'luci-theme-footstrap*' \( -name '*.apk' -o -name '*.ipk' \) -print
diff --git a/luci-theme-footstrap/build-css.sh b/luci-theme-footstrap/build-css.sh
deleted file mode 100755
index dc3f7356e..000000000
--- a/luci-theme-footstrap/build-css.sh
+++ /dev/null
@@ -1,274 +0,0 @@
-#!/bin/sh
-# Concatenate the styles/ tree into a single cascade.css.
-#
-#   ./build-css.sh [outfile] [--dev]
-#
-# Directory order is layer order (styles/, base/, theme/, pages/); filename order is source order
-# within a directory. A later layer beats an earlier one whatever the specificity, so nothing needs
-# !important to override base.
-#
-# Minified unless --dev: ~468 KB of source to ~136 KB. uhttpd serves CSS with NO gzip, so every byte
-# is a wire byte. Comments and whitespace go; a selector or a declaration is never rewritten, which
-# is why LuCI's csstidy stays off (it mangles :has() and color-mix()). Needs only cat and awk, so an
-# OpenWrt buildbot can run it.
-set -e
-
-D="$(cd "$(dirname "$0")" && pwd)"
-OUT=""
-DEV=0
-for a in "$@"; do
-	case "$a" in
-		--dev) DEV=1 ;;
-		# An unknown option used to fall through to `OUT="$a"`: a typo like `--devv`
-		# wrote the stylesheet to a file named "--devv".
-		-*) echo "build-css: unknown option: $a" >&2; exit 1 ;;
-		*) OUT="$a" ;;
-	esac
-done
-[ -n "$OUT" ] || OUT="$D/htdocs/luci-static/footstrap/cascade.css"
-
-for d in styles styles/base styles/theme styles/pages; do
-	[ -d "$D/$d" ] || { echo "build-css: $D/$d missing" >&2; exit 1; }
-done
-
-TMP="$OUT.tmp.$$"
-# $TMP.min too: an awk failure used to leave it behind next to the real output.
-trap 'rm -f "$TMP" "$TMP.min" "$TMP.layer"' EXIT
-mkdir -p "$(dirname "$OUT")"
-
-# One `@layer X{` per LAYER, not one per FILE. Every source file carries its own wrapper so it can
-# be read and edited alone; concatenated, that is 37 copies of the same six-to-eleven bytes, 542 of
-# them.
-#
-# The wrapper now comes from the DIRECTORY, so a file filed under the wrong layer would be silently
-# re-layered instead of just being wrong — hence the check that each file opens with the layer its
-# directory means. A file with no wrapper at all (00-header.css: the banner and the layer-order
-# statement) is copied through, and must come before the wrapped ones or it would land inside the
-# block.
-emit_layer() {
-	layer="$1"; shift
-	body="$TMP.layer"
-	: > "$body"
-	for f in "$@"; do
-		if head -1 "$f" | grep -q '^@layer '; then
-			head -1 "$f" | grep -q "^@layer $layer {\$" || {
-				echo "build-css: $f is in a $layer directory but does not open with '@layer $layer {'" >&2
-				exit 1; }
-			# the file's own wrapper: its first line, and its last line, which is that wrapper's `}`
-			sed '1d;$d' "$f" >> "$body"
-		elif [ -s "$body" ]; then
-			echo "build-css: $f has no @layer wrapper but follows one that does — it would be" >&2
-			echo "build-css: swallowed into the $layer block instead of staying above it." >&2
-			exit 1
-		else
-			cat "$f"
-		fi
-	done
-	if [ -s "$body" ]; then
-		printf '@layer %s {\n' "$layer"
-		cat "$body"
-		printf '}\n'
-	fi
-	# not covered by the EXIT trap: the success path runs `trap - EXIT` first
-	rm -f "$body"
-}
-
-# glob expands in filename order
-{
-	emit_layer tokens "$D"/styles/*.css
-	emit_layer base   "$D"/styles/base/*.css
-	emit_layer theme  "$D"/styles/theme/*.css
-	emit_layer page   "$D"/styles/pages/*.css
-} > "$TMP"
-
-# Comments, and — unless told not to — the whitespace CSS ignores anyway, in one string-aware
-# pass rather than two chained ones. squeeze=0 (the pre-compression rule count below, and every
-# `--dev` build) strips comments and trims each line; squeeze=1 also does the ~9.5 KB whitespace
-# pass. Why a scan has to be string-aware, what the whitespace pass leaves alone and why, and the
-# sed bug this replaced: docs/css.md, "The build".
-squeeze() {
-	awk -v SQUEEZE="$2" '
-		BEGIN { q = ""; inc = 0; banner = 0; lastc = ""; buf = ""; lastreal = "" }
-		{
-			line = $0
-			# a banner already open on an earlier line: nothing on a pure continuation line can
-			# change q or inc, so the whole line is banner content until its own closing "*/".
-			if (banner) {
-				trimmed = line
-				sub(/^[ \t]+/, "", trimmed); sub(/[ \t]+$/, "", trimmed)
-				print trimmed
-				lastc = ""; lastreal = ""
-				if (index(line, "*/")) banner = 0
-				next
-			}
-			# The line BREAK a squeeze pass is about to swallow is whitespace, and a declaration
-			# may be wrapped across it. Feed it to the whitespace-run logic below as a leading
-			# space: it survives only where a space would and is dropped next to { } ; , : —
-			# lastc == "" means output is already at the start of a line, with nothing to glue to.
-			if (SQUEEZE && lastc != "" && q == "" && !inc) line = " " line
-			out = ""; i = 1; n = length(line); opened = 0
-			while (i <= n) {
-				c = substr(line, i, 1)
-				if (inc) {                                  # inside a stripped /* ... */
-					if (c == "*" && substr(line, i + 1, 1) == "/") { inc = 0; i += 2; continue }
-					i++; continue
-				}
-				if (q != "") {                              # inside a "..." or '"'"'...'"'"' string
-					out = out c
-					if (c == "\\") { out = out substr(line, i + 1, 1); i += 2; continue }
-					if (c == q) q = ""
-					lastreal = ""
-					i++; continue
-				}
-				if (c == "\"" || c == "'"'"'") { q = c; out = out c; lastreal = ""; i++; continue }
-				if (c == "/" && substr(line, i + 1, 1) == "*") {
-					if (substr(line, i + 2, 1) == "!") {
-						# a genuine banner open: reached only with q=="" and inc==0 AT THIS
-						# CHARACTER, the same guard strip_comments used before this pass absorbed
-						# it — so a "/*!" that is really bytes inside a string or an already-open
-						# ordinary comment never lands here. Copied raw from the marker on, byte
-						# for byte: it is an Apache-2.0 attribution, not formatting.
-						banner = 1
-						rest = substr(line, i)
-						out = out rest
-						if (index(rest, "*/")) banner = 0
-						opened = 1
-						break
-					}
-					inc = 1; i += 2; continue
-				}
-				if (SQUEEZE) {
-					if (c == " " || c == "\t") {     # collapse a run of whitespace to one space
-						while (i <= n && (substr(line, i, 1) == " " || substr(line, i, 1) == "\t")) i++
-						# the last char EMITTED, which on a continuation line lives on the
-						# previous output line — hence lastc, not just `out`.
-						prev = (length(out) ? substr(out, length(out), 1) : lastc)
-						nxt  = (i <= n ? substr(line, i, 1) : "")
-						# drop it entirely next to a delimiter; otherwise it may be a combinator
-						#
-						# `>` is a delimiter too, and the only one that is itself a combinator: a
-						# space either side of it is decoration. Safe because a `>` outside a
-						# string can only be the child combinator — the sheet has no media range
-						# syntax (`@media (width > 600px)`) — and a `>` inside a string never
-						# reaches here, the scanner having copied it verbatim above. `~` and `+`
-						# are deliberately NOT joined: `[attr~=v]` and `calc(100% - 10px)` make
-						# them ambiguous without tracking bracket depth.
-						if (prev == "" || prev == "{" || prev == "}" || prev == ";" || prev == "," || prev == ":" || prev == ">")
-							continue
-						if (nxt == "{" || nxt == "}" || nxt == ";" || nxt == "," || nxt == "" || nxt == ">")
-							continue
-						out = out " "; lastreal = " "
-						continue
-					}
-					# THE LAST `;` OF A BLOCK IS REDUNDANT — dropped as the `}` is emitted, i.e.
-					# INSIDE the string-aware scanner. It used to be a `| sed "s/;}/}/g"` bolted
-					# onto the awk output, and sed cannot see strings: `content: ";}"` came out as
-					# `content: "}"`, and a data-URI containing `;}` was corrupted the same way
-					# (both reproduced). Nothing in the tree holds that byte pair today — which is
-					# how such a bug waits for whoever adds the first one.
-					#
-					# The `;` may already sit in the previous line output, so text is held in
-					# `buf` until the rule closes: a `;` already printed cannot be taken back.
-					if (c == "}") {
-						if (length(out) && substr(out, length(out), 1) == ";")
-							out = substr(out, 1, length(out) - 1)
-						else if (!length(out) && length(buf) && substr(buf, length(buf), 1) == ";")
-							buf = substr(buf, 1, length(buf) - 1)
-					}
-					out = out c; lastreal = c; i++
-				} else {
-					out = out c; i++
-				}
-			}
-			if (opened) {
-				trimmed = out
-				sub(/^[ \t]+/, "", trimmed); sub(/[ \t]+$/, "", trimmed)
-				print trimmed
-				lastc = ""; lastreal = ""
-				next
-			}
-			if (SQUEEZE) {
-				buf = buf out
-				if (length(out)) lastc = substr(out, length(out), 1)
-				# newline only after a closing brace — one rule per line. lastreal, not lastc: a
-				# line ending in a QUOTED `}` (content: "}") is not the end of a rule, and
-				# flushing there would split the rule and lose the space before its next token.
-				if (lastreal == "}") { print buf; buf = ""; lastc = ""; lastreal = "" }
-			} else {
-				sub(/^[ \t]+/, "", out)
-				sub(/[ \t]+$/, "", out)
-				if (length(out)) print out
-			}
-		}
-		END { if (SQUEEZE) { if (length(buf)) print buf; else printf "\n" } }
-	' "$1"
-}
-
-# Fail loudly rather than let an unbalanced block ship.
-#
-# String-aware, for the same reason the comment stripper is: a brace inside a CSS STRING is not a
-# block, so a counter that gsub()s over the raw line makes a perfectly valid rule fail the build.
-# Measured, all three shapes: `content: ";}"`, `content: "{"`, and a data-URI carrying `;}`. It
-# fails closed, so nothing is ever corrupted; what it costs is a hunt for an imbalance that is not
-# there.
-brace_count() {
-	awk '
-		BEGIN { q = "" }
-		{
-			line = $0; n = length(line); i = 1
-			while (i <= n) {
-				ch = substr(line, i, 1)
-				if (q != "") {				# inside a string
-					if (ch == "\\") { i += 2; continue }	# escape: skip the pair
-					if (ch == q) q = ""
-					i++; continue
-				}
-				if (ch == "\"" || ch == "'"'"'") { q = ch; i++; continue }
-				if (ch == "{") o++
-				else if (ch == "}") c++
-				i++
-			}
-		}
-		END {
-		if (o != c) { printf "build-css: %s: unbalanced braces (%d { vs %d })\n", FILENAME, o, c > "/dev/stderr"; exit 1 }
-		if (o < 100) { printf "build-css: %s: suspiciously few rules (%d)\n", FILENAME, o > "/dev/stderr"; exit 1 }
-		print o
-	}' "$1"
-}
-
-# Always brace-check a COMMENT-STRIPPED, UNSQUEEZED copy, --dev included: counting braces in the
-# raw file made a stray "{" in prose fail the build on perfectly valid CSS.
-squeeze "$TMP" 0 > "$TMP.min"
-RULES_BEFORE=$(brace_count "$TMP.min") || exit 1
-rm -f "$TMP.min"
-
-if [ "$DEV" -eq 0 ]; then
-	squeeze "$TMP" 1 > "$TMP.min"
-	mv "$TMP.min" "$TMP"
-
-	# AND AGAIN, on what actually ships: the check above only saw the squeeze's INPUT, yet
-	# the squeeze is the pass most able to corrupt the sheet — it tracks strings, joins lines
-	# and deletes the `;` before a `}`. An unchanged rule count is what says it did not.
-	#
-	# That equality — not brace balance on its own — is also why the 80 KB FS_CSS_FLOOR size
-	# guard once here is gone. Balance alone misses a cut right after any of the four
-	# `@layer NAME { … }` wrappers closes: everything up to that point is still a matched set, so
-	# a truncated write — a full disk, a compression that ate the tail — that stops exactly there
-	# reads as a valid, complete-looking file. Fed the real sheet, a cut right after the base
-	# layer's own close counts a balanced 283 rules against the file's true 1057. What catches
-	# that is the COUNT: RULES_BEFORE is measured from the untruncated concatenation before this
-	# pass runs, so a write that stops early can only ever come up short against it.
-	RULES_AFTER=$(brace_count "$TMP") || exit 1
-	if [ "$RULES_BEFORE" != "$RULES_AFTER" ]; then
-		echo "build-css: the squeeze changed the rule count ($RULES_BEFORE -> $RULES_AFTER)." >&2
-		exit 1
-	fi
-fi
-# --dev: $TMP is still the raw concatenation — comments and formatting intact, for reading on a
-# router rather than shipping.
-
-SIZE=$(wc -c < "$TMP" | tr -d ' ')
-mv "$TMP" "$OUT"
-trap - EXIT
-
-echo "build-css: $SIZE bytes -> $OUT"
diff --git a/luci-theme-footstrap/dev-sync.sh b/luci-theme-footstrap/dev-sync.sh
deleted file mode 100755
index e11956d8e..000000000
--- a/luci-theme-footstrap/dev-sync.sh
+++ /dev/null
@@ -1,146 +0,0 @@
-#!/bin/sh
-# Push the footstrap theme to a HARDWARE router over ssh (containers use owlab).
-# ONE theme, one luci.themes entry; layout (top/sidebar) is a client axis on the Footstrap tab of
-# System -> System, not a theme entry. This registers the theme; it does NOT activate it.
-set -e
-
-R="${1:-router2512}"
-N=footstrap
-D="$(cd "$(dirname "$0")" && pwd)"
-
-# cascade.css is generated from styles/ and is not in git. --dev keeps the comments so the
-# file on the router still reads like the source.
-"$D"/build-css.sh "$D/htdocs/luci-static/$N/cascade.css" --dev
-
-ssh "$R" "mkdir -p /usr/share/ucode/luci/template/themes/$N \
-	/www/luci-static/$N \
-"
-
-# the ONE template (+ partials/). Sidebar and top bar are the same markup, morphed by
-# :root[data-layout] — there is no second theme dir to copy.
-scp -q  "$D"/ucode/template/themes/$N/*.ut      "$R":/usr/share/ucode/luci/template/themes/$N/
-ssh "$R" "mkdir -p /usr/share/ucode/luci/template/themes/$N/partials"
-scp -q  "$D"/ucode/template/themes/$N/partials/*.ut "$R":/usr/share/ucode/luci/template/themes/$N/partials/
-
-# shared static: cascade.css and the two logo files, which is all this directory holds. No
-# wallpapers — the doodles this theme once downloaded from GitHub are gone, and nothing in the
-# package fetches anything at run time; the Background axis paints an SVG the admin uploads.
-scp -qr "$D"/htdocs/luci-static/$N/* "$R":/www/luci-static/$N/
-
-# EVERY resource JS, by GLOB — never by name. This used to list the four files
-# individually, so a FIFTH would be shipped by the package (luci.mk copies htdocs/
-# wholesale) yet silently never reach the dev router — first tested after a release.
-#
-# A TAR OF THE TREE, not `scp *.js`: the flat glob does not descend, so anything a subdirectory
-# holds shipped in the package (luci.mk copies htdocs/ wholesale) and reached the dev router never —
-# the same class of miss the glob above was written to end, one directory level down. Every path
-# under resources/ is ours to place.
-tar -C "$D/htdocs/luci-static/resources" -cf - . | ssh "$R" "mkdir -p /www/luci-static/resources && tar -C /www/luci-static/resources -xf -"
-
-# stamp the git-derived version into the deployed fs-version.js (the package does the same in
-# Build/Prepare) so the Footstrap tab shows a real version instead of "(dev)". The FILE NAME is part
-# of the contract: FS_VERSION lives in fs-version.js and moving it means changing every sed.
-FS_V="$(git -C "$D" describe --tags --always 2>/dev/null | sed 's/^v//')"
-# if-form, not `[ -n ] && ssh`: under `set -e` a bare `&&`-list whose LAST command fails is what
-# aborts, so `[ -n "$FS_V" ] && ssh …` would take the sync down whenever git describe yielded
-# nothing (a copied tree without .git). An `&&` guarding an ASSIGNMENT is a different shape and is
-# safe. expr refuses a tag with sed- or shell-special characters rather than interpolating it.
-if [ -n "$FS_V" ] && expr "$FS_V" : '[0-9A-Za-z._-]*$' >/dev/null; then
-	ssh "$R" "sed -i \"s#const FS_VERSION *= *'[^']*'#const FS_VERSION = '$FS_V'#\" /www/luci-static/resources/fs-version.js"
-fi
-
-# The catalogue. Nothing in Build/Prepare compiles it, and the release does not come from luci.mk
-# either — owfeed bundles the catalogues inside the theme package — so a dev router gets them from
-# nowhere unless this sync makes them. po2lmo is a luci-base host tool, absent from a dev machine
-# by default, so this is best-effort: without it the strings stay English and the sync still
-# succeeds. (Build it once from the pinned luci checkout: make -C modules/luci-base/src po2lmo —
-# plural_formula.c is GENERATED by contrib/lemon, so a bare cc over the .c files cannot work.)
-#
-# The basename is `footstrap-theme`, matching owfeed.yml and NOT the po file's own name:
-# lmo_load_catalog globs *..lmo so any basename loads, and a v0.8.4 router still owns
-# footstrap.ru.lmo through the old luci-i18n package, where the same path is a file conflict.
-if command -v po2lmo >/dev/null 2>&1; then
-	ssh "$R" "mkdir -p /usr/lib/lua/luci/i18n"
-	for po in "$D"/po/*/*.po; do
-		[ -e "$po" ] || continue
-		lang="$(basename "$(dirname "$po")")"
-		po2lmo "$po" "/tmp/footstrap-theme.$lang.lmo"
-		scp -q "/tmp/footstrap-theme.$lang.lmo" "$R":/usr/lib/lua/luci/i18n/
-		rm -f "/tmp/footstrap-theme.$lang.lmo"
-	done
-else
-	echo "  (po2lmo not found — skipping the translation catalogue; strings stay English)"
-fi
-
-# root/ -> / as a TREE, never as a list of names. luci.mk installs root/ wholesale, so a file named
-# here one-by-one is a file that ships in the package and silently never reaches the dev router —
-# first noticed after a release. The top-level dirs come from a GLOB for the same reason: this used
-# to tar `usr` literally, which is a hand-written list of one, and root/etc/ was the counterexample
-# already sitting in the tree — root/etc/config/footstrap shipped in the package and never reached
-# the router. tar, not `scp -r`, because scp's merge semantics on an existing /usr are ambiguous.
-#
-# Two subtrees have semantics a blind tar would break, so each is excluded and handled explicitly
-# below. Both exclusions are about BEHAVIOUR, not about naming files: a new file under either dir
-# still arrives for free.
-#   - etc/uci-defaults: deliberately run from /tmp (see below). Landing it in /etc/uci-defaults
-#     would make the router's next boot execute it and then DELETE it.
-#   - etc/config: conffiles. The package manager does not overwrite them on upgrade (Makefile's
-#     conffiles define) and neither may dev-sync — the live file holds the admin's "Save as
-#     default", and clobbering it here would be the very wipe that define exists to prevent.
-set --
-for _d in "$D"/root/*/; do set -- "$@" "$(basename "$_d")"; done
-tar -C "$D/root" --exclude=etc/uci-defaults --exclude=etc/config -cf - "$@" | ssh "$R" "tar -C / -xf -"
-
-# conffiles: install only when ABSENT, which is exactly what the package manager does with them.
-for _f in "$D"/root/etc/config/*; do
-	[ -f "$_f" ] || continue
-	_b=$(basename "$_f")
-	scp -q "$_f" "$R":"/tmp/.fs-conf-$_b"
-	ssh "$R" "[ -f /etc/config/$_b ] || { mv /tmp/.fs-conf-$_b /etc/config/$_b; echo '  installed /etc/config/$_b (was absent)'; }; rm -f /tmp/.fs-conf-$_b"
-done
-# No rpcd reload here — same race as the Makefile's postinst note (Package/luci-theme-
-# footstrap/postinst): this copies files onto a router where the theme registers no rpcd object,
-# rpcd re-reads acl.d/*.json at LOGIN (not only on reload), and a developer's root session already
-# carries read='*' write='*', so the theme's ACL group is irrelevant to it regardless.
-ssh "$R" "rm -f /tmp/luci-indexcache*"
-
-ssh "$R" "
-# Sweep every pre-consolidation name, including $N-top: the top bar is not a theme any more, so a
-# stale mediaurlbase pointing at its media dir would still render a theme LuCI no longer lists.
-#
-# rm -rf, not rm -f: these are DIRECTORIES (and $N-top a symlink to one, which rm -rf removes as the
-# link rather than its target). \`rm -f\` refuses a directory, 2>/dev/null swallows the error, and
-# this block runs without set -e — so the sweep silently does nothing. Every path is a literal built
-# from \$N: no glob, no user input.
-cd /www/luci-static
-rm -rf $N/$N $N-top $N-dark $N-light $N-top-dark $N-top-light
-cd /usr/share/ucode/luci/template/themes
-rm -rf $N/$N $N-top $N-dark $N-light $N-top-dark $N-top-light
-# The overview layout must not be left in luci-mod-status's global include dir, where LuCI loads
-# every *.js under any theme: it is a chrome module now. A package upgrade drops the old file, but
-# dev-sync copies rather than installs, so it has to be swept by name — otherwise the dev router
-# runs both copies and the grid is arranged twice.
-rm -f /www/luci-static/resources/view/status/include/05_footstrap_overview_layout.js
-for db in /lib/apk/db/installed /usr/lib/opkg/status; do [ -f \"\$db\" ] && touch \"\$db\"; done
-rm -f /tmp/luci-indexcache*"
-
-# Register by running the package's own uci-defaults script — the single source of truth
-# (ONE entry, luci.themes.Footstrap; layout, mode and palette are client-side toggles).
-# PKG_UPGRADE=1 forces its upgrade branch, so the dev router's active theme is left alone.
-scp -q "$D"/root/etc/uci-defaults/30_luci-theme-footstrap "$R":/tmp/30_luci-theme-footstrap
-ssh "$R" "PKG_UPGRADE=1 sh /tmp/30_luci-theme-footstrap; rm -f /tmp/30_luci-theme-footstrap /tmp/luci-indexcache*"
-
-
-# ---- bust the browser cache, or a dev deploy is invisible to the device you are testing on ----
-#
-# head.ut versions every asset with `?v={{ pkgs_update_time }}`, and luci-base derives that from the
-# mtime of the PACKAGE DATABASE. Installing a package moves it; scp'ing files over an existing
-# install does not — so a dev deploy leaves the URL identical while the bytes change, and a browser
-# is entitled to keep serving what it has. On a phone that is not a theory: an afternoon of "still
-# shakes" reports were taken against a stylesheet several deploys old.
-#
-# Touching the database file is what an install would have done to it, so the next page carries a
-# new `?v=` for every asset. Router-only, dev-only: nothing in the package does this.
-ssh "$R" 'touch /lib/apk/db/installed 2>/dev/null || touch /usr/lib/opkg/status 2>/dev/null || true'
-
-echo "synced to $R (single Footstrap theme registered, active theme unchanged)"
diff --git a/luci-theme-footstrap/htdocs/luci-static/footstrap/cascade.css b/luci-theme-footstrap/htdocs/luci-static/footstrap/cascade.css
new file mode 100644
index 000000000..baba2f05f
--- /dev/null
+++ b/luci-theme-footstrap/htdocs/luci-static/footstrap/cascade.css
@@ -0,0 +1,1068 @@
+/*!
+* luci-theme-footstrap
+* Copyright 2026 footstrap contributors
+* Licensed under the Apache License v2.0
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Portions of styles/base are derived from luci-theme-bootstrap, whose notices
+* the Apache License requires this file to carry for as long as that code ships:
+*   Copyright 2012 Nut & Bolt 
+*   Copyright 2011 Twitter, Inc
+*/
+@layer tokens,base,theme,page;@layer tokens{:root{--fs-shadow-pop:0 12px 34px rgba(0,0,0,.30);--fs-focus-ring:0 0 0 3px var(--fs-accent-soft);--fs-focus-ring-solo:0 0 0 2px var(--fs-panel),0 0 0 5px var(--fs-accent);--fs-focus-ring-invalid:0 0 0 3px var(--fs-danger-fill);--fs-row-rail:inset 2px 0 0 var(--fs-accent);--fs-hairline:1px solid var(--fs-border);--fs-field-transition:border-color var(--fs-dur),box-shadow var(--fs-dur);--fs-scrim:rgba(0,0,0,.7);--fs-good-soft:color-mix(in srgb,var(--fs-good) 12%,transparent);--fs-warn-soft:color-mix(in srgb,var(--fs-warn) 12%,transparent);--fs-danger-soft:color-mix(in srgb,var(--fs-danger) 12%,transparent);--fs-accent-fill:color-mix(in srgb,var(--fs-accent) 18%,transparent);--fs-good-fill:color-mix(in srgb,var(--fs-good) 18%,transparent);--fs-warn-fill:color-mix(in srgb,var(--fs-warn) 18%,transparent);--fs-danger-fill:color-mix(in srgb,var(--fs-danger) 18%,transparent);--fs-accent-line:color-mix(in srgb,var(--fs-accent) 40%,transparent);--fs-good-line:color-mix(in srgb,var(--fs-good) 40%,transparent);--fs-warn-line:color-mix(in srgb,var(--fs-warn) 40%,transparent);--fs-danger-line:color-mix(in srgb,var(--fs-danger) 40%,transparent);--fs-accent-line-hi:color-mix(in srgb,var(--fs-accent) 55%,transparent);--fs-good-line-hi:color-mix(in srgb,var(--fs-good) 55%,transparent);--fs-warn-line-hi:color-mix(in srgb,var(--fs-warn) 55%,transparent);--fs-danger-line-hi:color-mix(in srgb,var(--fs-danger) 55%,transparent);--fs-icon-refresh:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M4.4 9.6A8.5 8.5 0 0 1 18.6 7.6'/%3E%3Cpath d='M19.6 14.4A8.5 8.5 0 0 1 5.4 16.4'/%3E%3Cpath fill='%23000' stroke='none' d='M21.6 10.6 15.4 8.4 20.4 4.2Z'/%3E%3Cpath fill='%23000' stroke='none' d='M2.4 13.4 8.6 15.6 3.6 19.8Z'/%3E%3C/svg%3E");--fs-icon-refresh-sz:calc(18px * var(--fs-density-type));--fs-hover-lift:brightness(.90);--fs-dur:.15s;--fs-dur-move:.2s;--fs-dur-fade:.25s;--fs-dur-fill:.4s;--fs-content-max:1280px;--fs-content-pad:calc(28px * var(--fs-density-space));--fs-sidebar-w:calc(224px * var(--fs-density-box));--fs-rail-w:calc(68px * var(--fs-density-box));--fs-content-min:500px;--fs-bar-bg:var(--fs-panel);--fs-bar-ctl-bg:transparent;--fs-select-chevron:linear-gradient(-45deg,transparent 45%,var(--fs-dim) 45% 55%,transparent 55%),linear-gradient(45deg,transparent 45%,var(--fs-dim) 45% 55%,transparent 55%);--fs-bar-h:calc(46px * var(--fs-density-box));--fs-btn-size:calc(34px * var(--fs-density-box));--fs-radius-base:12px;--fs-radius-lg:var(--fs-radius-base);--fs-radius:calc(var(--fs-radius-base) * 5 / 6);--fs-radius-sm:calc(var(--fs-radius-base) * 2 / 3);--fs-radius-pill:999px;--fs-card-pad:calc(16px * var(--fs-density-space));--fs-card-gap:calc(16px * var(--fs-density-space));--fs-font-sans:"Manrope",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;--fs-font-mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace;--fs-density-type:1;--fs-density-space:1;--fs-density-box:1;--fs-ctl-h-sm:calc(30px * var(--fs-density-box));--fs-ctl-h:calc(38px * var(--fs-density-box));--fs-ctl-h-lg:calc(44px * var(--fs-density-box));--fs-type-2xs:calc(10px * var(--fs-density-type));--fs-type-xs:calc(11px * var(--fs-density-type));--fs-type:calc(13px * var(--fs-density-type));--fs-type-lg:calc(16px * var(--fs-density-type));--fs-type-xl:calc(20px * var(--fs-density-type));--fs-type-2xl:calc(26px * var(--fs-density-type));--fs-weight-normal:400;--fs-weight:600;--fs-weight-bold:700;--fs-leading:1.5;--fs-leading-tight:1.2;--fs-eyebrow-tracking:.06em;--fs-eyebrow-weight:700;--fs-eyebrow-color:var(--fs-faint);--fs-space-0-5:calc(2px * var(--fs-density-space));--fs-space-1:calc(4px * var(--fs-density-space));--fs-space-1-5:calc(6px * var(--fs-density-space));--fs-space-2:calc(8px * var(--fs-density-space));--fs-space-2-5:calc(10px * var(--fs-density-space));--fs-space-3:calc(12px * var(--fs-density-space));--fs-space-3-5:calc(14px * var(--fs-density-space));--fs-space-4:calc(16px * var(--fs-density-space));--fs-space-5:calc(20px * var(--fs-density-space));--fs-space-6:calc(24px * var(--fs-density-space));--fs-space-7:calc(28px * var(--fs-density-space));--fs-space-8:calc(32px * var(--fs-density-space));--fs-space-10:calc(40px * var(--fs-density-space));--fs-spin-size:var(--fs-space-5);--fs-control-h:calc(var(--fs-type) * var(--fs-leading) + var(--fs-space-1) * 2 + 2px);--fs-field-max:440px;--fs-z-raise:2;--fs-z-sticky:50;--fs-z-flyout:70;--fs-z-header:800;--fs-z-popover:850;--fs-z-overlay:900;--fs-z-tooltip:1000;--fs-z-dropdown:1100;--background-color-high:var(--fs-panel);--background-color-medium:color-mix(in srgb,var(--fs-panel) 55%,var(--fs-panel2));--background-color-low:var(--fs-panel2);--text-color-highest:var(--fs-text);--text-color-high:var(--fs-text);--text-color-medium:var(--fs-dim);--text-color-low:var(--fs-faint);--border-color-high:color-mix(in srgb,var(--fs-border) 60%,var(--fs-text));--border-color-medium:var(--fs-border);--border-color-low:color-mix(in srgb,var(--fs-border) 55%,var(--fs-panel));--primary-color-high:var(--fs-accent);--primary-color-medium:color-mix(in oklch,var(--fs-accent) 78%,var(--fs-dim));--primary-color-low:color-mix(in oklch,var(--fs-accent) 45%,var(--fs-dim));--on-primary-color:var(--fs-on-accent);--error-color-high:var(--fs-danger);--error-color-medium:color-mix(in oklch,var(--fs-danger) 78%,var(--fs-dim));--error-color-low:color-mix(in oklch,var(--fs-danger) 45%,var(--fs-dim));--on-error-color:var(--fs-on-danger);--success-color-high:var(--fs-good);--success-color-medium:color-mix(in oklch,var(--fs-good) 78%,var(--fs-dim));--success-color-low:color-mix(in oklch,var(--fs-good) 45%,var(--fs-dim));--on-success-color:var(--fs-on-good);--warn-color-high:var(--fs-warn);--warn-color-medium:color-mix(in oklch,var(--fs-warn) 78%,var(--fs-dim));--warn-color-low:color-mix(in oklch,var(--fs-warn) 45%,var(--fs-dim));--on-warn-color:var(--fs-on-warn);--fs-disabled-opacity:.6;color-scheme:light}
+@supports (width:round(1px,1px)){:root{--fs-type-2xs:calc(round(10px * var(--fs-density-type),1px));--fs-type-xs:calc(round(11px * var(--fs-density-type),1px));--fs-type:calc(round(13px * var(--fs-density-type),1px));--fs-type-lg:calc(round(16px * var(--fs-density-type),1px));--fs-type-xl:calc(round(20px * var(--fs-density-type),1px));--fs-type-2xl:calc(round(26px * var(--fs-density-type),1px));--fs-control-h:calc(round(up,calc(var(--fs-type) * var(--fs-leading) + var(--fs-space-1) * 2 + 2px),4px))}
+}
+:root[data-density="compact"]{--fs-density-type:.9;--fs-density-box:.85;--fs-density-space:.65}
+:root[data-density="large"]{--fs-density-type:1.15;--fs-density-box:1.15;--fs-density-space:1}
+:root[data-darkmode="true"]{--fs-disabled-opacity:.45;--fs-hover-lift:brightness(1.15);color-scheme:dark}
+:root{--fs-bg:var(--fs-bg-base);--fs-accent:var(--fs-accent-base);--fs-accent-soft:color-mix(in srgb,var(--fs-accent) 10%,transparent);--fs-panel:var(--fs-panel-base);--fs-panel2:var(--fs-panel2-base);--fs-border:var(--fs-border-base);--fs-good:var(--fs-good-base);--fs-warn:var(--fs-warn-base);--fs-danger:var(--fs-danger-base);--fs-placeholder-mix:55%;--fs-placeholder:color-mix(in oklch,var(--fs-text) var(--fs-placeholder-mix),var(--fs-panel2));--fs-accent-h:0;--fs-tint-h:0;--fs-good-h:0;--fs-warn-h:0;--fs-danger-h:0;--fs-tint-strength:1;--fs-login-bg-url:none;--fs-photo-dim:74%;--fs-photo-scrim:color-mix(in srgb,var(--fs-bg-base) var(--fs-photo-dim),transparent);--fs-pattern-url:none;--fs-pattern-size:440px;--fs-pattern-strength:.2;--fs-tint-c:calc((.016 + .012 * max(0,cos((var(--fs-tint-h) - 258) * 1deg))) * var(--fs-tint-strength))}
+:root[data-darkmode="true"]{--fs-tint-c:calc((.011 + .016 * max(0,cos((var(--fs-tint-h) - 258) * 1deg)) - .004 * max(0,cos((var(--fs-tint-h) - 55) * 1deg))) * var(--fs-tint-strength));--fs-placeholder-mix:64%}
+:root[data-tint="hue"]{--fs-bg:oklch(from var(--fs-bg-base) l var(--fs-tint-c) var(--fs-tint-h))}
+:root[data-accent="hue"]{--fs-accent:oklch(from var(--fs-accent-base) l c var(--fs-accent-h))}
+:root[data-good="hue"]{--fs-good:oklch(from var(--fs-good-base) l c var(--fs-good-h))}
+:root[data-warn="hue"]{--fs-warn:oklch(from var(--fs-warn-base) l c var(--fs-warn-h))}
+:root[data-danger="hue"]{--fs-danger:oklch(from var(--fs-danger-base) l c var(--fs-danger-h))}
+:root[data-accent="hex"][data-accent]{--fs-on-accent:oklch(from var(--fs-accent) clamp(0,(l - .62) * -100,1) 0 0)}
+:root[data-good="hex"][data-good]{--fs-on-good:oklch(from var(--fs-good) clamp(0,(l - .62) * -100,1) 0 0)}
+:root[data-warn="hex"][data-warn]{--fs-on-warn:oklch(from var(--fs-warn) clamp(0,(l - .62) * -100,1) 0 0)}
+:root[data-danger="hex"][data-danger]{--fs-on-danger:oklch(from var(--fs-danger) clamp(0,(l - .62) * -100,1) 0 0)}
+:root,:root[data-palette="footstrap"]{--fs-bg-base:#f6f8fa;--fs-panel-base:#fff;--fs-panel2-base:#f6f8fa;--fs-border-base:#d0d7de;--fs-text:#1f2328;--fs-dim:#33383d;--fs-faint:#3d4348;--fs-accent-base:#0969da;--fs-track:#eaeef2;--fs-good-base:#197c36;--fs-warn-base:#946300;--fs-danger-base:#cf222e;--fs-on-accent:#fff;--fs-on-good:#fff;--fs-on-warn:#fff;--fs-on-danger:#fff}
+:root[data-darkmode="true"],:root[data-palette="footstrap"][data-darkmode="true"]{--fs-bg-base:#1c2128;--fs-panel-base:#22272e;--fs-panel2-base:#2d333b;--fs-border-base:#444c56;--fs-text:#adbac7;--fs-dim:#8f9cab;--fs-faint:#959ca5;--fs-accent-base:#569df5;--fs-accent-soft:color-mix(in srgb,var(--fs-accent) 15%,transparent);--fs-track:#373e47;--fs-good-base:#5aad5d;--fs-warn-base:#c79128;--fs-danger-base:#ea7a74;--fs-on-accent:#10151c;--fs-on-good:#10151c;--fs-on-warn:#10151c;--fs-on-danger:#10151c}
+:root[data-palette="hicontrast"]{--fs-bg-base:#eef1f6;--fs-panel-base:#fff;--fs-panel2-base:#f6f8fb;--fs-border-base:#e4e9f1;--fs-text:#101725;--fs-dim:#272f3d;--fs-faint:#2e3745;--fs-accent-base:#0b6fbd;--fs-track:#eaeef4;--fs-good-base:#0f7a52;--fs-warn-base:#8a5a00;--fs-danger-base:#c02b2b;--fs-on-accent:#fff;--fs-on-good:#fff;--fs-on-warn:#fff;--fs-on-danger:#fff}
+:root[data-palette="hicontrast"][data-darkmode="true"]{--fs-bg-base:#0a0d13;--fs-panel-base:#111620;--fs-panel2-base:#161c28;--fs-border-base:#232b3a;--fs-text:#e8edf5;--fs-dim:#9ba6b8;--fs-faint:#8f9aad;--fs-accent-base:#3ba7ff;--fs-accent-soft:color-mix(in srgb,var(--fs-accent) 14%,transparent);--fs-track:#1b2230;--fs-good-base:#43e0a0;--fs-warn-base:#ffb454;--fs-danger-base:#ff6b6b;--fs-on-accent:#0a0d13;--fs-on-good:#0a0d13;--fs-on-warn:#0a0d13;--fs-on-danger:#0a0d13}
+:root[data-palette="bootstrap"]{--fs-bg-base:#fff;--fs-panel-base:#fff;--fs-panel2-base:#f5f5f5;--fs-border-base:#ccc;--fs-text:#232323;--fs-dim:#383838;--fs-faint:#424242;--fs-accent-base:#1564c0;--fs-track:#eee;--fs-good-base:#007936;--fs-warn-base:#826706;--fs-danger-base:#d00000;--fs-on-accent:#fff;--fs-on-good:#fff;--fs-on-warn:#fff;--fs-on-danger:#fff}
+:root[data-palette="bootstrap"][data-darkmode="true"]{--fs-bg-base:#222;--fs-panel-base:#282828;--fs-panel2-base:#303030;--fs-border-base:#444;--fs-text:#bfbfbf;--fs-dim:#a6a6a6;--fs-faint:#9a9a9a;--fs-accent-base:#53a4c2;--fs-accent-soft:color-mix(in srgb,var(--fs-accent) 15%,transparent);--fs-track:#333;--fs-good-base:#1aaf7b;--fs-warn-base:#aa9968;--fs-danger-base:#dd807e;--fs-on-accent:#222;--fs-on-good:#222;--fs-on-warn:#222;--fs-on-danger:#222}
+:root[data-palette="2020"]{--fs-bg-base:#fff;--fs-panel-base:#fff;--fs-panel2-base:#eef4f8;--fs-border-base:#c3d2dd;--fs-text:#10202b;--fs-dim:#2c4150;--fs-faint:#3d5566;--fs-accent-base:#036c92;--fs-track:#e3ebf1;--fs-good-base:#276e2a;--fs-warn-base:#8a5a00;--fs-danger-base:#c1121f;--fs-on-accent:#fff;--fs-on-good:#fff;--fs-on-warn:#fff;--fs-on-danger:#fff}
+:root[data-palette="2020"][data-darkmode="true"]{--fs-bg-base:#002b49;--fs-panel-base:#0a3a58;--fs-panel2-base:#0e4362;--fs-border-base:#1d5a7d;--fs-text:#e6f1f7;--fs-dim:#b8cfdd;--fs-faint:#9dbccd;--fs-accent-base:#35c6ea;--fs-accent-soft:color-mix(in srgb,var(--fs-accent) 15%,transparent);--fs-track:#123c56;--fs-good-base:#74cc74;--fs-warn-base:#eab346;--fs-danger-base:#ff9d9d;--fs-on-accent:#002b49;--fs-on-good:#002b49;--fs-on-warn:#002b49;--fs-on-danger:#002b49}
+:root[data-palette="forum"]{--fs-bg-base:#fff;--fs-panel-base:#fff;--fs-panel2-base:#f0f0f0;--fs-border-base:#ddd;--fs-text:#222;--fs-dim:#3d3d3d;--fs-faint:#4a4a4a;--fs-accent-base:#0073ac;--fs-track:#eee;--fs-good-base:#008000;--fs-warn-base:#717100;--fs-danger-base:#c63c1b;--fs-on-accent:#fff;--fs-on-good:#fff;--fs-on-warn:#fff;--fs-on-danger:#fff}
+:root[data-palette="forum"][data-darkmode="true"]{--fs-bg-base:#222;--fs-panel-base:#262626;--fs-panel2-base:#2e2e2e;--fs-border-base:#3a3a3a;--fs-text:#ddd;--fs-dim:#a3a3a3;--fs-faint:#969696;--fs-accent-base:#13a8e3;--fs-accent-soft:color-mix(in srgb,var(--fs-accent) 15%,transparent);--fs-track:#333;--fs-good-base:#1fb65a;--fs-warn-base:#c99259;--fs-danger-base:#eb8066;--fs-on-accent:#222;--fs-on-good:#222;--fs-on-warn:#222;--fs-on-danger:#222;--fs-bar-bg:#002b49}
+@supports not (color:color-mix(in srgb,red 50%,transparent)){:root{--fs-accent-soft:transparent;--fs-good-soft:transparent;--fs-warn-soft:transparent;--fs-danger-soft:transparent;--fs-accent-fill:transparent;--fs-good-fill:transparent;--fs-warn-fill:transparent;--fs-danger-fill:transparent;--fs-accent-line:var(--fs-accent);--fs-good-line:var(--fs-good);--fs-warn-line:var(--fs-warn);--fs-danger-line:var(--fs-danger);--fs-accent-line-hi:var(--fs-accent);--fs-good-line-hi:var(--fs-good);--fs-warn-line-hi:var(--fs-warn);--fs-danger-line-hi:var(--fs-danger);--fs-focus-ring:var(--fs-focus-ring-solo);--fs-focus-ring-invalid:0 0 0 2px var(--fs-panel),0 0 0 5px var(--fs-danger);--background-color-medium:var(--fs-panel2);--fs-photo-scrim:var(--fs-scrim);--fs-placeholder:var(--fs-faint);--border-color-high:var(--fs-border);--border-color-low:var(--fs-border);--primary-color-medium:var(--fs-accent);--primary-color-low:var(--fs-dim);--error-color-medium:var(--fs-danger);--error-color-low:var(--fs-dim);--success-color-medium:var(--fs-good);--success-color-low:var(--fs-dim);--warn-color-medium:var(--fs-warn);--warn-color-low:var(--fs-dim)}
+}
+}
+@layer base{*,*::before,*::after{scroll-margin-top:var(--fs-bar-h);margin:0;padding:0;border:0;box-sizing:border-box}
+abbr[title],acronym[title]{border-bottom:1px dotted;font-weight:inherit;cursor:help}
+table{border-collapse:collapse;border-spacing:0}
+[hidden]:not([hidden="until-found"]){display:none !important}
+html{font-size:100%;height:100%}
+a:focus-visible{outline:2px solid var(--fs-accent);outline-offset:2px}
+sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
+sup{top:-.5em}
+sub{bottom:-.25em}
+button,input,select,option,textarea{margin:0;box-sizing:border-box;vertical-align:baseline}
+button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;appearance:button;word-break:break-all}
+input[type="search"]{appearance:textfield}
+input[type="search"]::-webkit-search-decoration{appearance:none}
+textarea{overflow:auto;vertical-align:top;font-family:var(--fs-font-mono)}
+.control-group{display:inline-flex;gap:var(--fs-space-2)}
+body{font-size:var(--fs-type);font-weight:var(--fs-weight-normal);line-height:var(--fs-leading);color:var(--fs-text);min-height:100%;display:flex;flex-direction:column;scrollbar-gutter:stable}
+a{color:var(--fs-accent);text-decoration:none;line-height:inherit;font-weight:inherit}
+a:hover{color:var(--fs-accent);text-decoration:underline}
+.nowrap:not(span){white-space:nowrap}
+p,.cbi-map-descr,.cbi-section-descr,.table .tr.cbi-section-table-descr .th{font-size:var(--fs-type);font-weight:var(--fs-weight-normal);margin-bottom:var(--fs-space-2);text-wrap:pretty}
+p small{font-size:var(--fs-type-xs);color:var(--fs-dim)}
+h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-size:.7em;font-weight:var(--fs-weight-normal);color:var(--fs-dim)}
+ul,ol{margin:0 0 var(--fs-space-4) var(--fs-space-6)}
+ul ul,ul ol,ol ol,ol ul{margin-bottom:0}
+ul{list-style:disc}
+ol{list-style:decimal}
+li{color:var(--fs-dim)}
+dl{margin-bottom:var(--fs-space-4)}
+dl dt{font-weight:var(--fs-weight-bold)}
+dl dd{margin-inline-start:var(--fs-space-2)}
+hr{margin:var(--fs-space-5) 0;border:0;border-bottom:var(--fs-hairline)}
+strong{font-style:inherit;font-weight:var(--fs-weight-bold)}
+em{font-style:italic;font-weight:inherit;line-height:inherit}
+small{font-size:var(--fs-type)}
+address{display:block;margin-bottom:var(--fs-space-4)}
+code,pre{font-family:var(--fs-font-mono);font-size:var(--fs-type);border-radius:var(--fs-radius-sm)}
+code{background-color:var(--fs-panel2);color:var(--fs-text);padding:1px var(--fs-space-1);overflow-wrap:anywhere}
+pre{--border-color:color-mix(in srgb,var(--fs-border) 15%,transparent);background-color:var(--fs-panel2);display:block;padding:var(--fs-space-2);margin:0 0 var(--fs-space-4);border:1px solid var(--border-color);border-radius:var(--fs-radius-sm);white-space:pre-wrap;word-wrap:break-word}
+fieldset{margin-bottom:var(--fs-space-2);padding-top:var(--fs-space-2)}
+fieldset legend{display:block;line-height:1;color:var(--fs-text);padding-top:var(--fs-space-5)}
+label,input,button{font-family:var(--fs-font-sans);font-size:var(--fs-type);font-weight:var(--fs-weight-normal);line-height:normal}
+.cbi-value{display:flex;flex-wrap:wrap;margin-bottom:var(--fs-space-4);align-items:baseline}
+.cbi-value-field{margin-inline-start:var(--fs-space-5);flex:1;min-width:0}
+.cbi-value label.cbi-value-title{padding-top:var(--fs-space-1);flex:0 0 180px;text-align:end;min-width:0}
+.cbi-value>.cbi-section,.cbi-value>.cbi-tblsection{width:100%}
+label>input[type="checkbox"],label>input[type="radio"]{vertical-align:text-top;margin:0}
+label[for]{cursor:pointer}
+.cbi-dynlist{height:auto;display:inline-flex;flex-direction:column}
+.cbi-dynlist>.item{margin-bottom:var(--fs-space-1);padding:var(--fs-space-1) 2em var(--fs-space-1) var(--fs-space-2);position:relative;pointer-events:auto;overflow:hidden;word-break:break-all;cursor:move;user-select:text}
+.cbi-dynlist>.item::after{content:"×";position:absolute;align-items:center;top:-1px;inset-inline-end:-1px;bottom:-1px;font-weight:var(--fs-weight-bold);pointer-events:auto}
+.cbi-dynlist>.item.drag-over{border-top:1px solid var(--fs-text)}
+.cbi-dynlist>.item.dragging{opacity:.5}
+.cbi-dynlist>.item>span{pointer-events:none}
+.cbi-dynlist>.add-item{display:flex}
+.cbi-dynlist>.add-item>.cbi-input-text.drag-over{border-top:1px solid var(--fs-text)}
+.cbi-dynlist>.add-item>.cbi-button-add.drag-over{border-top:1px solid var(--fs-text)}
+.cbi-dynlist>.add-item>input,.cbi-dynlist>.add-item>button{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.cbi-value-field>.cbi-checkbox,.cbi-value-field>div>.cbi-radio{height:var(--fs-control-h);display:inline-flex;align-items:center}
+.cbi-radio{cursor:pointer;gap:var(--fs-space-1)}
+::placeholder{color:var(--fs-placeholder)}
+input[disabled],button[disabled],select[disabled],textarea[disabled],.cbi-dropdown[disabled]:not(.btn):not(.cbi-button),input[type="checkbox"][disabled]::before,input[type="checkbox"][disabled]::after,input[type="radio"][disabled]::before,input[type="radio"][disabled]::after{opacity:var(--fs-disabled-opacity);pointer-events:none;cursor:default}
+.cbi-optionals,.cbi-section-create{padding:0 0 var(--fs-space-3) var(--fs-space-3)}
+.cbi-section-create{display:inline-flex;align-items:center;gap:var(--fs-space-2)}
+.cbi-section-create>*{flex:1 1 auto}
+.actions{background:var(--fs-panel2);margin-bottom:var(--fs-space-4);padding:var(--fs-card-pad);border-top:var(--fs-hairline);border-radius:0 0 var(--fs-radius-lg) var(--fs-radius-lg);text-align:end}
+.actions .secondary-action,.cbi-page-actions .secondary-action{float:right}
+:root[dir="rtl"] .actions .secondary-action,:root[dir="rtl"] .cbi-page-actions .secondary-action{float:left}
+.actions .secondary-action a,.cbi-page-actions .secondary-action a{line-height:var(--fs-control-h)}
+.actions .secondary-action a:hover,.cbi-page-actions .secondary-action a:hover{text-decoration:underline}
+.cbi-page-actions>form{display:inline;margin:0}
+.tr{display:table-row}
+.table[width="33%"],.th[width="33%"],.td[width="33%"]{width:33%}
+.table[width="100%"],.th[width="100%"],.td[width="100%"]{width:100%}
+.table{display:table;width:100%;margin-bottom:var(--fs-space-4);padding:0;font-size:var(--fs-type);border-collapse:collapse;position:relative}
+.table .th,.table .td{display:table-cell;vertical-align:middle;padding:var(--fs-space-2) var(--fs-space-3);text-align:start}
+.table .td{overflow-wrap:anywhere}
+.table .th{overflow-wrap:break-word}
+.table .tr>.td:first-child{overflow-wrap:break-word}
+.table .tr:first-child .th{padding-top:var(--fs-space-2);font-weight:var(--fs-weight-bold);vertical-align:top}
+.tr.placeholder{height:calc(3em + var(--fs-space-4))}
+.tr.placeholder>.td{position:absolute;inset-inline:0;text-align:center;line-height:3em;padding-block:var(--fs-space-2)}
+.tr.drag-over-above,.tr.drag-over-below{border:2px solid var(--fs-accent);border-width:2px 0 0 0}
+.tr.drag-over-below{border-width:0 0 2px 0}
+header{position:sticky;top:0;z-index:var(--fs-z-header);overflow:visible;color:var(--fs-dim);display:flex}
+li.menu,.dropdown{position:relative}
+.dropdown:hover ul.dropdown-menu{inset-inline-start:0}
+.tabs>li,.cbi-tabmenu>li{flex:0 1 auto;display:flex;align-items:center}
+.tabs>li>a,.cbi-tabmenu>li>a{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-decoration:none;outline:none}
+.cbi-tab-disabled[data-errors]::after{content:attr(data-errors);color:var(--fs-on-danger);height:var(--fs-space-4);min-width:var(--fs-space-4);border-radius:var(--fs-radius-pill);text-align:center;margin-inline-end:var(--fs-space-1);padding:0 var(--fs-space-1);display:inline-flex;flex-direction:column;justify-content:center;font-size:var(--fs-type)}
+.cbi-tab-descr{margin:calc(var(--fs-space-2) * -1) 0 var(--fs-space-4) 0}
+.tabs .menu-dropdown,.tabs .dropdown-menu{top:100%;border-width:1px;border-radius:0 var(--fs-radius) var(--fs-radius) var(--fs-radius)}
+.tabs a.menu:after,.tabs .dropdown-toggle:after{border-top-color:var(--fs-dim);align-self:center;margin-inline-start:var(--fs-space-1)}
+.tabs li.open.menu .menu,.tabs .open.dropdown .dropdown-toggle{border-color:var(--fs-border)}
+.tabs li.open a.menu:after,.tabs .dropdown.open .dropdown-toggle:after{border-top-color:var(--fs-text)}
+.breadcrumb{padding:var(--fs-space-2) var(--fs-space-3);margin:0 0 var(--fs-space-4);background:linear-gradient(to bottom,var(--fs-panel),var(--fs-panel2));border:var(--fs-hairline);border-radius:var(--fs-radius);display:flex;flex:0}
+.breadcrumb li{list-style:none}
+.breadcrumb li:not(:last-child)::after{content:"|";padding:0 var(--fs-space-1)}
+.breadcrumb .active a{color:var(--fs-dim)}
+footer{margin-top:auto;display:flex;flex-wrap:wrap;align-items:baseline}
+footer ul.breadcrumb{margin-inline-start:auto}
+#modal_overlay{position:fixed;top:0;bottom:0;background:var(--fs-scrim);z-index:var(--fs-z-overlay);overflow:auto;transition:opacity var(--fs-dur);opacity:0;visibility:hidden}
+.modal{width:90%;margin:var(--fs-space-6) auto;min-height:var(--fs-control-h);max-width:600px;border-radius:var(--fs-radius-lg);background:var(--fs-panel);border:var(--fs-hairline);padding:var(--fs-card-pad);min-width:270px}
+.modal>.button-row{display:flex;justify-content:space-between;flex-wrap:wrap}
+.modal>.button-row>button:not(:last-of-type){margin-inline-end:var(--fs-space-2)}
+.modal>.button-row>button:first-of-type{margin-inline-end:auto}
+body.modal-overlay-active{overflow:hidden;height:100svh}
+body.modal-overlay-active #modal_overlay{inset-inline:0;opacity:1;visibility:visible}
+.btn .close,.alert-message .close{font-family:var(--fs-font-sans);line-height:1}
+.btn,.cbi-button{--on-color:var(--fs-text);cursor:pointer;font-size:var(--fs-type);white-space:pre}
+.cbi-input-invalid,.cbi-input-invalid.cbi-dropdown:not(.btn):not(.cbi-button),.cbi-input-invalid.cbi-dropdown:not([open])>ul>li,.cbi-value-error input{color:var(--fs-danger)}
+.cbi-button-positive,.cbi-button-fieldadd,.cbi-button-add,.cbi-button-save{--on-color:var(--fs-good)}
+.btn.primary,.cbi-button-action,.cbi-button-apply,.cbi-button-reload,.cbi-button-edit{--on-color:var(--fs-accent)}
+.cbi-button-negative,.cbi-section-remove .cbi-button,.cbi-button-reset,.cbi-button-remove{--on-color:var(--fs-danger)}
+.cbi-page-actions::after{display:table;content:"";clear:both}
+.cbi-page-actions>*{vertical-align:middle}
+.cbi-page-actions>:not([method="post"]):not(.cbi-button-apply):not(.cbi-button-negative):not(.cbi-button-save):not(.cbi-button-reset){margin-inline-end:var(--fs-space-1)}
+.btn.primary,.cbi-button-action.important,.cbi-page-actions .cbi-button-apply,.cbi-section-actions .cbi-button-edit{--on-color:var(--fs-on-accent)}
+.cbi-button-positive.important,.cbi-page-actions .cbi-button-save{--on-color:var(--fs-on-good)}
+.cbi-button-negative.important{--on-color:var(--fs-on-danger)}
+.cbi-page-actions .cbi-button-apply + .cbi-button-save,.cbi-page-actions .cbi-button-negative + .cbi-button-save{--on-color:var(--fs-good)}
+.cbi-title-ref::after{content:"➙"}
+.cbi-tooltip-container{cursor:help}
+.cbi-tooltip{position:absolute;z-index:var(--fs-z-tooltip);inset-inline-start:-10000px;border-radius:var(--fs-radius-sm);background:var(--fs-panel);white-space:pre-wrap;padding:var(--fs-space-0-5) var(--fs-space-1);opacity:0;transition:opacity var(--fs-dur-fade)}
+.cbi-tooltip-container:hover .cbi-tooltip:not(:empty){inset-inline-start:auto;opacity:1;transition:opacity var(--fs-dur-fade)}
+.cbi-progressbar>div{width:0%}
+.zonebadge .cbi-tooltip{padding:1px;background:inherit;margin:-1.6em 0 0 -5px;border-radius:var(--fs-radius-sm);pointer-events:none}
+.zonebadge .cbi-tooltip>*{margin:1px;z-index:var(--fs-z-raise);position:relative}
+.zone-forwards{display:flex;flex-wrap:nowrap}
+.zone-forwards>*{flex:1 1 40%;padding:1px;text-wrap:auto}
+.zone-forwards>span{flex-basis:10%;text-align:center}
+.zone-forwards .zone-src,.zone-forwards .zone-dest{display:flex;flex-direction:column}
+.btn.disabled,.btn[disabled]{cursor:default;opacity:var(--fs-disabled-opacity)}
+.close{float:right;font-size:var(--fs-type-xl);font-weight:var(--fs-weight-bold);line-height:1}
+:root[dir="rtl"] .close{float:left}
+.close:hover{text-decoration:none}
+.alert-message{position:relative;margin-bottom:var(--fs-space-2);white-space:unset}
+.alert-message .close{margin-top:1px}
+.alert-message button{margin:var(--fs-space-1) 0}
+header [data-indicator]{white-space:nowrap;text-shadow:none;margin:var(--fs-space-1) 0 var(--fs-space-1) var(--fs-space-1)}
+form.inline{display:inline;margin-bottom:0}
+#modemenu li:last-child span.divider{display:none}
+.cbi-section-table .tr:not(.placeholder):hover .td,.cbi-section-table .tr:not(.placeholder):hover .th,.cbi-section-table .tr:not(.placeholder):hover::before{background-color:var(--fs-panel2)}
+.cbi-section-table .tr.cbi-section-table-descr .th{font-weight:var(--fs-weight-normal)}
+.cbi-section-table-titles{font-weight:var(--fs-weight-bold)}
+.cbi-section-table-titles.named[data-title]::before,.cbi-section-table-titles.named[data-title] ~ .cbi-section-table-descr.named::before{content:attr(data-title) " ";display:table-cell;vertical-align:middle}
+.cbi-section-table-row[data-title]:not(:has(>.cbi-section-table-titles))::before{content:attr(data-title) " ";display:table-cell;vertical-align:middle}
+.left{text-align:left !important}
+.right{text-align:right !important}
+.center{text-align:center !important}
+.top{vertical-align:top !important}
+.middle{vertical-align:middle !important}
+.bottom{vertical-align:bottom !important}
+table table td,.cbi-value-field table td{border:none}
+.table.cbi-section-table .td.cbi-section-table-cell{white-space:nowrap;text-align:end}
+.td.cbi-section-actions{text-align:end;vertical-align:middle;width:15%}
+.td.cbi-section-actions>*{display:flex}
+.td.cbi-section-actions>:not(.cbi-dropdown)>*,.td.cbi-section-actions>*>form>*{flex:1 1 4em;margin:0 var(--fs-space-1)}
+.td.cbi-section-actions>*>form{display:inline-flex;margin:0}
+.table.valign-middle .td{vertical-align:middle}
+.cbi-rowstyle-2,.tr.table-titles,.tr.cbi-section-table-titles{background:var(--fs-panel2)}
+th[data-sortable-row]{cursor:pointer}
+th[data-sort-direction="asc"]::after{content:"\a0\25b2"}
+th[data-sort-direction="desc"]::after{content:"\a0\25bc"}
+.cbi-value-description{--help-indent:1.25em;margin:var(--fs-space-1) 0 0 var(--help-indent);position:relative}
+.cbi-value-description:not(:empty)::before{--help-icon:url("data:image/svg+xml,");position:absolute;inset-inline-start:calc(var(--help-indent) * -1);content:"\a0";display:inline-block;width:1em;height:1em;margin-inline-end:var(--fs-space-1);background:var(--fs-accent);mask-image:var(--help-icon);mask-size:cover;-webkit-mask-image:var(--help-icon);-webkit-mask-size:cover}
+.cbi-section-error{border:1px solid var(--fs-danger);border-radius:var(--fs-radius);padding:var(--fs-space-2);margin-bottom:var(--fs-space-4)}
+.cbi-section-error ul{margin:0 0 0 var(--fs-space-5)}
+.cbi-section-error ul li{font-weight:var(--fs-weight-bold)}
+.ifacebox{border:var(--fs-hairline);margin:0 var(--fs-space-3);text-align:center;white-space:nowrap;border-radius:var(--fs-radius-sm);display:inline-flex;flex-direction:column;line-height:var(--fs-leading-tight);min-width:100px}
+.ifacebox .ifacebox-head{border-bottom:var(--fs-hairline);border-radius:calc(var(--fs-radius-sm) - 1px) calc(var(--fs-radius-sm) - 1px) 0 0;padding:var(--fs-space-0-5)}
+[data-darkmode="true"] .zonebadge[style],[data-darkmode="true"] .ifacebox-head[style]{background:linear-gradient(rgba(var(--zone-color-rgb),.4),rgba(var(--zone-color-rgb),.3)) !important}
+.ifacebox .ifacebox-body{padding:var(--fs-space-1);white-space:normal;overflow-wrap:anywhere}
+#cbi-network-interface .ifacebox img{width:24px;height:24px}
+.ifacebadge{display:inline-block;flex-direction:row;border:var(--fs-hairline);padding:var(--fs-space-0-5);color:var(--fs-text);border-radius:var(--fs-radius-sm);cursor:default;line-height:var(--fs-leading-tight)}
+.ifacebadge img{width:32px;height:32px;vertical-align:middle}
+.ifacebadge-active{font-weight:var(--fs-weight-bold)}
+.network-status-table{display:flex;flex-wrap:wrap}
+.network-status-table .ifacebox{margin:var(--fs-space-2);flex-grow:1}
+.network-status-table .ifacebox-body{display:flex;flex-direction:column;height:100%;text-align:start}
+.network-status-table .ifacebox-head{white-space:normal;overflow-wrap:anywhere}
+.network-status-table .ifacebox-body>*{margin:var(--fs-space-1)}
+.network-status-table .ifacebox-body>span{flex:10 10 auto;height:100%}
+.network-status-table .ifacebox-body>div{margin:-.125em;display:flex;flex-wrap:wrap}
+#dsl_status_table .ifacebox-body span>strong{display:inline-block;min-width:35%}
+.ifacebadge.large,.network-status-table .ifacebox-body .ifacebadge{display:flex;flex:1;padding:var(--fs-space-1);min-width:220px;margin:var(--fs-space-1)}
+.ifacebadge.large{display:inline-flex}
+.network-status-table .ifacebox-body .ifacebadge>span{overflow:hidden;text-overflow:ellipsis}
+.ifacebadge>*,.ifacebadge.large>*{margin:0 var(--fs-space-1)}
+.zonebadge{padding:var(--fs-space-0-5);border-radius:var(--fs-radius-sm);display:inline-block;white-space:nowrap;color:var(--fs-text)}
+.zonebadge>em,.zonebadge>strong{margin:0 var(--fs-space-1);display:inline-block}
+.zonebadge>.ifacebadge{margin-inline-start:var(--fs-space-0-5)}
+.zonebadge-empty{border:1px dashed var(--fs-border);font-style:italic;font-size:smaller}
+div.cbi-value var,.td.cbi-value-field var{font-style:italic}
+div.cbi-value var[data-tooltip],.td.cbi-value-field var[data-tooltip],div.cbi-value var.cbi-tooltip-container,.td.cbi-value-field var.cbi-tooltip-container{cursor:help;border-bottom:1px dotted var(--fs-accent)}
+div.cbi-value var.cbi-tooltip-container,.td.cbi-value-field var.cbi-tooltip-container .cbi-tooltip{font-style:normal;white-space:normal;color:var(--fs-text)}
+#modal_overlay>.modal.uci-dialog,#modal_overlay>.modal.cbi-modal{max-width:900px}
+#modal_overlay>.modal.login>button.important{font-size:var(--fs-type-lg);margin-top:var(--fs-space-2);width:100%}
+.uci-change-list{line-height:var(--fs-leading);white-space:pre}
+.uci-dialog div>del,.uci-dialog div>ins,.uci-dialog div>var{margin-bottom:var(--fs-space-0-5);border:var(--fs-hairline);line-height:var(--fs-leading-tight);overflow:hidden;text-overflow:ellipsis;padding:var(--fs-space-0-5);position:relative;background-color:var(--fs-panel2)}
+.uci-dialog div>ins{background-color:var(--fs-good-fill);border-color:var(--fs-good-line)}
+.uci-dialog div>del{background-color:var(--fs-danger-fill);border-color:var(--fs-danger-line)}
+.uci-dialog var>ins{background-color:var(--fs-good-fill)}
+.uci-dialog var>del{background-color:var(--fs-danger-fill)}
+.uci-change-list>var>*{overflow:hidden;text-overflow:ellipsis}
+.uci-dialog del,.uci-dialog ins,.uci-dialog var{text-decoration:none;font-family:var(--fs-font-mono);font-style:normal;color:var(--fs-text);display:block}
+.uci-change-legend{display:flex;flex-wrap:wrap}
+.uci-change-legend-label{flex-basis:150px;margin:var(--fs-space-0-5);display:flex;align-items:center}
+.uci-change-legend-label>ins,.uci-change-legend-label>del,.uci-change-legend-label>var{margin-inline-end:var(--fs-space-1);width:16px;height:16px;display:flex}
+.uci-change-legend-label>*>*{flex-basis:100%;padding:1px}
+#applyreboot-section{line-height:300%}
+@keyframes flash{0%{opacity:1}
+50%{opacity:.5}
+100%{opacity:1}
+}
+.flash{animation:flash var(--fs-dur-fade)}
+#view>div.spinning:first-child{display:table;margin:15vh auto}
+.spinning::before{--spinner-icon:url("data:image/svg+xml,");display:inline-block;flex:0 0 auto;vertical-align:middle;margin-inline-end:var(--fs-space-1);width:var(--fs-spin-size);height:var(--fs-spin-size);content:" ";background:var(--on-color,#000);mask:var(--spinner-icon) center/cover no-repeat;-webkit-mask:var(--spinner-icon) center/cover no-repeat;animation:fs-spin 1s linear infinite}
+@keyframes fs-spin{to{transform:rotate(360deg)}}
+[data-darkmode="true"] .spinning::before{background:var(--on-color,#fff)}
+[data-tab-title]{height:0;opacity:0;overflow:hidden}
+[data-tab-active="true"],.cbi-filebrowser.open{opacity:1;height:auto;overflow:visible;transition:opacity var(--fs-dur-fade)}
+.cbi-filebrowser{min-width:min(210px,100%);max-width:100%;border:var(--fs-hairline);display:flex;flex-direction:column;opacity:0;height:0;overflow:hidden}
+.cbi-filebrowser>*{max-width:100%;overflow:hidden;text-overflow:ellipsis;margin:.25em .25em 0 .25em;white-space:nowrap;border-bottom:var(--fs-hairline)}
+.cbi-filebrowser .cbi-button-positive{margin-inline-end:.25em}
+.cbi-filebrowser>.right{white-space:normal;overflow:visible}
+.cbi-filebrowser>div{border-bottom:none}
+.cbi-filebrowser>ul>li{display:flex;flex-direction:row}
+.cbi-filebrowser>ul>li>div:first-child{overflow:hidden;text-overflow:ellipsis}
+.cbi-filebrowser>ul>li>div:last-child{text-align:end}
+.cbi-filebrowser>ul>li>div:last-child>button{padding:.125em .25em}
+.cbi-filebrowser .upload{display:flex;flex-direction:row;flex-wrap:wrap;margin:0 -.125em .25em -.125em;padding:0 0 .125em 0;border-bottom:var(--fs-hairline)}
+.cbi-filebrowser .upload>*{margin:.125em;flex:1}
+.cbi-filebrowser .upload>.btn{flex-basis:60px}
+.cbi-filebrowser .upload>div{flex:10;min-width:150px}
+@keyframes fade-in{0%{opacity:0}
+100%{opacity:1}
+}
+@keyframes fade-out{0%{opacity:1}
+100%{opacity:0}
+}
+@keyframes fs-fade{from{opacity:0;transform:translateY(4px)}
+to{opacity:1;transform:none}
+}
+.fade-out{animation:fade-out var(--fs-dur-fade)}
+.assoclist .ifacebadge{display:flex;flex-direction:column;align-items:center;white-space:normal;text-align:center}
+.assoclist .ifacebadge>img{margin:var(--fs-space-1)}
+.assoclist .td:nth-of-type(3),.assoclist .td:nth-of-type(5){width:25%}
+.assoclist .td:nth-of-type(6) button{word-break:normal}
+[data-darkmode="true"] [data-page="admin-statistics-graphs"] [data-plugin] img{filter:invert(100%) hue-rotate(180deg)}
+#view div[style]>svg{background-color:var(--fs-panel)}
+#view div[style]>svg line[style]{stroke:var(--fs-text) !important}
+#view div[style]>svg>text[style]{fill:var(--fs-text) !important;text-shadow:none !important;font-weight:var(--fs-weight-bold)}
+}
+@layer theme{:where([data-fs-chrome]:not([data-fs-chrome] *)){font-style:normal;font-variant:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;text-indent:0;text-align:start;cursor:auto}
+.fs-brand{display:flex;align-items:center;gap:var(--fs-space-2-5);color:var(--fs-text);font-weight:var(--fs-weight-bold)}
+.fs-logo{width:var(--fs-btn-size);height:var(--fs-btn-size);border-radius:var(--fs-radius);border:var(--fs-hairline);display:flex;align-items:center;justify-content:center;flex:0 0 auto}
+.fs-logo svg{width:20px;height:20px}
+.fs-logo-ring{fill:#002b49}
+:root[data-darkmode="true"] .fs-logo-ring{fill:#e6f4fb}
+.fs-wordmark{font-size:var(--fs-type-lg);font-weight:var(--fs-weight-bold);letter-spacing:-.01em}
+.fs-skip{position:fixed;top:8px;inset-inline-start:8px;z-index:var(--fs-z-dropdown);padding:var(--fs-space-2-5) var(--fs-space-4);border-radius:var(--fs-radius);background:var(--fs-accent);color:var(--fs-on-accent);font-weight:var(--fs-weight-bold);font-size:var(--fs-type);text-decoration:none;box-shadow:var(--fs-shadow-pop)}
+.fs-skip:not(:focus){clip-path:inset(50%);width:1px;height:1px;padding:0;overflow:hidden;white-space:nowrap}
+.fs-skip:focus{outline:2px solid var(--fs-panel);outline-offset:2px}
+.fs-sr{position:absolute;clip-path:inset(50%);width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;white-space:nowrap}
+#maincontent:focus{outline:none}
+.fs-logout{display:flex;align-items:center;color:var(--fs-dim)}
+.fs-logout:hover{color:var(--fs-danger)}
+.fs-indicators{display:flex;align-items:center;gap:var(--fs-space-2)}
+.fs-sidebar ul.nav.fs-dense1>li>a{padding:var(--fs-space-1) var(--fs-space-2)}
+.fs-sidebar ul.nav.fs-dense2>li>a{padding:var(--fs-space-1) var(--fs-space-1-5);font-size:var(--fs-type)}
+ul.nav{position:relative;margin:0;list-style:none}
+ul.nav>li{display:block}
+ul.nav a{line-height:19px;text-decoration:none}
+.dropdown-menu li{display:block;position:relative}
+ul.nav .dropdown-menu a,.dropdown-menu a{line-height:18px}
+.fs-sidebar ul.nav>li>a:hover,.fs-sidebar ul.nav ul a:hover,.fs-themerow:hover{background:var(--fs-panel2);color:var(--fs-text);opacity:1}
+.fs-sidebar ul.nav>li>a.active,.fs-sidebar ul.nav ul a.active,.fs-themerow[aria-expanded="true"],.fs-modemenu li.active a{background:var(--fs-accent-soft);color:var(--fs-accent)}
+.fs-sidebar ul.nav>li.active>a:not(.active){color:var(--fs-accent)}
+.fs-railtoggle:focus-visible,.fs-themerow:focus-visible,.fs-logout:focus-visible,.fs-sidebar ul.nav a:focus-visible{outline:none;box-shadow:var(--fs-focus-ring-solo)}
+::selection{background:var(--fs-accent);color:var(--fs-on-accent)}
+:root{caret-color:var(--fs-accent);accent-color:var(--fs-accent)}
+.fs-pattern{display:none}
+:root[data-wallpaper="pattern"]{background-color:var(--fs-bg)}
+:root[data-wallpaper="pattern"] body,:root[data-wallpaper="pattern"] .fs-main{background-color:transparent}
+:root[data-wallpaper="pattern"] footer.fs-footer{background:transparent}
+:root[data-wallpaper="pattern"] .fs-pattern{display:block;position:fixed;inset:0;z-index:-1;pointer-events:none;opacity:var(--fs-pattern-strength)}
+:root[data-wallpaper="pattern"]:not([data-pattern-ink="original"]) .fs-pattern{background-color:var(--fs-text);-webkit-mask-image:var(--fs-pattern-url);mask-image:var(--fs-pattern-url);-webkit-mask-repeat:repeat;mask-repeat:repeat;-webkit-mask-size:var(--fs-pattern-size) auto;mask-size:var(--fs-pattern-size) auto}
+:root[data-wallpaper="pattern"][data-pattern-ink="original"] .fs-pattern{background-image:var(--fs-pattern-url);background-repeat:repeat;background-size:var(--fs-pattern-size) auto}
+:root[data-wallpaper="file"] :is(.fs-shell,body:not(:has(.fs-shell))){background-image:linear-gradient(var(--fs-photo-scrim),var(--fs-photo-scrim)),var(--fs-login-bg-url);background-position:center;background-size:cover;background-repeat:no-repeat;background-attachment:fixed}
+:root[data-wallpaper="file"] .fs-main{background-color:transparent}
+:root[data-wallpaper="file"] footer.fs-footer{background:transparent}
+body{padding:0;background-color:var(--fs-bg);font-family:var(--fs-font-sans)}
+.fs-brand{flex:1 1 auto;min-width:0}
+.fs-wordmark{overflow-wrap:anywhere;min-width:0;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden}
+.fs-railtoggle{align-items:center;justify-content:center;width:calc(28px * var(--fs-density-box));height:calc(28px * var(--fs-density-box));padding:0;flex:0 0 auto;border:1px solid transparent;border-radius:var(--fs-radius-sm);background:transparent;color:var(--fs-faint);cursor:pointer;transition:background var(--fs-dur),border-color var(--fs-dur),color var(--fs-dur)}
+.fs-railtoggle:hover{color:var(--fs-text);background:var(--fs-panel2);border-color:var(--fs-border)}
+.fs-railtoggle .fs-ico{width:16px;height:16px;transition:transform var(--fs-dur-move)}
+.fs-sidebar ul.nav,.fs-sidebar #modemenu{list-style:none;margin:0;padding:0;display:flex}
+.fs-sidebar ul.nav>li>a .fs-label{flex:1 1 auto}
+.fs-chevron{width:14px;height:14px;flex:0 0 auto;color:var(--fs-dim);transition:rotate var(--fs-dur-move)}
+.fs-sidebar ul.nav>li.open>a .fs-chevron{rotate:90deg}
+:root[dir="rtl"] .fs-chevron,:root[dir="rtl"] .fs-railtoggle .fs-ico{scale:-1 1}
+.fs-sidebar ul.nav>li.has-sub>a{cursor:pointer}
+.fs-sidebar ul.nav ul{list-style:none}
+.fs-shell{display:flex;flex-direction:column;min-height:100svh;width:100%}
+.fs-sidebar{display:flex;width:100%;height:auto;position:sticky;top:0;z-index:var(--fs-z-sticky);flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--fs-space-2-5);padding:var(--fs-space-2-5) var(--fs-space-4);background:var(--fs-bar-bg);border-inline-end:0;border-bottom:var(--fs-hairline);overflow:visible}
+.fs-brandrow{flex:0 1 auto;order:1;min-width:0;max-width:min(100%,30ch);padding:0}
+.fs-navlabel,.fs-spacer,.fs-railtoggle,.fs-modemenu{display:none}
+.fs-sidebar ul.nav{order:3;flex:1 1 100%;flex-direction:row;flex-wrap:wrap;align-items:center;gap:var(--fs-space-0-5)}
+.fs-sidebar ul.nav>li.has-sub{position:relative}
+.fs-sidebar ul.nav>li>a{display:flex;align-items:center;padding:var(--fs-space-1-5) var(--fs-space-3);border-radius:var(--fs-radius);color:var(--fs-dim);font-size:var(--fs-type);font-weight:var(--fs-weight);white-space:nowrap}
+.fs-sidebar ul.nav>li>a .fs-ico,.fs-sidebar ul.nav>li>a .fs-chevron{display:none}
+.fs-sidebar ul.nav ul,.fs-sidebar ul.nav>li.open>ul{display:none;position:absolute;inset-inline:0 auto;top:calc(100% + 8px);z-index:var(--fs-z-flyout);width:max-content;min-width:200px;max-width:calc(100vw - 32px);margin:0;padding:var(--fs-space-2);background:var(--fs-panel);border:var(--fs-hairline);border-radius:var(--fs-radius-lg);box-shadow:var(--fs-shadow-pop)}
+.fs-sidebar ul.nav>li.has-sub>ul::before{content:"";position:absolute;inset-inline:0;top:-10px;height:10px}
+@media (hover:hover){.fs-sidebar ul.nav>li.has-sub:hover>ul{display:block}
+}
+.fs-sidebar ul.nav>li.has-sub.open>ul{display:block}
+.fs-sidebar ul.nav ul a{display:block;padding:var(--fs-space-2) var(--fs-space-3);border-radius:var(--fs-radius-sm);color:var(--fs-dim);font-size:var(--fs-type);font-weight:var(--fs-weight)}
+.fs-sidebar #indicators{display:flex;flex-wrap:wrap;gap:var(--fs-space-1-5);order:2;margin-inline-start:auto;padding:0}
+.fs-themerow{display:flex;align-items:center;justify-content:center;flex:0 0 auto;order:2;margin-inline-start:auto;width:var(--fs-btn-size);height:var(--fs-btn-size);padding:0;border:var(--fs-hairline);border-radius:var(--fs-radius);background:var(--fs-bar-ctl-bg);color:var(--fs-dim);font:inherit;font-weight:var(--fs-weight);font-size:var(--fs-type);line-height:1.1;text-align:start;cursor:pointer;transition:background var(--fs-dur),border-color var(--fs-dur),color var(--fs-dur)}
+.fs-sidebar.fs-bar-actrow .fs-brandrow{flex:1 1 100%;max-width:100%}
+.fs-sidebar.fs-ind-compact #indicators [data-indicator="poll-status"]{width:var(--fs-btn-size);padding:0;font-size:0;justify-content:center;position:relative}
+.fs-sidebar.fs-ind-compact #indicators [data-indicator="poll-status"]::before{content:"";width:var(--fs-icon-refresh-sz);height:var(--fs-icon-refresh-sz);background:var(--fs-good);-webkit-mask:var(--fs-icon-refresh) center / var(--fs-icon-refresh-sz) no-repeat;mask:var(--fs-icon-refresh) center / var(--fs-icon-refresh-sz) no-repeat}
+.fs-sidebar.fs-ind-compact #indicators [data-indicator="poll-status"]:not([data-style="active"])::before{background:var(--fs-dim)}
+.fs-sidebar.fs-ind-compact #indicators [data-indicator]:not([data-indicator="poll-status"]){min-width:var(--fs-btn-size);padding:0 var(--fs-space-1);font-size:0;justify-content:center}
+.fs-sidebar.fs-ind-compact #indicators [data-indicator]:not([data-indicator="poll-status"])::after{content:attr(data-fs-badge);font-size:var(--fs-type-xs);font-weight:var(--fs-weight-bold);font-variant-numeric:tabular-nums}
+.fs-sidebar #indicators:not(:empty) ~ .fs-themerow{margin-inline-start:0}
+.fs-themerow ~ .fs-themerow{margin-inline-start:0}
+.fs-logout{flex:0 0 auto;order:2;justify-content:center;width:var(--fs-btn-size);height:var(--fs-btn-size);padding:0;border:var(--fs-hairline);border-radius:var(--fs-radius);background:var(--fs-bar-ctl-bg)}
+.fs-themerow:hover,.fs-themerow[aria-expanded="true"]{border-color:var(--fs-accent)}
+.fs-logout:hover{border-color:var(--fs-danger)}
+.fs-themerow .fs-label,.fs-logout .fs-label{display:none}
+.fs-themerow .fs-ico,.fs-logout .fs-ico{width:18px;height:18px;flex:0 0 auto}
+.fs-sidebar ul.nav .fs-ico{flex:0 0 auto}
+@media (min-width:521px){:root[data-layout="sidebar"]:not([data-narrow]) .fs-shell{flex-direction:row;height:100svh;overflow:hidden}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-main{height:100svh;overflow-y:auto;contain:paint}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar{width:var(--fs-sidebar-w);height:100svh;flex:0 0 auto;position:static;flex-direction:column;flex-wrap:nowrap;align-items:normal;gap:normal;padding:var(--fs-space-5) var(--fs-space-3-5);background:var(--fs-panel);border-inline-end:var(--fs-hairline);border-bottom:0;box-shadow:none;overflow:auto;z-index:auto}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-brandrow{display:flex;align-items:center;gap:var(--fs-space-1-5);flex:0 0 auto;order:0;min-width:auto;max-width:none;padding:0 var(--fs-space-1) var(--fs-space-5) var(--fs-space-2)}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-navlabel{display:block;color:var(--fs-dim);font-size:var(--fs-type-xs);font-weight:var(--fs-weight-bold);letter-spacing:.1em;text-transform:uppercase;padding:var(--fs-space-1-5) var(--fs-space-2-5) var(--fs-space-2)}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-spacer{display:block;flex:1 1 auto;min-height:12px}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-railtoggle{display:inline-flex}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-modemenu:not(:empty):not(.single){display:flex;margin-top:var(--fs-space-1)}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav,:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar #modemenu{order:0;flex:0 1 auto;flex-direction:column;flex-wrap:nowrap;align-items:normal;gap:var(--fs-space-0-5)}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav>li,:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav>li.has-sub{position:relative}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav>li>a{display:flex;align-items:center;gap:var(--fs-space-3);padding:var(--fs-space-2-5) var(--fs-space-3);font-size:var(--fs-type);font-weight:var(--fs-weight-bold);white-space:normal;line-height:1.1}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav>li>a .fs-ico{display:block}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav>li>a .fs-chevron{display:block}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav .fs-ico,:root[data-layout="sidebar"]:not([data-narrow]) .fs-themerow .fs-ico,:root[data-layout="sidebar"]:not([data-narrow]) .fs-logout .fs-ico{width:17px;height:17px}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav ul{display:none;position:static;top:auto;inset-inline:auto;z-index:auto;width:auto;min-width:0;max-width:none;flex-direction:column;gap:var(--fs-space-0-5);margin:0 0 0 var(--fs-space-7);padding:0;background:none;border:0;border-radius:0;box-shadow:none;overflow:hidden}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav>li.has-sub>ul::before{content:none}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav>li.open>ul{display:flex;margin:var(--fs-space-1) 0 var(--fs-space-3) var(--fs-space-7);padding-inline-start:var(--fs-space-1);border-inline-start:var(--fs-hairline)}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav>li.has-sub.open>ul{display:flex}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar ul.nav ul a{padding:var(--fs-space-2) var(--fs-space-3)}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-themerow,:root[data-layout="sidebar"]:not([data-narrow]) .fs-logout{order:0;margin-inline-start:0;width:100%;height:auto;justify-content:normal;gap:var(--fs-space-3);padding:var(--fs-space-2-5) var(--fs-space-3);border:0;border-radius:var(--fs-radius);background:transparent}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-themerow{text-align:start}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-logout{font-weight:var(--fs-weight);font-size:var(--fs-type)}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-logout:hover{background:var(--fs-panel2);border-color:transparent}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-themerow:hover,:root[data-layout="sidebar"]:not([data-narrow]) .fs-themerow[aria-expanded="true"]{border-color:transparent}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-themerow .fs-label,:root[data-layout="sidebar"]:not([data-narrow]) .fs-logout .fs-label{display:block;flex:1 1 auto}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar #indicators{order:0;margin-inline-start:0;flex-wrap:wrap;gap:var(--fs-space-1-5);padding:0 var(--fs-space-1-5) var(--fs-space-1)}
+:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar #indicators .label,:root[data-layout="sidebar"]:not([data-narrow]) .fs-sidebar #indicators [data-indicator]{flex:1 1 100%;justify-content:center}
+}
+@media (min-width:521px){:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar{width:var(--fs-rail-w);padding:var(--fs-space-5) var(--fs-space-2-5);overflow:visible}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-brandrow{flex-direction:column;gap:var(--fs-space-2-5);padding:0 0 var(--fs-space-5)}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-brand{flex:0 0 auto;justify-content:center}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-railtoggle .fs-ico{rotate:180deg}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-wordmark,:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-navlabel,:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-modemenu,:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav>li>a>.fs-label,:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-themerow>.fs-label,:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-logout>.fs-label,:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav>li>a .fs-chevron{display:none}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav>li>a,:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-themerow,:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-logout{justify-content:center;gap:0;padding:var(--fs-space-2-5) 0}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav>li:not(.has-sub)>a::after{content:attr(data-label);position:absolute;inset-inline-start:100%;top:50%;transform:translateY(-50%);margin-inline-start:var(--fs-space-2-5);z-index:var(--fs-z-flyout);padding:var(--fs-space-1-5) var(--fs-space-2-5);border-radius:var(--fs-radius-sm);background:var(--fs-panel);border:var(--fs-hairline);box-shadow:var(--fs-shadow-pop);color:var(--fs-text);font-size:var(--fs-type);font-weight:var(--fs-weight);white-space:nowrap;opacity:0;pointer-events:none;transition:opacity var(--fs-dur)}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav>li:not(.has-sub)>a:hover::after{opacity:1}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav>li.has-sub>ul{display:none;position:absolute;inset-inline-start:100%;top:-8px;margin:0;margin-inline-start:var(--fs-space-2-5);min-width:196px;padding:var(--fs-space-2);z-index:var(--fs-z-flyout);background:var(--fs-panel);border:var(--fs-hairline);border-radius:var(--fs-radius-lg);box-shadow:var(--fs-shadow-pop);animation:fs-fade var(--fs-dur)}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav>li.has-sub.open>ul{display:flex}
+@media (hover:hover){:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav>li.has-sub:hover>ul{display:flex}
+}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav>li>ul[data-title]::before{content:attr(data-title);position:static;inset:auto;height:auto;padding:var(--fs-space-1) var(--fs-space-3) var(--fs-space-2);color:var(--fs-eyebrow-color);font-size:var(--fs-type-xs);font-weight:var(--fs-eyebrow-weight);letter-spacing:var(--fs-eyebrow-tracking);text-transform:uppercase}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar ul.nav>li.has-sub::after{content:"";position:absolute;inset-inline-start:100%;top:0;width:12px;height:100%}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar #indicators [data-indicator="poll-status"]{font-size:0;width:var(--fs-ctl-h-sm);height:var(--fs-ctl-h-sm);padding:0;border:0;box-shadow:none;background:var(--fs-good);-webkit-mask:var(--fs-icon-refresh) center / var(--fs-icon-refresh-sz) no-repeat;mask:var(--fs-icon-refresh) center / var(--fs-icon-refresh-sz) no-repeat}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] .fs-sidebar #indicators [data-indicator="poll-status"]:not([data-style="active"]){background:var(--fs-dim)}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] #indicators [data-indicator]:not([data-indicator="poll-status"]){font-size:0;min-width:var(--fs-ctl-h-sm);max-width:100%;height:var(--fs-ctl-h-sm);box-sizing:border-box;padding:0 var(--fs-space-1);overflow:hidden;display:inline-flex;align-items:center;justify-content:center}
+:root[data-layout="sidebar"]:not([data-narrow])[data-rail="true"] #indicators [data-indicator]:not([data-indicator="poll-status"])::after{content:attr(data-fs-badge);font-size:var(--fs-type-xs);font-weight:var(--fs-weight-bold);font-variant-numeric:tabular-nums}
+}
+.fs-main{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;background:var(--fs-bg);overflow-x:clip}
+.fs-sidebar #indicators:empty{display:none}
+.fs-indicators{flex-wrap:wrap}
+.fs-content{padding:var(--fs-space-5) var(--fs-content-pad);flex:1 1 auto;width:100%;max-width:var(--fs-content-max);margin:0 auto}
+.fs-staging{visibility:hidden;height:0;overflow:clip}
+#fs-nav-progress{position:fixed;inset-inline:0;top:0;height:2px;background:var(--fs-accent);transform:scaleX(0);transform-origin:left center;opacity:0;pointer-events:none;z-index:var(--fs-z-tooltip)}
+:root[dir="rtl"] #fs-nav-progress{transform-origin:right center}
+#fs-nav-progress[data-state="active"]{opacity:1;transform:scaleX(.8);transition:transform 2s cubic-bezier(.1,.7,.1,1),opacity 120ms linear}
+#fs-nav-progress[data-state="done"]{opacity:0;transform:scaleX(1);transition:transform 120ms ease-out,opacity 300ms linear 100ms}
+@media (prefers-reduced-motion:reduce){#fs-nav-progress[data-state="active"],#fs-nav-progress[data-state="done"]{transition:none;transform:scaleX(1)}
+}
+footer{padding:var(--fs-space-3-5) var(--fs-content-pad);border-top:var(--fs-hairline);color:var(--fs-dim);font-size:var(--fs-type);background:var(--fs-bg);justify-content:center;text-align:center;line-height:max(26px,calc(var(--fs-type) * 2))}
+footer a{color:var(--fs-dim)}
+footer a:hover{color:var(--fs-accent)}
+@media (max-width:767px){.fs-content{padding:var(--fs-space-4)}
+}
+.fs-main>.alert-message{order:1;margin:var(--fs-space-4) auto 0;width:calc(100% - var(--fs-content-pad) * 2);max-width:calc(var(--fs-content-max) - var(--fs-content-pad) * 2)}
+.fs-main>.fs-content{order:2}
+.fs-main>.fs-footer{order:3}
+@media (max-width:767px){.fs-main>.alert-message{margin:var(--fs-space-3) var(--fs-space-4) 0;width:auto}
+}
+@media (max-width:767px){.fs-main .alert-message{flex-wrap:wrap}
+.fs-main .alert-message>*{min-width:0;margin-inline-start:0}
+.fs-main .alert-message div{flex-wrap:wrap;min-width:0}
+.fs-main .alert-message .btn,.fs-main .alert-message button{white-space:normal;max-width:100%}
+}
+.fs-indicators .label,.fs-indicators [data-indicator],#indicators .label,#indicators [data-indicator]{display:inline-flex;align-items:center;min-height:var(--fs-btn-size);text-transform:none;font-weight:var(--fs-weight);font-size:var(--fs-type);background:var(--fs-bar-ctl-bg);background-image:none;border:var(--fs-hairline);color:var(--fs-dim);border-radius:var(--fs-radius);padding:0 var(--fs-space-3);box-shadow:none}
+.fs-indicators [data-indicator][data-style="active"],#indicators [data-indicator][data-style="active"]{color:var(--fs-good)}
+.fs-indicators [data-indicator][data-clickable],#indicators [data-indicator][data-clickable]{cursor:pointer}
+.fs-indicators [data-indicator][data-clickable]:focus-visible,#indicators [data-indicator][data-clickable]:focus-visible{outline:none;border-color:var(--fs-accent);box-shadow:var(--fs-focus-ring)}
+.cbi-progressbar{position:relative;height:10px;background:var(--fs-track);border:0;border-radius:var(--fs-radius-pill);overflow:visible;min-width:120px;margin:0}
+.cbi-progressbar>div{height:100%;background:var(--fs-accent);border-radius:var(--fs-radius-pill);transition:width var(--fs-dur-fill)}
+.cbi-progressbar[data-fs-level="warn"]>div{background:var(--fs-warn)}
+.cbi-progressbar[data-fs-level="danger"]>div{background:var(--fs-danger)}
+.cbi-progressbar::after{content:attr(title);position:absolute;inset-inline:auto 0;bottom:calc(100% + var(--fs-space-1));top:auto;font-family:var(--fs-font-mono);font-size:var(--fs-type-xs);font-weight:var(--fs-weight-normal);color:var(--fs-dim);white-space:nowrap;text-shadow:none}
+.cbi-value-field>.cbi-progressbar{margin-inline-end:var(--fs-meter-value);--fs-meter-value:calc(var(--fs-space-10) * 2)}
+.cbi-value-field>.cbi-progressbar::after{inset-inline:calc(100% + var(--fs-space-2)) auto;bottom:auto;top:50%;transform:translateY(-50%)}
+.cbi-section .table:has(.tr.table-titles) .td .cbi-progressbar{margin-inline-end:var(--fs-meter-value);--fs-meter-value:var(--fs-space-10);min-width:60px}
+.cbi-section .table:has(.tr.table-titles) .td .cbi-progressbar::after{inset-inline:calc(100% + var(--fs-space-2)) auto;bottom:auto;top:50%;transform:translateY(-50%)}
+.cbi-section .table.fs-stacked:has(.tr.table-titles) .td .cbi-progressbar{margin-inline-end:0;margin-block-start:calc(var(--fs-type-xs) * var(--fs-leading))}
+.cbi-section .table.fs-stacked:has(.tr.table-titles) .td .cbi-progressbar::after{inset-inline:auto 0;bottom:calc(100% + var(--fs-space-1));top:auto;transform:none}
+.cbi-section .table:not([id]):not(.cbi-section-table) .tr:has(.cbi-progressbar) .td{vertical-align:middle}
+.cbi-section,.cbi-map>.cbi-section-node,fieldset.cbi-section,.panel{background:var(--fs-panel);border:var(--fs-hairline);border-radius:var(--fs-radius-lg)}
+.cbi-section{padding:var(--fs-card-pad);margin-bottom:var(--fs-card-gap)}
+legend{font-size:var(--fs-type-lg);font-weight:var(--fs-weight-bold);color:var(--fs-text)}
+.cbi-section>.cbi-title{margin-bottom:var(--fs-space-3)}
+.cbi-section>.cbi-title h3,.cbi-section>h3:first-child,.cbi-section>legend{font-size:var(--fs-type-lg);font-weight:var(--fs-weight-bold);color:var(--fs-text);margin:0;padding:0;border:0}
+.cbi-section>h3:first-child,.cbi-section>legend{margin-bottom:var(--fs-space-3)}
+.cbi-section .table + h3{margin-top:var(--fs-space-5)}
+.cbi-section h3 + .table{margin-top:var(--fs-space-3)}
+.cbi-section .table:not([id]):not(.fs-dt),.cbi-section-node .table:not([id]):not(.fs-dt),.cbi-modal .table,.table .table,.cbi-value .table{border:0;border-radius:0;overflow:visible;background:none}
+.cbi-section .table{margin:0;width:100%}
+.table.cbi-section-table input[type="password"],.table.cbi-section-table input[type="text"],.table.cbi-section-table textarea,.table.cbi-section-table select,.table.cbi-section-table .cbi-select{width:100%;min-width:auto}
+.table.cbi-section-table .td.cbi-section-table-cell select,.table.cbi-section-table .td.cbi-section-table-cell .cbi-select{width:inherit}
+.table.cbi-section-table .cbi-section-filter{padding:var(--fs-space-1) var(--fs-space-1-5);font-size:var(--fs-type-xs);text-overflow:ellipsis}
+.cbi-section .table .td,.cbi-section .table .th{padding:var(--fs-space-2-5) var(--fs-space-4) var(--fs-space-2-5) 0;border:0;border-bottom:var(--fs-hairline);line-height:1.45;vertical-align:middle;text-align:start}
+.cbi-section .table .td.cbi-section-table-cell.cbi-section-actions{text-align:end}
+.cbi-section .table .tr:last-child .td{border-bottom:0}
+.cbi-section .table .th{color:var(--fs-eyebrow-color);font-weight:var(--fs-eyebrow-weight);font-size:var(--fs-type-xs);letter-spacing:var(--fs-eyebrow-tracking)}
+.cbi-section .table:is([id],.fs-dt) .td,.cbi-section .table:is([id],.fs-dt) .th,table.fs-dt .td,table.fs-dt .th{padding:var(--fs-space-2-5) var(--fs-space-3)}
+.cbi-section .table:is([id],.fs-dt) .td{font-family:var(--fs-font-mono);font-size:var(--fs-type);color:var(--fs-text)}
+.table.fs-dt:not(.fs-stacked) .td{overflow-wrap:break-word}
+.table.fs-dt:not(.fs-stacked) .td.fs-td-break{overflow-wrap:anywhere}
+.table.fs-dt .td,.table.fs-dt .td :not(button):not(.btn):not(input):not(select):not(textarea){word-break:normal !important}
+:where(:root[data-layout="sidebar"]:not([data-narrow]) #view,#modal_overlay) .table.fs-dt:not(.fs-stacked) .tr.table-titles .th{position:sticky;top:0;z-index:1;background:var(--fs-panel2)}
+:where(:root[data-layout="top"] #view,:root[data-narrow] #view) .table.fs-dt:not(.fs-stacked) .tr.table-titles .th{position:sticky;top:var(--fs-bar-live,var(--fs-bar-h));z-index:1;background:var(--fs-panel2)}
+.table.fs-dt{overflow-anchor:none}
+.cbi-section .table:is([id],.fs-dt) .tr:not(.table-titles):not(.placeholder):hover .td{background:var(--fs-panel2)}
+.cbi-section .table:is([id],.fs-dt) .tr.table-titles .th{border-bottom-color:var(--fs-border)}
+.cbi-section .table[id],#view .table[id],.cbi-section .table.fs-dt,.cbi-section table.fs-dt,table.fs-dt{border-collapse:separate;border-spacing:0;border:var(--fs-hairline);border-radius:var(--fs-radius-lg);overflow:visible}
+.cbi-section .table[id] .tr.table-titles .th:first-child,#view .table[id] .tr.table-titles .th:first-child,table.fs-dt .tr.table-titles .th:first-child{border-top-left-radius:var(--fs-radius-lg)}
+.cbi-section .table[id] .tr.table-titles .th:last-child,#view .table[id] .tr.table-titles .th:last-child,table.fs-dt .tr.table-titles .th:last-child{border-top-right-radius:var(--fs-radius-lg)}
+.cbi-section .table[id] .tr:last-child .td:first-child,#view .table[id] .tr:last-child .td:first-child,table.fs-dt .tr:last-child .td:first-child{border-bottom-left-radius:var(--fs-radius-lg)}
+.cbi-section .table[id] .tr:last-child .td:last-child,#view .table[id] .tr:last-child .td:last-child,table.fs-dt .tr:last-child .td:last-child{border-bottom-right-radius:var(--fs-radius-lg)}
+.cbi-section .table[id] .tr.placeholder .td,#view .table[id] .tr.placeholder .td,table.fs-dt .tr.placeholder .td{padding-block:var(--fs-space-2);text-align:start;color:var(--fs-dim);font-family:var(--fs-font-sans);border-radius:0 0 var(--fs-radius-lg) var(--fs-radius-lg)}
+.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:first-child{color:var(--fs-dim);font-weight:var(--fs-weight);width:40%;white-space:nowrap}
+.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:last-child:not(:first-child){font-family:var(--fs-font-mono);color:var(--fs-text);text-align:end}
+@container fs-content (max-width:560px){.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .tr{display:block;padding:var(--fs-space-1) 0 var(--fs-space-1-5);border:0;border-bottom:var(--fs-hairline)}
+.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .tr:last-child{border-bottom:0}
+.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:first-child:first-child{display:block;width:auto;white-space:normal;padding:0;border:0;font-size:var(--fs-type-xs);font-weight:var(--fs-eyebrow-weight);letter-spacing:var(--fs-eyebrow-tracking);color:var(--fs-eyebrow-color);text-transform:uppercase}
+.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:last-child:last-child:not(:first-child){display:block;width:auto;text-align:start;padding:0;border:0}
+}
+@media (max-width:767px){.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt)[style*="table-layout"] .tr{display:flex;flex-wrap:wrap}
+.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt)[style*="table-layout"] .td{flex:1 1 50%;min-width:0;width:auto}
+}
+.cbi-section .cbi-section{border:0;box-shadow:none;padding:0;margin:0;background:transparent}
+.cbi-section .ifacebox,.cbi-section .zonebadge{border-radius:var(--fs-radius)}
+.zonebadge input{width:6em}
+.network-status-table .ifacebox-body .ifacebadge{border-radius:var(--fs-radius)}
+.cbi-section .ifacebox>.ifacebox-head{--fs-head-radius:max(0px,calc(var(--fs-radius) - 1px));border-radius:var(--fs-head-radius) var(--fs-head-radius) 0 0}
+[data-tab-title]:not([data-tab-active="true"]){visibility:hidden;position:relative;height:0;overflow:hidden;padding:0;margin:0;border:0}
+.cbi-section-error{border-color:var(--fs-danger);background:color-mix(in srgb,var(--fs-danger) 6%,var(--fs-panel));color:var(--fs-text)}
+.cbi-section-error ul li{color:var(--fs-danger)}
+.cbi-section-table .th,.cbi-section-table .td,.table.cbi-section-table .th,.table.cbi-section-table .td,.table.cbi-section-table .tr::before{padding:var(--fs-space-2) var(--fs-space-3) var(--fs-space-2) 0}
+.table.cbi-section-table:not(.cbi-section *):not(.cbi-section-node *) .th,.table.cbi-section-table:not(.cbi-section *):not(.cbi-section-node *) .td{padding:var(--fs-space-2-5) var(--fs-space-3)}
+.cbi-section-table .tr.cbi-section-table-titles .th,.cbi-section-table .tr.table-titles .th{font-size:var(--fs-type-xs);letter-spacing:var(--fs-eyebrow-tracking);line-height:1.25}
+.table{border:var(--fs-hairline);border-radius:var(--fs-radius-lg);overflow:hidden;background:var(--fs-panel)}
+.table .tr.table-titles>.th,.table .tr.cbi-section-table-titles>.th,.table .tr.cbi-section-table-titles::before{border-bottom:var(--fs-hairline)}
+.table .tr:not(.table-titles):not(.cbi-section-table-titles):not(:last-child)>.td,.table .tr.cbi-section-table-row:not(:last-child)::before{border-bottom:var(--fs-hairline)}
+.table .tr.table-titles,.table .tr.cbi-section-table-titles{background:transparent}
+.table .tr.table-titles .th,.table .tr.cbi-section-table-titles .th,.table .tr.cbi-section-table-titles::before{color:var(--fs-eyebrow-color);font-size:var(--fs-type-xs);font-weight:var(--fs-eyebrow-weight);letter-spacing:var(--fs-eyebrow-tracking);background:transparent}
+.table.cbi-section-table .td.cbi-section-table-titles,.table.cbi-section-table .tr.cbi-section-table-row::before{font-family:var(--fs-font-sans);font-weight:var(--fs-weight-bold);color:var(--fs-text)}
+.table .tr:not(.table-titles):not(.cbi-section-table-titles){transition:background var(--fs-dur)}
+.table.cbi-section-table .tr.cbi-section-table-row:not(.placeholder):hover{background:var(--fs-panel2)}
+.cbi-section .table>.tr,.cbi-modal .table>.tr{border-radius:0}
+:root[data-fs-fit] #view .table.fs-dt:not(.fs-fitted),:root[data-fs-fit] #modal_overlay .table.fs-dt:not(.fs-fitted){display:none}
+.table.fs-stacked{display:block;width:100%}
+.table.fs-stacked .td.hide-sm,.table.fs-stacked .th.hide-sm,.table.fs-stacked .td.hide-xs,.table.fs-stacked .th.hide-xs,.table.fs-drop-xs .td.hide-sm,.table.fs-drop-xs .th.hide-sm,.table.fs-drop-xs .td.hide-xs,.table.fs-drop-xs .th.hide-xs{display:none}
+.table.fs-stacked .tr.table-titles,.table.fs-stacked .thead,.table.fs-stacked .tr.cbi-section-table-titles,.table.fs-stacked .tr.cbi-section-table-descr{display:none}
+.table.fs-stacked .tr{display:flex;flex-wrap:wrap}
+.table.fs-stacked tfoot:not(:has(>.tr)),.table.fs-stacked .tfoot:not(:has(>.tr)){display:flex;flex-wrap:wrap}
+.table.fs-stacked .td{display:block;min-width:0;width:auto;padding-block:var(--fs-space-1-5);padding-inline:0 var(--fs-space-2-5);border:0;text-align:start;overflow-wrap:anywhere;white-space:normal}
+.table.fs-stacked .td[data-title]::before{content:attr(data-title);display:block;margin-bottom:var(--fs-space-1);font-size:var(--fs-type-xs);font-weight:var(--fs-eyebrow-weight);letter-spacing:var(--fs-eyebrow-tracking);color:var(--fs-eyebrow-color)}
+.table.fs-stacked .td.cbi-section-actions::before,.table.fs-stacked .td:not([data-title])::before,.table.fs-stacked .td[data-title=""]::before,.table.fs-stacked tfoot .td[data-title]::before,.table.fs-stacked .tfoot .td[data-title]::before{content:none}
+.table.fs-stacked .td.cbi-section-actions{display:flex;flex:1 1 100%;justify-content:flex-start;flex-wrap:wrap;gap:var(--fs-space-2);padding-top:var(--fs-space-2)}
+.table.fs-stacked .td.cbi-section-actions>*{flex:1 1 auto;display:flex;flex-wrap:wrap;gap:var(--fs-space-2)}
+.table.fs-stacked .td>*,.table.fs-stacked .td .ifacebadge{min-width:0;max-width:100%}
+.table.fs-stacked tfoot:has(>.tr),.table.fs-stacked .tfoot:has(>.tr){display:block}
+.table.fs-stacked tfoot .td:empty,.table.fs-stacked .tfoot .td:empty{display:none}
+#view .table.fs-dt.fs-stacked,#modal_overlay .table.fs-dt.fs-stacked{overflow:hidden}
+.table.fs-dt.fs-stacked .tr:not(.placeholder){padding:var(--fs-space-2-5) var(--fs-space-4) var(--fs-space-3)}
+.table.fs-stacked .td:not(.cbi-section-actions):not([class*="col-"]):not(:has(.cbi-progressbar)){flex:1 1 50% !important}
+.table.fs-stacked .td:has(.cbi-progressbar){flex:1 1 100% !important}
+.table .td.col-1,.table .th.col-1{flex:1 1 30px}
+.table .td.col-2,.table .th.col-2{flex:2 2 60px}
+.table .td.col-3,.table .th.col-3{flex:3 3 90px}
+.table .td.col-4,.table .th.col-4{flex:4 4 120px}
+.table .td.col-5,.table .th.col-5{flex:5 5 150px}
+.table .td.col-6,.table .th.col-6{flex:6 6 180px}
+.table .td.col-7,.table .th.col-7{flex:7 7 210px}
+.table .td.col-8,.table .th.col-8{flex:8 8 240px}
+.table .td.col-9,.table .th.col-9{flex:9 9 270px}
+.table .td.col-10,.table .th.col-10{flex:10 10 300px}
+.table .td{padding-top:var(--fs-space-2);padding-bottom:var(--fs-space-2)}
+#view table:not(.table):not(.cbi-section-table):not(.fs-dt),#modal_overlay table:not(.table):not(.cbi-section-table):not(.fs-dt),#view .table.fs-xscroll,#modal_overlay .table.fs-xscroll{display:block;max-width:100%;overflow-x:auto;overscroll-behavior-x:contain;scrollbar-width:thin}
+.table.fs-rowstack,.table.fs-rowstack .tr,.table.fs-rowstack .td,.table.fs-rowstack .th{display:block;width:auto}
+.table.fs-rowstack .td{padding-inline:0;padding-block:var(--fs-space-1)}
+#view .table.fs-xscroll .tr>:first-child,#modal_overlay .table.fs-xscroll .tr>:first-child,#view .table.fs-xscroll tr>:first-child,#modal_overlay .table.fs-xscroll tr>:first-child{position:sticky;left:0;z-index:2;background:var(--fs-panel);border-inline-end:var(--fs-hairline)}
+#view .table.fs-xscroll:focus-visible,#modal_overlay .table.fs-xscroll:focus-visible{outline:2px solid var(--fs-accent);outline-offset:2px}
+.alert-message{padding:var(--fs-space-3-5) var(--fs-space-4);border-radius:var(--fs-radius-lg);border:var(--fs-hairline);border-inline-start:3px solid var(--fs-accent);background-color:var(--fs-panel);background-image:linear-gradient(var(--fs-alert-tint,transparent),var(--fs-alert-tint,transparent));color:var(--fs-text);gap:var(--fs-space-3)}
+.alert-message.warning:not(.btn){border-color:var(--fs-border);border-inline-start-color:var(--fs-warn);--fs-alert-tint:var(--fs-warn-fill)}
+.alert-message.error:not(.btn),.alert-message.danger:not(.btn){border-color:var(--fs-border);border-inline-start-color:var(--fs-danger);--fs-alert-tint:var(--fs-danger-fill)}
+.alert-message.success:not(.btn){border-inline-start-color:var(--fs-good);--fs-alert-tint:var(--fs-good-fill)}
+.alert-message.info:not(.btn),.alert-message.notice:not(.btn){border-inline-start-color:var(--fs-accent);--fs-alert-tint:var(--fs-accent-fill)}
+.alert-message.spinning{display:grid;grid-template-columns:auto 1fr;align-items:center;column-gap:var(--fs-space-2)}
+.alert-message.spinning::before{grid-row:1;margin-inline-end:0}
+.alert-message.spinning>*{grid-column:2}
+.alert-message :is(h2,h3,h4,h5,h6){margin:0 0 var(--fs-space-1);font-size:var(--fs-type-lg);font-weight:var(--fs-weight-bold);color:var(--fs-text)}
+.alert-message h4,.alert-message h5,.alert-message pre,.alert-message ul,.alert-message li,.alert-message p{border:none;line-height:inherit;background:transparent;padding:0}
+.alert-message p{margin:var(--fs-space-1) 0;color:var(--fs-text);font-size:var(--fs-type)}
+.alert-message :is(ul,ol,li,pre){color:var(--fs-text);margin:.25em 0}
+.alert-message .table{margin:var(--fs-space-2-5) 0;background:transparent;border:0;box-shadow:none;border-radius:0}
+.alert-message .table .th,.alert-message .table .tr.table-titles .th,.alert-message .table .tr.cbi-section-table-titles .th{color:var(--fs-text);font-weight:var(--fs-eyebrow-weight);font-size:var(--fs-type-xs);letter-spacing:var(--fs-eyebrow-tracking)}
+.alert-message .table .th,.alert-message .table .td{padding:var(--fs-space-2) var(--fs-space-3) var(--fs-space-2) 0;background:transparent;border-color:color-mix(in srgb,var(--fs-border) 60%,transparent)}
+.alert-message .table .td{color:var(--fs-text)}
+.cbi-tooltip{box-shadow:var(--fs-shadow-pop)}
+.cbi-tooltip-container:focus-within .cbi-tooltip:not(:empty){inset-inline-start:auto;opacity:1}
+.cbi-tooltip.info{background:var(--fs-accent);border-color:var(--fs-accent);color:var(--fs-on-accent)}
+.cbi-tooltip.success{background:var(--fs-good);border-color:var(--fs-good);color:var(--fs-on-good)}
+.cbi-tooltip.error{background:var(--fs-danger);border-color:var(--fs-danger);color:var(--fs-on-danger)}
+.cbi-tooltip.notice{background:var(--fs-panel2);border-color:var(--fs-border);color:var(--fs-text)}
+.label{display:inline-block;padding:var(--fs-space-1) var(--fs-space-2);border-radius:var(--fs-radius-pill);font-size:var(--fs-type-xs);font-weight:var(--fs-weight);line-height:1.4;background:var(--fs-panel2);border:var(--fs-hairline);color:var(--fs-dim);text-shadow:none;text-transform:none;white-space:nowrap;margin:.125em 0 .125em .4em}
+.label.important,.label.notice{border-color:var(--fs-accent);color:var(--fs-accent)}
+.label.warning{border-color:var(--fs-warn);color:var(--fs-warn)}
+.label.success{border-color:var(--fs-good);color:var(--fs-good)}
+.label.danger,.label.error{border-color:var(--fs-danger);color:var(--fs-danger)}
+a.label:link,a.label:visited{color:var(--fs-dim)}
+a.label:hover{text-decoration:none;color:var(--fs-text)}
+.cbi-title .label{font-size:var(--fs-type-xs);font-weight:var(--fs-weight);padding:var(--fs-space-1) var(--fs-space-3);border-radius:var(--fs-radius-pill);background:var(--fs-panel2);border:var(--fs-hairline);color:var(--fs-dim);cursor:pointer;line-height:1.4;text-transform:none;letter-spacing:0;margin:0;min-width:0;box-shadow:none}
+.cbi-title .label:hover{color:var(--fs-text);border-color:var(--fs-dim);opacity:1}
+.cbi-title .label:focus-visible{outline:none;border-color:var(--fs-accent);box-shadow:var(--fs-focus-ring)}
+.cbi-title .label.notice{opacity:1;color:var(--fs-accent);border-color:var(--fs-accent)}
+#tabmenu{margin-bottom:0}
+#tabmenu ul.tabs,ul.cbi-tabmenu.mode,.tabs,.cbi-tabmenu{display:flex;flex-wrap:wrap;gap:var(--fs-space-1);list-style:none;margin:0 0 var(--fs-card-gap);padding:var(--fs-space-2);background:var(--fs-panel);border:var(--fs-hairline);border-radius:var(--fs-radius-lg);background-size:0 0}
+.cbi-section .tabs,.cbi-section .cbi-tabmenu:not(.mode):not(.map){background:var(--fs-panel2);padding:var(--fs-space-1-5);box-shadow:none}
+#tabmenu ul.tabs>li,ul.cbi-tabmenu.mode>li,.tabs>li,.cbi-tabmenu>li{height:auto;margin:0;padding:0;border:0;background:none;border-radius:var(--fs-radius);color:var(--fs-dim)}
+#tabmenu ul.tabs>li,ul.cbi-tabmenu.mode>li,.tabs>li{max-width:none}
+.cbi-tabmenu>li{max-width:100%}
+#tabmenu ul.tabs>li>a,ul.cbi-tabmenu.mode>li>a,.tabs>li>a,.cbi-tabmenu>li>a{display:block;margin:0;padding:var(--fs-space-2) var(--fs-space-3-5);border:0;border-radius:var(--fs-radius);line-height:1.3;font-size:var(--fs-type);font-weight:var(--fs-weight);color:var(--fs-dim);transition:background var(--fs-dur),color var(--fs-dur)}
+#tabmenu ul.tabs>li>a:hover,ul.cbi-tabmenu.mode>li>a:hover,.tabs>li>a:hover,.cbi-tabmenu>li>a:hover{background:var(--fs-accent-soft);color:var(--fs-text)}
+#tabmenu ul.tabs>li.active,ul.cbi-tabmenu.mode>li.cbi-tab,.tabs>li.active,.cbi-tabmenu>li.cbi-tab{color:var(--fs-on-accent)}
+#tabmenu ul.tabs>li.active>a,ul.cbi-tabmenu.mode>li.cbi-tab>a,.tabs>li.active>a,.cbi-tabmenu>li.cbi-tab>a{background:var(--fs-accent);color:var(--fs-on-accent);font-weight:var(--fs-weight-bold)}
+#tabmenu ul.tabs>li>a:focus-visible,ul.cbi-tabmenu.mode>li>a:focus-visible,.tabs>li>a:focus-visible,.cbi-tabmenu>li>a:focus-visible{outline:none;box-shadow:var(--fs-focus-ring-solo)}
+#tabmenu ul.tabs.fs-dense1>li>a,.tabs.fs-dense1>li>a,.cbi-tabmenu.fs-dense1>li>a{padding:var(--fs-space-1) var(--fs-space-2)}
+#tabmenu ul.tabs.fs-dense2,.tabs.fs-dense2,.cbi-tabmenu.fs-dense2{gap:var(--fs-space-1)}
+#tabmenu ul.tabs.fs-dense2>li>a,.tabs.fs-dense2>li>a,.cbi-tabmenu.fs-dense2>li>a{padding:var(--fs-space-1) var(--fs-space-2);font-size:var(--fs-type)}
+.cbi-tabmenu.map>li{font-size:var(--fs-type);font-weight:var(--fs-weight)}
+.cbi-tab-disabled[data-errors]::after{background:var(--fs-danger)}
+#view .cbi-section>div[style]:has(>svg){width:auto !important;margin-inline:calc(-1 * (var(--fs-card-pad) + 1px))}
+#view div[style]:has(>svg){border-color:var(--fs-border) !important}
+p a,dd a,.cbi-value-description a,.alert-message a:not(.btn):not(.cbi-button){text-decoration:underline}
+:is(strong,b):not(pre *,code *,#syslog *,textarea *){font-family:var(--fs-font-sans)}
+.ifacebadge,.cbi-value-field .ifacebadge{font-family:var(--fs-font-sans)}
+code,pre{font-weight:var(--fs-weight-normal)}
+.cbi-title-ref{color:var(--fs-accent)}
+.close{color:var(--fs-text);text-shadow:none;opacity:.6}
+.close:hover{opacity:1}
+.ifacebox{background:var(--fs-panel);border-color:var(--fs-border)}
+.ifacebox .ifacebox-head{background:var(--fs-panel2);color:var(--fs-text)}
+.ifacebox .ifacebox-head.active{background:var(--fs-accent);color:var(--fs-on-accent)}
+.ifacebadge{background:var(--fs-panel2);border-color:var(--fs-border)}
+.ifacebadge-active{border-color:var(--fs-accent)}
+.zonebadge-empty{border-color:var(--fs-border);color:var(--fs-dim)}
+.cbi-map{container-type:inline-size;container-name:fs-content}
+#view{container-type:inline-size;container-name:fs-view fs-content}
+.modal{container-type:inline-size;container-name:fs-view fs-content}
+.pager.center{display:flex;flex-direction:row;flex-wrap:nowrap;align-items:center;justify-content:center;gap:var(--fs-space-2-5);padding:var(--fs-space-3-5) 0}
+.pager .text{flex:0 0 auto;font-size:var(--fs-type);color:var(--fs-dim)}
+h1,h2,h3,h4,h5,h6{font-weight:var(--fs-weight-bold);color:var(--fs-text);line-height:var(--fs-leading-tight)}
+h1{margin-bottom:var(--fs-space-4);font-size:var(--fs-type-2xl)}
+h2{font-size:var(--fs-type-xl)}
+h3{font-size:var(--fs-type-lg)}
+h4{font-size:var(--fs-type)}
+h5{font-size:var(--fs-type)}
+h6{font-size:var(--fs-type-xs);color:var(--fs-dim);text-transform:uppercase}
+.cbi-map>h2,#view>h2{background:var(--fs-panel);border:var(--fs-hairline);border-radius:var(--fs-radius-lg);margin:0 0 var(--fs-card-gap);padding:var(--fs-space-4);font-size:var(--fs-type-xl);font-weight:var(--fs-weight-bold);color:var(--fs-text)}
+.cbi-map>h2:has(+ .cbi-map-descr),#view>h2:has(+ .cbi-map-descr){border-bottom:0;border-radius:var(--fs-radius-lg) var(--fs-radius-lg) 0 0;padding-bottom:var(--fs-space-1);margin-bottom:0;box-shadow:none}
+.cbi-map>h2 + .cbi-map-descr,#view>h2 + .cbi-map-descr{background:var(--fs-panel);border:var(--fs-hairline);border-top:0;border-radius:0 0 var(--fs-radius-lg) var(--fs-radius-lg);margin:0 0 var(--fs-card-gap);padding:0 var(--fs-space-4) var(--fs-space-4);color:var(--fs-dim);font-size:var(--fs-type);line-height:var(--fs-leading)}
+#view :is(textarea,input[type="text"],input[type="password"],.cbi-dropdown) + .right{margin-top:var(--fs-space-2-5)}
+.cbi-title-buttons{margin-block:var(--fs-space-2) var(--fs-card-gap)}
+.cbi-section-remove{margin-bottom:var(--fs-space-2)}
+.cbi-map>*:not(.cbi-section):not(.cbi-map-descr):not(.cbi-page-actions):not(h2):not(legend):not(.cbi-tabmenu):not(.cbi-map-tabbed):not(:empty){margin-bottom:var(--fs-card-gap)}
+:root[data-layout="top"] .fs-sidebar{min-height:var(--fs-bar-h);height:auto;gap:var(--fs-space-3);flex-wrap:nowrap;padding:0 max(20px,calc((100% - var(--fs-content-max)) / 2 + var(--fs-content-pad)))}
+:root[data-layout="top"] .fs-sidebar ul.nav{order:2;flex:1 1 auto;margin-inline-start:var(--fs-space-2)}
+:root[data-layout="top"] .fs-sidebar #indicators,:root[data-layout="top"] .fs-themerow,:root[data-layout="top"] .fs-logout{order:3;margin-inline-start:0}
+:root[data-layout="top"] .fs-modemenu:not(:empty):not(.single){display:flex;order:4;flex:1 1 100%;margin:0;gap:var(--fs-space-1)}
+:root[data-layout="top"] .fs-content{padding-top:var(--fs-space-6)}
+:root[data-layout="top"] .fs-sidebar.fs-bar-stack{flex-wrap:wrap;height:auto;padding:var(--fs-space-2-5) var(--fs-space-4)}
+:root[data-layout="top"] .fs-sidebar.fs-bar-stack ul.nav{order:3;flex:1 1 100%;margin-inline-start:0}
+:root[data-layout="top"] .fs-sidebar.fs-bar-stack #indicators,:root[data-layout="top"] .fs-sidebar.fs-bar-stack .fs-themerow,:root[data-layout="top"] .fs-sidebar.fs-bar-stack .fs-logout{order:2}
+:root[data-layout="top"] .fs-sidebar.fs-bar-stack #indicators,:root[data-layout="top"] .fs-sidebar.fs-bar-stack .fs-themerow{margin-inline-start:auto}
+:root[data-layout="top"] .fs-sidebar.fs-bar-stack #indicators:not(:empty) ~ .fs-themerow{margin-inline-start:0}
+:root[data-layout="top"] .fs-sidebar.fs-bar-stack .fs-themerow ~ .fs-themerow{margin-inline-start:0}
+.cbi-value-field:has(>.cbi-button){display:inline-flex;flex-wrap:wrap;gap:var(--fs-space-2);align-items:center;justify-content:flex-end}
+.cbi-button,.btn,input[type="button"],input[type="submit"],input[type="reset"]{display:inline-flex;align-items:center;justify-content:center;width:auto;height:auto;min-height:var(--fs-ctl-h);padding:var(--fs-space-2) var(--fs-space-3-5);font-family:var(--fs-font-sans);font-weight:var(--fs-weight);line-height:1.1;color:var(--fs-text);background:var(--fs-panel2);border:var(--fs-hairline);border-radius:var(--fs-radius);box-shadow:none;text-shadow:none;transition:var(--fs-field-transition)}
+.cbi-button:hover,.btn:hover,button:hover,input[type="button"]:hover,input[type="submit"]:hover,input[type="reset"]:hover{border-color:var(--fs-dim)}
+.cbi-button:focus-visible,.btn:focus-visible,input[type="button"]:focus-visible,input[type="submit"]:focus-visible,input[type="reset"]:focus-visible{outline:none;border-color:var(--fs-accent);box-shadow:var(--fs-focus-ring)}
+.cbi-button-positive:focus-visible,.cbi-button-action:focus-visible,.cbi-button-negative:focus-visible,.cbi-button-remove:focus-visible,.cbi-button-apply:focus-visible,.cbi-button-save:focus-visible,.btn.primary:focus-visible,.btn.danger:focus-visible,.btn.error:focus-visible,.btn.success:focus-visible,.btn.info:focus-visible{outline:none;border-color:transparent;box-shadow:var(--fs-focus-ring-solo)}
+.cbi-button:active,.btn:active,.btn.active,input[type="button"]:active,input[type="submit"]:active,input[type="reset"]:active{box-shadow:inset 0 2px 4px color-mix(in srgb,var(--fs-border) 40%,transparent)}
+.cbi-button-positive,.btn.success{background:var(--fs-good);border-color:transparent;color:var(--fs-on-good)}
+.cbi-button-action,.cbi-button-action.important,.btn.primary,.btn.info{background:var(--fs-accent);border-color:transparent;color:var(--fs-on-accent)}
+.cbi-button-negative,.cbi-button-negative.important,.cbi-button-remove,.btn.danger,.btn.error{background:var(--fs-danger);border-color:transparent;color:var(--fs-on-danger)}
+.cbi-button-positive:hover,.cbi-button-action:hover,.cbi-button-apply:hover,.cbi-button-save:hover,.cbi-button-negative:hover,.btn.success:hover,.btn.info:hover,.btn.danger:hover,.btn.error:hover{filter:var(--fs-hover-lift);border-color:transparent}
+.cbi-button-reset,.cbi-button-link{background:transparent;border-color:var(--fs-border);color:var(--fs-dim)}
+.btn.large{font-size:var(--fs-type-lg);min-height:var(--fs-ctl-h-lg);padding:var(--fs-space-2) var(--fs-space-4);border-radius:var(--fs-radius)}
+.btn.small{font-size:var(--fs-type-xs);min-height:var(--fs-ctl-h-sm);padding:var(--fs-space-1) var(--fs-space-2-5);border-radius:var(--fs-radius-sm)}
+.cbi-section .table[id] .td .btn,.cbi-section .table[id] .td .cbi-button{padding:var(--fs-space-1) var(--fs-space-2-5);font-size:var(--fs-type)}
+.alert-message .btn,.alert-message .cbi-button{border-radius:var(--fs-radius);padding:var(--fs-space-2) var(--fs-space-3);font-weight:var(--fs-weight);background:var(--fs-panel2);border:var(--fs-hairline);color:var(--fs-text)}
+.alert-message .btn:hover{border-color:var(--fs-accent);color:var(--fs-accent)}
+.cbi-page-actions{background:transparent;border:0;border-radius:0;box-shadow:none;padding:var(--fs-space-2) 0;margin:0 0 var(--fs-card-gap);text-align:end;display:flex;align-items:center;justify-content:flex-end;gap:var(--fs-space-2);flex-wrap:wrap}
+.cbi-page-actions .cbi-button,.cbi-page-actions .btn{min-height:var(--fs-ctl-h);padding:0 var(--fs-space-5);border-radius:var(--fs-radius);font-weight:var(--fs-weight);display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}
+.cbi-page-actions .cbi-button-reset{background:var(--fs-danger-soft);border:1px solid var(--fs-danger-line);color:var(--fs-text)}
+.cbi-page-actions .cbi-button-reset:hover{background:var(--fs-danger-fill);border-color:var(--fs-danger-line-hi)}
+input[type="file"]::file-selector-button{margin-inline-end:var(--fs-space-3);padding:var(--fs-space-2) var(--fs-space-3-5);font-family:var(--fs-font-sans);font-size:var(--fs-type);font-weight:var(--fs-weight);color:var(--fs-text);background:var(--fs-panel2);border:var(--fs-hairline);border-radius:var(--fs-radius-sm);cursor:pointer}
+input[type="file"]::file-selector-button:hover{border-color:var(--fs-accent);color:var(--fs-accent)}
+.cbi-section-actions .cbi-button,.cbi-section-actions .btn,.cbi-section-table .cbi-button{min-height:var(--fs-ctl-h-sm);padding:0 var(--fs-space-3);font-size:var(--fs-type);flex-basis:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.td .cbi-button,.cbi-section-actions .cbi-button,.cbi-section-table .cbi-button{background:transparent;background-image:none;border:var(--fs-hairline);color:var(--fs-dim)}
+.td .cbi-button:hover{border-color:var(--fs-dim);color:var(--fs-text)}
+.td .cbi-button-action,.td .cbi-button-edit,.td .cbi-button-apply,.cbi-section-actions .cbi-button-edit,.cbi-section-actions .cbi-button-action{color:var(--fs-accent);border-color:var(--fs-accent-line);background:transparent}
+.td .cbi-button-action:hover,.td .cbi-button-edit:hover,.td .cbi-button-apply:hover,.cbi-section-actions .cbi-button-action:hover{background:var(--fs-accent-soft);color:var(--fs-text);border-color:var(--fs-accent-line-hi)}
+.td .cbi-button-positive,.cbi-section-actions .cbi-button-positive{color:var(--fs-good);border-color:var(--fs-good-line);background:transparent}
+.td .cbi-button-positive:hover,.cbi-section-actions .cbi-button-positive:hover{background:var(--fs-good-soft);color:var(--fs-text);border-color:var(--fs-good-line-hi)}
+.td .cbi-button-negative,.td .cbi-button-remove,.cbi-section-actions .cbi-button-remove,.cbi-section-actions .cbi-button-negative{color:var(--fs-danger);border-color:var(--fs-danger-line);background:transparent}
+.td .cbi-button-negative:hover,.td .cbi-button-remove:hover,.cbi-section-actions .cbi-button-negative:hover{background:var(--fs-danger-soft);color:var(--fs-text);border-color:var(--fs-danger-line-hi)}
+.cbi-page-actions .cbi-button-apply,.cbi-page-actions .cbi-dropdown .cbi-button-apply{background:var(--fs-accent);background-image:none;border:1px solid transparent;color:var(--fs-on-accent);--on-color:var(--fs-on-accent);text-shadow:none}
+.cbi-page-actions .cbi-button-save,.cbi-page-actions .cbi-button-apply + .cbi-button-save,.cbi-page-actions .cbi-button-negative + .cbi-button-save{background:var(--fs-accent-soft);background-image:none;border:1px solid var(--fs-accent-line);color:var(--fs-text);--on-color:var(--fs-on-accent);text-shadow:none}
+.cbi-page-actions .cbi-button-save:hover{background:var(--fs-accent-fill);border-color:var(--fs-accent-line-hi);filter:none}
+.cbi-dynlist>.add-item>button,.cbi-dynlist>.add-item>.cbi-button-add{flex:0 0 auto;width:auto;min-width:var(--fs-ctl-h);min-height:var(--fs-ctl-h-sm);padding:0 var(--fs-space-3);margin:0;border:0;border-inline-start:var(--fs-hairline);border-radius:0;background:transparent;color:var(--fs-dim)}
+.cbi-dynlist>.add-item>button:hover,.cbi-dynlist>.add-item>.cbi-button-add:hover{color:var(--fs-accent)}
+.control-group:has(>.cbi-input-password)>button.cbi-button-neutral{border:0;border-radius:0;margin:0;background:transparent;color:var(--fs-dim);padding:0 var(--fs-space-3);min-height:var(--fs-ctl-h);border-inline-start:var(--fs-hairline)}
+.control-group:has(>.cbi-input-password)>button.cbi-button-neutral:hover{color:var(--fs-accent)}
+.pager .btn.prev,.pager .btn.next{flex:0 0 auto;width:var(--fs-ctl-h);min-width:var(--fs-ctl-h);height:var(--fs-ctl-h);padding:0;border-radius:var(--fs-radius);border:var(--fs-hairline);background:var(--fs-panel2);color:var(--fs-text);display:inline-flex;align-items:center;justify-content:center}
+.pager .btn.prev:hover,.pager .btn.next:hover{border-color:var(--fs-accent);color:var(--fs-accent)}
+.cbi-value{justify-content:space-between;gap:var(--fs-space-4);padding:var(--fs-space-2) 0;margin:0;border-bottom:var(--fs-hairline)}
+.cbi-value-field>.control-group{margin-top:var(--fs-space-2)}
+.cbi-value-field>.control-group:not(:has(.cbi-input-password)){display:inline-flex;flex-wrap:wrap;gap:var(--fs-space-2);align-items:center}
+.cbi-value:not(:has(+ .cbi-value)){border-bottom:0}
+.cbi-value:not(:has(.cbi-value-description)):has(>.cbi-value-field>.cbi-checkbox,>.cbi-value-field>div>.cbi-radio){align-items:center}
+input,button{transition:var(--fs-field-transition)}
+.cbi-value-title{color:var(--fs-dim);font-size:var(--fs-type);font-weight:var(--fs-weight);line-height:var(--fs-leading)}
+.cbi-value-field,.cbi-value-field *:where(:not(.btn,.cbi-button,.cbi-value-description)){font-family:var(--fs-font-mono)}
+.cbi-value-description{font-family:var(--fs-font-sans);text-wrap:pretty}
+.cbi-value-field input[type="text"],.cbi-value-field input[type="url"],.cbi-value-field input[type="search"]{width:100%;max-width:var(--fs-field-max)}
+.cbi-value:has(.cbi-value-title ~ .cbi-value-title)>.cbi-value-title,.cbi-value:has(.cbi-value-title ~ .cbi-value-title)>.cbi-value-field{flex:1 1 100%;margin-inline-start:0;text-align:start}
+.td>input[type="text"],.td>input[type="password"],.td>select,.td>.cbi-dropdown:not(.btn):not(.cbi-button),.cbi-dynlist>.add-item>.cbi-dropdown{width:100%}
+.cbi-section-create>*>input{width:100%}
+div.cbi-value var,.td.cbi-value-field var{color:var(--fs-accent)}
+.td.cbi-value-field var.cbi-tooltip-container{border-bottom-color:var(--fs-accent)}
+.cbi-filebrowser{border-color:var(--fs-border);background:var(--fs-panel);border-radius:var(--fs-radius-lg)}
+.cbi-filebrowser>ul>li:hover{background:var(--fs-panel2)}
+.cbi-filebrowser .upload{border-top-color:var(--fs-border)}
+.cbi-filebrowser>*{border-bottom-color:var(--fs-border);padding:var(--fs-space-1-5) var(--fs-space-2)}
+.cbi-filebrowser>ul{display:flex;flex-direction:column;gap:var(--fs-space-1)}
+.cbi-filebrowser>ul>li{align-items:center;flex-wrap:wrap;gap:var(--fs-space-2)}
+.cbi-filebrowser>ul>li>div:first-child{flex:1 1 auto;min-width:0}
+.cbi-filebrowser>ul>li>div:last-child{flex:0 1 auto;min-width:0;display:flex;flex-wrap:wrap;gap:var(--fs-space-2);justify-content:flex-end}
+.cbi-filebrowser>ul>li>div:last-child>button{margin:0}
+.cbi-filebrowser .upload>div>input{width:100%}
+.btn>input[type="checkbox"],.btn>input[type="radio"]{margin-inline-end:.5em}
+.cbi-checkbox{display:inline-flex;align-items:center;gap:var(--fs-space-2);overflow-wrap:anywhere}
+.cbi-checkbox>input[type="checkbox"]{position:absolute;opacity:0;width:0;height:0;margin:0}
+.cbi-checkbox>label[for]{--sw-w:calc(40px * var(--fs-density-box));--sw-h:calc(22px * var(--fs-density-box));--sw-knob:calc(16px * var(--fs-density-box));--sw-pad:calc(2px * var(--fs-density-box));display:inline-block;position:relative;margin:0;flex-shrink:0;width:var(--sw-w);height:var(--sw-h);border-radius:var(--fs-radius-pill);background:var(--fs-track);border:var(--fs-hairline);cursor:pointer;transition:background var(--fs-dur),border-color var(--fs-dur);vertical-align:middle}
+.cbi-checkbox>label[for]::before{content:"";position:absolute;inset:-4px -2px}
+.cbi-checkbox>label[for]::after{content:"";position:absolute;top:var(--sw-pad);inset-inline-start:var(--sw-pad);width:var(--sw-knob);height:var(--sw-knob);border-radius:50%;background:var(--fs-dim);transform:translateX(0);transition:transform var(--fs-dur-move),background var(--fs-dur)}
+.cbi-checkbox>input[type="checkbox"]:checked + label{background:var(--fs-accent);border-color:transparent}
+.cbi-checkbox>input[type="checkbox"]:checked + label::after{transform:translateX(calc(var(--sw-w) - var(--sw-knob) - var(--sw-pad) * 2 - 2px));background:var(--fs-on-accent)}
+.cbi-checkbox>input[type="checkbox"]:focus-visible + label{border-color:var(--fs-accent);box-shadow:var(--fs-focus-ring-solo)}
+.cbi-checkbox>input[type="checkbox"]:disabled + label{opacity:var(--fs-disabled-opacity);cursor:not-allowed}
+input[type="checkbox"],input[type="radio"]{--bd-color:var(--fs-border);--fg-color:var(--fs-text);--box:calc(16px * var(--fs-density-box));--mark-inset:3px;--hit:max(24px,var(--box));appearance:none;width:var(--hit);height:var(--hit);margin:calc((var(--box) - var(--hit)) / 2);color:var(--fg-color);position:relative;display:inline-block;cursor:pointer;background:none;border:none}
+input[type="checkbox"]::before,input[type="checkbox"]::after,input[type="radio"]::before,input[type="radio"]::after{position:absolute;content:""}
+input[type="checkbox"]::before,input[type="radio"]::before{inset:0;margin:auto;width:var(--box);height:var(--box);background:linear-gradient(var(--fs-panel),var(--fs-panel2));border:1px solid var(--bd-color);transition:var(--fs-field-transition)}
+input[type="checkbox"]::before{border-radius:2px}
+input[type="radio"],input[type="radio"]::before{border-radius:50%}
+input[type="checkbox"]::after,input[type="radio"]::after{inset:0;margin:auto;width:calc(var(--box) - var(--mark-inset) * 2);height:calc(var(--box) - var(--mark-inset) * 2)}
+input[type="checkbox"]:checked::after,input[type="radio"]:checked::after{--checkmark-icon:url("data:image/svg+xml,");-webkit-mask:var(--checkmark-icon) center/cover no-repeat;mask:var(--checkmark-icon) center/cover no-repeat;background:var(--fg-color)}
+input[type="radio"]:checked:after{--checkmark-icon:url("data:image/svg+xml,")}
+input[type="checkbox"].cbi-input-invalid,input[type="radio"].cbi-input-invalid{--bd-color:var(--fs-danger);--fg-color:var(--fs-danger)}
+input{display:inline-block;width:210px;height:var(--fs-control-h);padding:var(--fs-space-1) var(--fs-space-2);background:var(--fs-panel);color:var(--fs-text);border:var(--fs-hairline);border-radius:var(--fs-radius)}
+input[type="file"]{color:var(--fs-dim);font-size:var(--fs-type);padding:initial;border:initial;line-height:initial;box-shadow:none;width:auto}
+select,select.cbi-input-select,.cbi-value-field select{appearance:none;background-color:var(--fs-panel2);background-image:var(--fs-select-chevron);background-repeat:no-repeat;background-size:6px 6px;background-position:right calc(var(--fs-space-3) - 1px) center,right calc(var(--fs-space-3) + 5px) center;padding:var(--fs-space-2) var(--fs-space-8) var(--fs-space-2) var(--fs-space-3);border:var(--fs-hairline);border-radius:var(--fs-radius);color:var(--fs-text);font-family:var(--fs-font-sans);font-size:var(--fs-type);line-height:1.3;min-height:var(--fs-ctl-h);cursor:pointer;width:210px;display:inline-block;transition:var(--fs-field-transition)}
+select[multiple]{background-color:var(--fs-panel2);color:var(--fs-text);height:inherit}
+option{font-size:100%;line-height:2em}
+select option{background:var(--fs-panel);color:var(--fs-text)}
+@supports (appearance:base-select){.cbi-value-field select{appearance:base-select}
+.cbi-value-field select::picker-icon{display:none}
+::picker(select){appearance:base-select;background:var(--fs-panel);border:var(--fs-hairline);border-radius:var(--fs-radius-lg);box-shadow:var(--fs-shadow-pop);padding:var(--fs-space-1-5);margin-top:var(--fs-space-1-5)}
+.cbi-value-field select option{padding:var(--fs-space-2) var(--fs-space-3);border-radius:var(--fs-radius-sm);font-size:var(--fs-type);background:transparent;min-height:0}
+.cbi-value-field select option::checkmark{display:none}
+.cbi-value-field select option:hover,.cbi-value-field select option:focus-visible{background:var(--fs-panel2)}
+.cbi-value-field select option:checked{background:var(--fs-panel2);color:var(--fs-accent);box-shadow:var(--fs-row-rail)}
+}
+.cbi-select:not(select){display:inline-block;width:210px;height:var(--fs-ctl-h);color:var(--fs-text);font-size:var(--fs-type);border:var(--fs-hairline);border-radius:var(--fs-radius);padding:0;position:relative;background-color:var(--fs-panel2);background-image:var(--fs-select-chevron);background-repeat:no-repeat;background-size:6px 6px;background-position:right calc(var(--fs-space-3) - 1px) center,right calc(var(--fs-space-3) + 5px) center;transition:var(--fs-field-transition)}
+.cbi-select[disabled]:not(select){opacity:var(--fs-disabled-opacity);pointer-events:none}
+.cbi-select select,.cbi-select select:focus{appearance:none;outline:none;border:none;background:transparent;background-image:none;box-shadow:none;height:100%;min-height:0;width:100%;padding:0 var(--fs-space-8) 0 var(--fs-space-3);font-size:var(--fs-type);line-height:normal;cursor:pointer}
+.spinning,.cbi-value em,em.spinning{color:var(--fs-dim)}
+.cbi-input-text,.cbi-input-password,.cbi-input-textarea,.cbi-section-create-name,.cbi-value-field input.create-item-input,input[type="text"],input[type="password"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="date"],input[type="time"],input[type="datetime-local"],input[type="month"],input[type="week"],input[type="color"],textarea{background:var(--fs-panel2);border:var(--fs-hairline);border-radius:var(--fs-radius);color:var(--fs-text);padding:var(--fs-space-2) var(--fs-space-3);min-height:var(--fs-ctl-h);box-shadow:none;transition:var(--fs-field-transition)}
+textarea{min-height:84px;line-height:var(--fs-leading);width:210px;max-width:100%;font-size:var(--fs-type)}
+textarea::-webkit-scrollbar{width:16px;height:16px}
+textarea::-webkit-scrollbar-track{background:transparent;margin:var(--fs-space-4) 0 var(--fs-space-2-5)}
+textarea::-webkit-scrollbar-thumb{background:var(--fs-dim);border-radius:var(--fs-radius-pill);border:5px solid transparent;background-clip:padding-box}
+textarea::-webkit-scrollbar-thumb:hover{background:var(--fs-text)}
+textarea::-webkit-scrollbar-corner{background:transparent}
+textarea::-webkit-resizer{background:transparent;border-right:2px solid var(--fs-accent);border-bottom:2px solid var(--fs-accent);border-bottom-right-radius:var(--fs-radius);margin:calc(16px - var(--fs-radius)) 0 0 calc(16px - var(--fs-radius))}
+:root[dir="rtl"] textarea::-webkit-resizer{border-right:0;border-left:2px solid var(--fs-accent);border-bottom-right-radius:0;border-bottom-left-radius:var(--fs-radius);margin:calc(16px - var(--fs-radius)) calc(16px - var(--fs-radius)) 0 0}
+.cbi-input-text:hover,.cbi-input-password:hover,.cbi-input-textarea:hover,input[type="text"]:hover,input[type="password"]:hover,input[type="number"]:hover,input[type="email"]:hover,input[type="url"]:hover,input[type="search"]:hover,textarea:hover{border-color:var(--fs-dim)}
+.cbi-input-text:focus,.cbi-input-password:focus,.cbi-input-textarea:focus,.cbi-section-create-name:focus,input:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="date"]:focus,input[type="time"]:focus,textarea:focus,select:focus,.cbi-dynlist>.item:focus{border-color:var(--fs-accent);box-shadow:var(--fs-focus-ring);outline:none}
+input[type="checkbox"]:focus::before,input[type="radio"]:focus::before,.cbi-select.focus,.cbi-dynlist>.add-item:focus-within,.control-group:is(:has(>.cbi-input-password)):focus-within{border-color:var(--fs-accent);box-shadow:var(--fs-focus-ring)}
+input[type="checkbox"].cbi-input-invalid:focus::before,input[type="radio"].cbi-input-invalid:focus::before,.cbi-select.cbi-input-invalid.focus,.cbi-input-invalid,input.cbi-input-invalid,textarea.cbi-input-invalid,.cbi-value-error input,.cbi-value-error textarea,.cbi-input-invalid:focus{border-color:var(--fs-danger);box-shadow:var(--fs-focus-ring-invalid)}
+.cbi-input-invalid.cbi-dropdown:not(.btn):not(.cbi-button),.cbi-input-invalid.cbi-dropdown:not([open])>ul>li{border-color:var(--fs-danger)}
+:is(input,select,textarea)[readonly][readonly]{border-color:color-mix(in srgb,var(--fs-border) calc(var(--fs-disabled-opacity) * 100%),transparent);pointer-events:auto;cursor:auto}
+.cbi-dynlist{gap:var(--fs-space-1-5);background:transparent;border:0;padding:0;min-height:0;min-width:min(240px,100%);max-width:var(--fs-field-max);width:100%;vertical-align:top}
+.cbi-dynlist>.item{background:var(--fs-panel);border:var(--fs-hairline);border-radius:var(--fs-radius-sm);color:var(--fs-text);box-shadow:none;margin:0;transition:var(--fs-field-transition)}
+.item::after{display:inline-block;cursor:pointer;background:linear-gradient(var(--fs-panel),var(--fs-panel) 25%,var(--fs-panel2));padding:0 var(--fs-space-3);color:var(--fs-text);line-height:2em;border:var(--fs-hairline);border-radius:var(--fs-radius);transition:var(--fs-field-transition)}
+.item:hover::after{filter:var(--fs-hover-lift)}
+.cbi-dynlist>.item::after{display:inline-flex;padding:0 var(--fs-space-2);color:var(--fs-danger);border-radius:0 var(--fs-radius-sm) var(--fs-radius-sm) 0}
+.cbi-dynlist>.add-item{align-items:stretch;background:var(--fs-panel2);border:var(--fs-hairline);border-radius:var(--fs-radius-sm);overflow:hidden;transition:var(--fs-field-transition)}
+.cbi-dynlist>.add-item>input,.cbi-dynlist>.add-item>.cbi-input-text{flex:1 1 auto;min-width:0;width:auto;border:0;background:transparent;box-shadow:none;min-height:var(--fs-ctl-h-sm);border-radius:0}
+.control-group:has(>.cbi-input-password){display:inline-flex;align-items:stretch;gap:0;background:var(--fs-panel2);border:var(--fs-hairline);border-radius:var(--fs-radius);overflow:hidden;transition:var(--fs-field-transition)}
+.control-group:has(>.cbi-input-password)>.cbi-input-password{border:0;border-radius:0;background:transparent;box-shadow:none;min-height:var(--fs-ctl-h)}
+.cbi-section-create,.cbi-section-create.cbi-tblsection-create{margin-top:var(--fs-space-3)}
+.hidden.hidden:not([class*=":"]){display:none}
+input[type="range"]{appearance:none;width:100%;height:24px;min-height:0;padding:0;margin-block:-9px;background:transparent;border:0;border-radius:0;outline:none;box-shadow:none;cursor:pointer}
+input[type="range"]::-webkit-slider-runnable-track{width:100%;height:6px;background:var(--fs-track);border:0;border-radius:var(--fs-radius-pill)}
+input[type="range"]::-moz-range-track{width:100%;height:6px;background:var(--fs-track);border:0;border-radius:var(--fs-radius-pill)}
+input[type="range"]::-webkit-slider-thumb{appearance:none;width:16px;height:16px;border-radius:50%;background:var(--fs-accent);border:2px solid var(--fs-panel);cursor:pointer;margin-top:-5px}
+input[type="range"]::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:var(--fs-accent);border:2px solid var(--fs-panel);cursor:pointer}
+input[type="range"]:focus::-webkit-slider-runnable-track{box-shadow:var(--fs-focus-ring-solo)}
+input[type="range"]:focus::-moz-range-track{box-shadow:var(--fs-focus-ring-solo)}
+.cbi-range-slider{display:inline-flex;align-items:center;vertical-align:middle;gap:var(--fs-space-3);width:100%;max-width:var(--fs-field-max)}
+.cbi-range-slider-value{background:var(--fs-panel2);border:var(--fs-hairline);border-radius:var(--fs-radius-sm);padding:var(--fs-space-1) var(--fs-space-2);color:var(--fs-accent);font-family:var(--fs-font-mono);font-weight:var(--fs-weight-normal);font-size:var(--fs-type);white-space:nowrap}
+.cbi-range-slider-calc,.cbi-range-slider-calc-units{color:var(--fs-dim);font-size:var(--fs-type)}
+.dropdown-menu{background-color:var(--fs-panel);border-color:var(--fs-border)}
+.dropdown-menu a{color:var(--fs-text);text-shadow:none}
+.dropdown-menu a:hover,.dropdown-menu a:focus-visible{background:var(--fs-panel2);background-image:none;color:var(--fs-accent)}
+.cbi-page-actions .cbi-dropdown:not(.btn):not(.cbi-button){border:0;border-radius:var(--fs-radius);background:transparent}
+.cbi-page-actions .cbi-dropdown.cbi-button-apply,.cbi-page-actions .cbi-dropdown>.cbi-button{background:var(--fs-accent);background-image:none;border:0;color:var(--fs-on-accent)}
+.cbi-page-actions .cbi-dropdown.cbi-button-apply:hover,.cbi-page-actions .cbi-dropdown>.cbi-button:hover{filter:var(--fs-hover-lift)}
+.cbi-dropdown{display:inline-flex;cursor:pointer;height:auto;position:relative;padding:0}
+.cbi-dropdown img{max-height:1.6em;width:auto}
+.cbi-dropdown>ul{margin:0;padding:0;list-style:none;overflow-x:hidden;overflow-y:hidden;display:flex;width:100%}
+.cbi-dropdown.btn>ul:not(.dropdown),.cbi-dropdown.cbi-button>ul:not(.dropdown){margin:0 0 0 var(--fs-space-3)}
+.cbi-dropdown.btn.spinning>ul:not(.dropdown),.cbi-dropdown.cbi-button.spinning>ul:not(.dropdown){margin:0}
+.cbi-dropdown>ul.preview{display:none}
+.cbi-dropdown>.open,.cbi-dropdown>.more{flex-grow:0;flex-shrink:0;flex-direction:column;justify-content:center;text-align:center}
+.cbi-dropdown.btn>.open,.cbi-dropdown.cbi-button>.open{margin-inline-start:.5em}
+.cbi-dropdown:not(.btn):not(.cbi-button)>ul>li[placeholder]{display:none}
+.cbi-dropdown>ul>li{display:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;flex-shrink:1;flex-grow:1;align-items:center;align-self:center;color:inherit}
+.cbi-dropdown>ul>li .hide-open{display:initial}
+.cbi-dropdown>ul>li .hide-close{display:none}
+.cbi-dropdown>ul>li[display]:not([display="0"]){border-inline-start:var(--fs-hairline)}
+.cbi-dropdown[empty]>ul{max-width:1px}
+.cbi-dropdown>ul>li>form{display:none;margin:0;padding:0;pointer-events:none}
+.cbi-dropdown>ul>li img{vertical-align:middle;margin-inline-end:.25em}
+.cbi-dropdown>ul>li>form>input[type="checkbox"]{margin:0 var(--fs-space-1) 0 0}
+.cbi-dropdown>ul>li[display],.cbi-dropdown[open]>ul.preview,.cbi-dropdown[open]>ul.dropdown>li,.cbi-dropdown[multiple]>ul>li>label,.cbi-dropdown[multiple][open]>ul.dropdown>li,.cbi-dropdown[multiple][more]>.more,.cbi-dropdown[multiple][empty]>.more{flex-grow:1;display:flex}
+.cbi-dropdown:not([open]):not(.btn):not(.cbi-button)>ul>li[placeholder][display]{flex-grow:1;display:flex}
+.cbi-dropdown[empty]>ul>li,.cbi-dropdown[optional][open]>ul.dropdown>li[placeholder]{display:block}
+.cbi-dropdown[multiple][open]>ul.dropdown>li>form{display:flex}
+.cbi-dropdown[open]>ul.dropdown>li .hide-open{display:none}
+.cbi-dropdown[open]>ul.dropdown>li .hide-close{display:initial;min-width:0 !important}
+.cbi-dropdown[open]>ul.dropdown>li:last-child{margin-bottom:0}
+.cbi-dropdown[open]>ul.dropdown>li[unselectable]{opacity:.7}
+.cbi-dropdown:not(.btn):not(.cbi-button){background:var(--fs-panel2);border:var(--fs-hairline);border-radius:var(--fs-radius);color:var(--fs-text);min-height:var(--fs-ctl-h);height:var(--fs-control-h);font-size:var(--fs-type);transition:var(--fs-field-transition);min-width:210px;max-width:400px;width:auto;display:inline-flex;padding:0}
+.cbi-dropdown.btn,.cbi-dropdown.cbi-button{display:inline-flex;padding:0}
+.cbi-dropdown:not(.btn):not(.cbi-button):hover{border-color:var(--fs-dim)}
+.cbi-dropdown:focus,.cbi-dropdown[open]{border-color:var(--fs-accent);box-shadow:var(--fs-focus-ring);outline:none}
+.cbi-dropdown:not(.btn):not(.cbi-button)>ul>li,.cbi-dropdown>ul.dropdown>li{padding:var(--fs-space-2) var(--fs-space-3);color:var(--fs-text);font-family:var(--fs-font-sans);min-height:var(--fs-space-5)}
+.cbi-dropdown>ul>li>form,.cbi-dropdown>ul>li label{font-family:inherit}
+.cbi-dropdown:not(.btn):not(.cbi-button)>ul>li[display]:not([display="0"]){border-inline-start:0}
+.cbi-dropdown:not(.btn):not(.cbi-button)>ul>li[placeholder],.cbi-dropdown>ul.dropdown>li[placeholder]{color:var(--fs-placeholder)}
+.cbi-dropdown>ul>li input[type="text"]{height:var(--fs-space-5)}
+.cbi-dropdown[open]>ul.dropdown>li>input.create-item-input:first-child:last-child{width:100%}
+.cbi-dropdown>.open{display:flex;font-size:0;line-height:0;border:0;position:relative;padding:0;align-self:stretch;min-width:var(--fs-btn-size);min-height:var(--fs-ctl-h-sm);align-items:center;cursor:pointer}
+.cbi-dropdown>.more{line-height:2em;padding:0 .3em;color:var(--fs-dim);display:none}
+.cbi-dropdown>.open>*{display:none}
+.cbi-dropdown>.open::after{content:"";position:absolute;inset:0;margin:auto;width:7px;height:7px;border-right:2px solid var(--fs-dim);border-bottom:2px solid var(--fs-dim);transform:translateY(-2px) rotate(45deg)}
+.cbi-dropdown.btn>.open::after,.cbi-dropdown.cbi-button>.open::after{border-color:currentColor}
+.cbi-dropdown[open]{z-index:var(--fs-z-dropdown)}
+.cbi-dropdown[open]>ul.dropdown{display:block;position:absolute;z-index:var(--fs-z-dropdown);transition:max-height var(--fs-dur-move);overflow-y:auto;background:var(--fs-panel);border:var(--fs-hairline);border-radius:var(--fs-radius-lg);box-shadow:var(--fs-shadow-pop);padding:var(--fs-space-1-5);margin-top:var(--fs-space-1-5)}
+.cbi-dropdown[open]>ul.dropdown>li{border-radius:var(--fs-radius-sm);padding:var(--fs-space-2) var(--fs-space-3);border-inline-start:0;color:var(--fs-text);position:relative;white-space:normal;overflow-wrap:anywhere;border-bottom:0}
+.cbi-dropdown[open]>ul.dropdown>li:not(:last-child)::after{content:"";position:absolute;inset-inline:11px;bottom:0;height:1px;background:var(--fs-border)}
+.cbi-dropdown[open]>ul.dropdown>li:hover,.cbi-dropdown[open]>ul.dropdown>li.focus{background:var(--fs-panel2);color:var(--fs-accent)}
+.cbi-dropdown[open]>ul.dropdown>li[selected]{background:var(--fs-panel2);color:var(--fs-accent);box-shadow:var(--fs-row-rail)}
+.cbi-dropdown[open]>ul.dropdown>li[selected]:focus-visible,.cbi-dropdown[open]>ul.dropdown>li:focus-visible{outline:none;box-shadow:var(--fs-row-rail),inset 0 0 0 2px var(--fs-accent-soft)}
+.cbi-dropdown[multiple][open]>ul.dropdown>li[selected]:not(:focus-visible){box-shadow:none}
+.cbi-dropdown[multiple][open]>ul.dropdown>li[selected]:focus-visible{box-shadow:inset 0 0 0 2px var(--fs-accent-soft)}
+.cbi-dropdown[open]>ul.dropdown>li.create-item{border-top:var(--fs-hairline);margin-top:var(--fs-space-1)}
+.cbi-dropdown .create-item-input{background:var(--fs-panel2);border:var(--fs-hairline);border-radius:var(--fs-radius);color:var(--fs-text)}
+.cbi-dynlist>.add-item:has(>.cbi-dropdown){background:none;border:0;overflow:visible;box-shadow:none}
+.cbi-section-table .td .cbi-dropdown,.cbi-section-table .td .cbi-input-select,.cbi-section-table .td select,.cbi-section-table .td input[type="text"]{min-width:0;max-width:none;width:100%}
+@container fs-content (max-width:992px){.table.cbi-section-table,.table.cbi-section-table .tbody,.table.cbi-section-table .td{display:block;width:auto}
+.table.cbi-section-table .thead,.table.cbi-section-table .tr.cbi-section-table-titles,.table.cbi-section-table .tr.cbi-section-table-descr{display:none}
+.table.cbi-section-table .tr{display:flex;flex-wrap:wrap;width:auto;border:var(--fs-hairline);border-radius:var(--fs-radius-lg);background:var(--fs-panel2);padding:var(--fs-space-2) var(--fs-space-3-5) var(--fs-space-3);margin-bottom:var(--fs-space-3)}
+.table.cbi-section-table .tr:hover{background:var(--fs-panel2)}
+.table.cbi-section-table .td{flex:1 1 100%;min-width:0;margin:0;padding-block:var(--fs-space-1-5);padding-inline:0 var(--fs-space-2-5);border:0;text-align:start;overflow-wrap:anywhere;white-space:normal}
+.table.cbi-section-table .td[data-widget="CBI.DummyValue"],.table.cbi-section-table .td[data-widget="CBI.FlagValue"],.table.cbi-section-table .td[data-widget="CBI.Button"],.table.cbi-section-table .td[data-widget="dvalue"],.table.cbi-section-table .td[data-widget="fvalue"],.table.cbi-section-table .td[data-widget="button"]{flex-basis:50%}
+.table.cbi-section-table .td[data-title]::before{content:attr(data-title);display:block;margin-bottom:var(--fs-space-1);font-size:var(--fs-type-xs);font-weight:var(--fs-eyebrow-weight);letter-spacing:var(--fs-eyebrow-tracking);color:var(--fs-eyebrow-color)}
+.table.cbi-section-table .td.cbi-section-actions{display:flex;flex:1 1 100%;justify-content:flex-start;flex-wrap:wrap;gap:var(--fs-space-2);padding-top:var(--fs-space-2)}
+.table.cbi-section-table .td.cbi-section-actions::before{content:none}
+.table.cbi-section-table .td.cbi-section-actions>*{flex:1 1 auto;display:flex;flex-wrap:wrap;gap:var(--fs-space-2)}
+.table.cbi-section-table .td .cbi-dropdown,.table.cbi-section-table .td select,.table.cbi-section-table .td input[type="text"]{width:100%;min-width:0}
+.table.cbi-section-table .td .zone-forwards{width:100%}
+}
+.table:has(.cbi-dropdown),.table:has(.cbi-dynlist){overflow:visible;border-collapse:separate;border-spacing:0}
+.modal{box-shadow:var(--fs-shadow-pop)}
+.modal>*{line-height:normal;margin-bottom:.5em;max-width:100%}
+.modal>.alert-message{margin-bottom:var(--fs-space-2)}
+.modal>pre,.modal>textarea{white-space:pre-wrap;overflow:auto;width:100%}
+.modal:has(.cbi-map){max-width:min(1100px,94vw)}
+#modal_overlay .modal label.btn{white-space:normal;overflow-wrap:anywhere;min-width:0}
+#modal_overlay .modal li{overflow-wrap:anywhere}
+#modal_overlay .uci-dialog div>ins,#modal_overlay .uci-dialog div>del,#modal_overlay .uci-dialog div>var{border-radius:var(--fs-radius-sm)}
+#modal_overlay .uci-change-legend-label>ins,#modal_overlay .uci-change-legend-label>del,#modal_overlay .uci-change-legend-label>var{border-radius:var(--fs-radius-sm);overflow:hidden}
+#modal_overlay .uci-dialog var>ins,#modal_overlay .uci-dialog var>del{border-radius:max(0px,calc(var(--fs-radius-sm) - 3px))}
+.fs-search-ov{position:fixed;inset:0;z-index:var(--fs-z-popover);display:flex;justify-content:center;align-items:flex-start;padding:max(8vh,var(--fs-space-6)) var(--fs-space-4) var(--fs-space-4);background:var(--fs-scrim);animation:fs-fade var(--fs-dur-fade)}
+.fs-search-box{display:flex;flex-direction:column;min-height:0;width:100%;max-width:560px;max-height:min(70dvh,560px);background:var(--fs-panel);border:var(--fs-hairline);border-radius:var(--fs-radius-lg);box-shadow:var(--fs-shadow-pop);overflow:hidden}
+.fs-search-row{display:flex;align-items:center;gap:var(--fs-space-2-5);flex:0 0 auto;padding:var(--fs-space-3) var(--fs-space-3-5);border-bottom:var(--fs-hairline)}
+.fs-search-ico{display:inline-flex;color:var(--fs-faint)}
+.fs-search-ico .fs-ico{width:18px;height:18px}
+.fs-search-row .fs-search-input{flex:1 1 auto;min-width:0;margin:0;padding:0;border:0;min-height:0;background:transparent;color:var(--fs-text);font-family:inherit;font-size:var(--fs-type-lg);font-weight:var(--fs-weight);line-height:1.4}
+.fs-search-row .fs-search-input:focus{outline:none;box-shadow:none}
+.fs-search-row .fs-search-input::placeholder{color:var(--fs-placeholder);font-weight:var(--fs-weight)}
+.fs-search-list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:var(--fs-space-1-5)}
+.fs-search-opt{display:flex;align-items:baseline;gap:var(--fs-space-2-5);padding:var(--fs-space-2) var(--fs-space-2-5);border-radius:var(--fs-radius);color:var(--fs-text);text-decoration:none;cursor:pointer}
+.fs-search-opt.active{background:var(--fs-accent-soft);color:var(--fs-accent)}
+.fs-search-opt-title{flex:0 1 auto;font-weight:var(--fs-weight-bold);font-size:var(--fs-type)}
+.fs-search-opt-path{flex:1 1 auto;min-width:0;color:var(--fs-faint);font-size:var(--fs-type);text-align:end;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.fs-search-opt.active .fs-search-opt-path{color:inherit}
+.fs-search-note{flex:0 0 auto;padding:var(--fs-space-2-5) var(--fs-space-3-5) var(--fs-space-0-5);color:var(--fs-eyebrow-color);font-size:var(--fs-type-xs);font-weight:var(--fs-eyebrow-weight);letter-spacing:var(--fs-eyebrow-tracking);text-transform:uppercase}
+.fs-search-box:has(.fs-search-list:empty) .fs-search-note{padding:var(--fs-space-6) var(--fs-space-3-5) var(--fs-space-7);text-align:center;text-transform:none;letter-spacing:0;font-size:var(--fs-type);font-weight:var(--fs-weight)}
+@container fs-view (max-width:360px){#view .cbi-button,#view .btn,#view input[type="button"],#view input[type="submit"],#view input[type="reset"]{white-space:normal;min-width:0;word-break:normal;overflow-wrap:break-word}
+}
+@media (max-width:767px){.cbi-dropdown{min-width:0;max-width:100%}
+:where(#view)[data-page="admin-system-package-manager"] .controls{display:block !important}
+:where(#view)[data-page="admin-system-package-manager"] .controls>div:not(.pager){display:block !important;min-width:0;margin-bottom:var(--fs-space-2)}
+:where(#view)[data-page="admin-system-package-manager"] .controls>.pager{display:flex;flex-wrap:nowrap;align-items:center;justify-content:center;gap:var(--fs-space-2)}
+:where(#view)[data-page="admin-system-package-manager"] .controls>.pager>.btn{flex:0 0 auto}
+:where(#view)[data-page="admin-system-package-manager"] .controls>.pager>.text{flex:0 1 auto;min-width:0}
+:where(#view)[data-page="admin-system-package-manager"] label + .cbi-progressbar{width:100%;margin-bottom:3em}
+:where(#view)[data-page="admin-system-package-manager"] label + .cbi-progressbar::after{top:calc(100% + 5px);inset-inline:0 auto;bottom:auto;text-align:start;white-space:normal;max-width:100%}
+:where(#view)[data-page="admin-system-package-manager"] .controls>div>label{display:block;min-width:0;white-space:normal;margin-bottom:var(--fs-space-1)}
+:where(#view)[data-page="admin-system-package-manager"] .controls>div>div:has(>label>input[type="radio"]){display:flex;flex-wrap:wrap;align-items:center;gap:var(--fs-space-1) var(--fs-space-5)}
+:where(#view)[data-page="admin-system-package-manager"] .controls .control-group{display:flex;flex-wrap:wrap;min-width:0;gap:var(--fs-space-2)}
+:where(#view)[data-page="admin-system-package-manager"] .controls .control-group input{flex:1 1 140px;min-width:0;max-width:100%}
+.cbi-value-field input,.cbi-value-field select,.cbi-value-field .cbi-dropdown{max-width:100%}
+.cbi-value-field select,.cbi-value-field input:not([type="checkbox"]):not([type="radio"]):not([type="file"]),.cbi-value-field .cbi-dropdown:not(.btn):not(.cbi-button),.cbi-value-field .cbi-dynlist,.cbi-value-field .cbi-select{width:100%}
+#view .fs-ovl .table .td,#view .fs-ovl .table .th{min-width:0}
+.cbi-value>.cbi-value-title{flex:1 1 100%;text-align:start;padding-top:0}
+.cbi-value>.cbi-value-field{flex:1 1 100%;min-width:0}
+.hide-sm,.hide-xs,.table .td.hide-sm,.table .td.hide-xs,.table .th.hide-sm,.table .th.hide-xs{display:none}
+input,textarea,select,.cbi-dropdown>ul>li{font-size:var(--fs-type-lg)}
+}
+.cbi-dropdown[open]>ul.dropdown{inset-inline-start:0 !important;inset-inline-end:auto !important;min-width:100%;width:max-content;max-width:min(92vw,420px)}
+@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}
+}
+@media (prefers-reduced-transparency:reduce){:root{--fs-photo-scrim:var(--fs-bg)}
+}
+@media (prefers-contrast:more){:root{--fs-border:color-mix(in srgb,var(--fs-text) 45%,var(--fs-bg));--fs-dim:color-mix(in srgb,var(--fs-text) 85%,var(--fs-bg));--fs-faint:color-mix(in srgb,var(--fs-text) 82%,var(--fs-bg));--fs-focus-ring:0 0 0 3px color-mix(in srgb,var(--fs-accent) 60%,transparent);--fs-accent-line:var(--fs-accent);--fs-accent-line-hi:var(--fs-accent);--fs-good-line:var(--fs-good);--fs-good-line-hi:var(--fs-good);--fs-warn-line:var(--fs-warn);--fs-warn-line-hi:var(--fs-warn);--fs-danger-line:var(--fs-danger);--fs-danger-line-hi:var(--fs-danger);--fs-placeholder-mix:62%}
+:root[data-darkmode="true"]{--fs-placeholder-mix:80%}
+}
+@media (forced-colors:active){#tabmenu ul.tabs>li.active>a,ul.cbi-tabmenu.mode>li.cbi-tab>a,.tabs>li.active>a,.cbi-tabmenu>li.cbi-tab>a,.cbi-dropdown>ul>li[selected],.ifacebox .ifacebox-head.active,.cbi-page-actions .cbi-button-apply{outline:2px solid Highlight;outline-offset:-2px;color:HighlightText;background:Highlight;forced-color-adjust:none}
+#topmenu li.active>a{border-inline-start:3px solid Highlight;padding-inline-start:calc(var(--fs-space-3) - 3px)}
+.cbi-checkbox>label[for]{border:1px solid ButtonBorder}
+.cbi-checkbox>input[type="checkbox"]:checked + label{background:Highlight;forced-color-adjust:none}
+.cbi-checkbox>label[for]::after{background:ButtonBorder}
+.cbi-checkbox>input[type="checkbox"]:checked + label::after{background:HighlightText}
+.fs-railtoggle,.fs-themerow{border-color:ButtonBorder}
+:focus-visible{outline:2px solid Highlight;outline-offset:1px}
+}
+@media print{:root{--fs-bg:#fff;--fs-panel:#fff;--fs-panel2:#f4f4f4;--fs-text:#000;--fs-dim:#333;--fs-faint:#555;--fs-border:#bbb;--fs-track:#ddd;--fs-shadow-pop:none;--fs-bar-bg:#fff}
+.fs-sidebar.fs-sidebar.fs-sidebar.fs-sidebar,.fs-skip.fs-skip,.fs-search-ov.fs-search-ov,.cbi-page-actions.cbi-page-actions,.cbi-tabmenu.cbi-tabmenu,footer{display:none}
+.fs-shell.fs-shell.fs-shell.fs-shell{height:auto;overflow:visible}
+.fs-main.fs-main.fs-main.fs-main{height:auto;overflow:visible}
+.fs-content.fs-content{max-width:none;padding:0}
+.fs-pattern.fs-pattern{display:none}
+:root[data-wallpaper="file"] :is(.fs-shell,body:not(:has(.fs-shell))){background-image:none}
+.cbi-section{break-inside:avoid}
+}
+}
+@layer page{.fs-login{width:min(400px,calc(100% - 32px));margin:7vh auto var(--fs-space-10);background:var(--fs-panel);border:var(--fs-hairline);border-radius:var(--fs-radius-lg);padding:var(--fs-space-6) var(--fs-space-6) var(--fs-space-6);box-shadow:var(--fs-shadow-pop)}
+.fs-login-host{margin:0 0 var(--fs-space-6);text-align:center;font-size:var(--fs-type-lg);font-weight:var(--fs-weight-bold);color:var(--fs-dim);overflow-wrap:anywhere}
+.fs-login .cbi-map,.fs-login .cbi-section,.fs-login .cbi-section-node{background:none;border:0;padding:0;margin:0;box-shadow:none}
+.fs-login .cbi-map>h1,.fs-login .cbi-map-descr{background:none;border:0;border-radius:0;box-shadow:none;padding:0}
+.fs-login .cbi-map>h1{font-size:var(--fs-type-xl);font-weight:var(--fs-weight-bold);letter-spacing:-.01em;margin:0 0 var(--fs-space-1-5)}
+.fs-login .cbi-map-descr{color:var(--fs-dim);font-size:var(--fs-type);margin:0 0 var(--fs-space-5)}
+.fs-login .cbi-value{display:block;border:0;padding:0 0 var(--fs-space-4);margin:0}
+.fs-login .cbi-value-title{display:block;text-align:start;width:auto;max-width:none;padding:0 0 var(--fs-space-1-5);font-size:var(--fs-type-xs);font-weight:var(--fs-eyebrow-weight);letter-spacing:var(--fs-eyebrow-tracking);text-transform:uppercase;color:var(--fs-eyebrow-color)}
+.fs-login .cbi-value-field{width:100%;margin:0;padding:0}
+.fs-login .cbi-value-field input{width:100%;margin:0;box-sizing:border-box}
+.fs-login .alert-message{margin:0 0 var(--fs-space-3-5);border-radius:var(--fs-radius)}
+.fs-login .cbi-page-actions{display:flex;flex-direction:column;gap:var(--fs-space-2-5);background:none;border:0;padding:0;margin:var(--fs-space-1-5) 0 0;text-align:start}
+.fs-login .cbi-page-actions .btn,.fs-login .cbi-page-actions input[type="submit"]{width:100%;box-sizing:border-box;min-height:var(--fs-ctl-h-lg);margin:0;border-radius:var(--fs-radius-lg);background:var(--fs-accent);color:var(--fs-on-accent);border:0;font-weight:var(--fs-weight-bold);font-size:var(--fs-type-lg)}
+.fs-login .cbi-page-actions .btn:hover{filter:var(--fs-hover-lift)}
+.cbi-title .label[data-indicator="poll-status"]{font-size:0;width:16px;height:16px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:none;border:0;color:var(--fs-faint)}
+.cbi-title .label[data-indicator="poll-status"]::after{content:"";width:7px;height:7px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;border-radius:1px;transform:rotate(45deg);margin-top:-2px;transition:transform var(--fs-dur-move)}
+.cbi-title .label[data-indicator="poll-status"][data-style="inactive"]::after{transform:rotate(-135deg);margin-top:var(--fs-space-0-5)}
+:is(.cbi-title h3,.cbi-section>h3)[role="button"]{cursor:pointer;border-radius:var(--fs-radius-sm)}
+:is(.cbi-title h3,.cbi-section>h3)[role="button"]:focus-visible{outline:none;box-shadow:var(--fs-focus-ring)}
+:where(#view)[data-page="admin-status-overview"] .cbi-section.fs-ovl-empty{display:none}
+.fs-content[data-page="admin-status-overview"] h2[name="content"]{display:none}
+.fs-ovl{display:grid;grid-template-columns:1fr 1fr;gap:var(--fs-space-4);margin-bottom:var(--fs-card-gap)}
+.fs-ovl>.fs-ovl-sys,.fs-ovl>.fs-ovl-mem,.fs-ovl>.fs-ovl-sto{margin:0;min-width:0}
+.fs-ovl>.fs-ovl-sys{grid-column:1;grid-row:1 / 3}
+.fs-ovl>.fs-ovl-mem{grid-column:2;grid-row:1}
+.fs-ovl>.fs-ovl-sto{grid-column:2;grid-row:2}
+@container fs-view (max-width:800px){.fs-ovl{grid-template-columns:1fr}
+.fs-ovl>.fs-ovl-sys,.fs-ovl>.fs-ovl-mem,.fs-ovl>.fs-ovl-sto{grid-column:1;grid-row:auto}
+}
+:where(#view)[data-page="admin-status-overview"] div:has(>.ifacebox img[src*="/port_"]){gap:var(--fs-space-2);--fs-port-min:calc(76px * var(--fs-density-type) + var(--fs-space-3) * 2);grid-template-columns:repeat(auto-fit,minmax(var(--fs-port-min),1fr)) !important;margin-bottom:0 !important}
+:where(#view)[data-page="admin-status-overview"] .ifacebox:has(img[src*="/port_"]){margin:0 !important;min-width:0 !important;max-width:200px !important;width:auto !important;justify-self:stretch;align-self:stretch;text-align:start;background:var(--fs-panel2);border:var(--fs-hairline);border-radius:var(--fs-radius-lg);padding:var(--fs-space-1-5) var(--fs-space-3);display:flex;flex-direction:row;flex-wrap:wrap;column-gap:var(--fs-space-2);row-gap:0;align-items:baseline}
+:where(#view)[data-page="admin-status-overview"] .ifacebox:has(img[src*="/port_"]) img{display:none}
+:where(#view)[data-page="admin-status-overview"] .ifacebox:has(img[src*="/port_"]) .ifacebox-head{border-bottom:0;border-radius:0}
+:where(#view)[data-page="admin-status-overview"] .ifacebox:has(img[src*="/port_"])>.ifacebox-head:nth-child(3){order:1;flex:0 0 100%;width:100%;margin:var(--fs-space-0-5) 0 var(--fs-space-1)}
+:where(#view)[data-page="admin-status-overview"] .ifacebox:has(img[src*="/port_"])>.ifacebox-head:first-child{flex:0 0 auto;width:100%;min-width:0;overflow:hidden;text-overflow:ellipsis;line-height:var(--fs-leading);font-size:var(--fs-type);font-weight:var(--fs-weight);color:var(--fs-text)}
+:where(#view)[data-page="admin-status-overview"] .ifacebox:has(img[src*="/port_"]):has(img[src*="_down.svg"])>.ifacebox-head:first-child{color:var(--fs-dim)}
+:where(#view)[data-page="admin-status-overview"] .ifacebox:has(img[src*="/port_"])>.ifacebox-body:nth-child(2){order:2;font-size:var(--fs-type);font-weight:var(--fs-weight);color:var(--fs-good);text-align:start;flex:1 1 auto;white-space:normal;overflow-wrap:break-word}
+:where(#view)[data-page="admin-status-overview"] .ifacebox:has(img[src*="/port_"])>.ifacebox-body:nth-child(2) br{display:none}
+:where(#view)[data-page="admin-status-overview"] .ifacebox:has(img[src*="/port_"]):has(img[src*="_down.svg"])>.ifacebox-body:nth-child(2){color:var(--fs-dim)}
+:where(#view)[data-page="admin-status-overview"] .ifacebox:has(img[src*="/port_"])>.ifacebox-body:nth-child(4){order:2;text-align:end;flex:1 0 100%}
+:where(#view)[data-page="admin-status-overview"] .ifacebox:has(img[src*="/port_"])>.ifacebox-body:nth-child(4)>.cbi-tooltip-container{font-family:var(--fs-font-mono);font-size:var(--fs-type-2xs) !important;color:var(--fs-dim);line-height:var(--fs-leading-tight);white-space:nowrap;text-align:end !important}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)):has(.cbi-progressbar){display:block}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar){display:grid;grid-template-columns:minmax(0,1fr);gap:var(--fs-space-1) var(--fs-space-2-5);padding:var(--fs-space-1) 0 var(--fs-space-1-5);border:0;position:relative}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar):last-child{padding-bottom:var(--fs-space-0-5)}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .td{display:block;width:auto;padding:0;border:0}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .td:first-child{grid-column:1;grid-row:1;align-self:center;min-width:0;white-space:normal;padding-inline-end:min(230px,50%);font-size:var(--fs-type)}
+@container fs-view (max-width:560px){:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .td:first-child{padding-inline-end:0}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .cbi-progressbar{margin-block-start:calc(var(--fs-type-xs) * var(--fs-leading))}
+}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr:has(.cbi-progressbar) .td:nth-child(2){grid-column:1 / -1;grid-row:2}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .td,:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .th,:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .tr{border-bottom:0}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .cbi-value{border-bottom:0}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .td:first-child{font-weight:var(--fs-weight-normal);color:var(--fs-dim)}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .ifacebox .ifacebox-head.active{background:var(--fs-panel2);color:var(--fs-good);font-weight:var(--fs-weight)}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .ifacebox-body strong{font-weight:var(--fs-weight-normal);color:var(--fs-dim)}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .table:not(:has(.tr.table-titles)) .cbi-progressbar::after{font-size:var(--fs-type);color:var(--fs-text)}
+:where(#view)[data-page="admin-status-overview"] .cbi-section .table:has(.tr.table-titles) .td,:where(#view)[data-page="admin-status-overview"] .cbi-section .table:has(.tr.table-titles) .th{padding-block:var(--fs-space-1-5)}
+:where(#view)[data-page="admin-system-package-manager"] .controls>div:first-child{position:relative;display:block;flex:1 1 100%;width:100%;margin:0 0 var(--fs-space-1-5)}
+#packages:not(.fs-stacked) .tr>.td:first-child{white-space:nowrap}
+#packages.fs-stacked .tr:not(.cbi-section-table-titles):not(.cbi-section-table-descr){display:block;padding:var(--fs-space-3) var(--fs-space-4)}
+#packages.fs-stacked .td{display:flex;align-items:baseline;justify-content:flex-end;gap:0;padding:var(--fs-space-1) 0;border:0;text-align:end;flex:none}
+#packages.fs-stacked .td[data-title]::before{content:attr(data-title);flex:0 0 auto;text-align:start;margin-bottom:0;margin-inline-end:auto;padding-inline-end:var(--fs-space-3-5);font-size:var(--fs-type-xs);font-weight:var(--fs-eyebrow-weight);letter-spacing:var(--fs-eyebrow-tracking);color:var(--fs-eyebrow-color);white-space:nowrap}
+#packages.fs-stacked .td:nth-child(4){display:block;text-align:start}
+#packages.fs-stacked .td:nth-child(4)::before{display:block;margin-bottom:var(--fs-space-1)}
+#packages.fs-stacked .td.cbi-section-actions{justify-content:flex-end;margin-top:var(--fs-space-1-5)}
+#packages.fs-stacked .td.cbi-section-actions::before,#packages.fs-stacked .td:not([data-title])::before{content:none}
+:where(#view)[data-page="admin-system-package-manager"] label:has(+ .cbi-progressbar){display:block;margin:0;font-size:var(--fs-type);font-weight:var(--fs-weight);color:var(--fs-dim)}
+:where(#view)[data-page="admin-system-admin-sshkeys"]>div{background:var(--fs-panel);border:var(--fs-hairline);border-radius:var(--fs-radius-lg);padding:var(--fs-space-4)}
+:where(#view)[data-page="admin-system-admin-sshkeys"] .cbi-dynlist{max-width:none}
+#view table.fs-dt:is(.leases,.lases,.leases6):not(.fs-stacked) .td{white-space:nowrap}
+#view table.fs-dt:is(.leases,.lases):has(:where(.tr.table-titles>:nth-child(6))):not(.fs-stacked) .td:is(:nth-last-child(7),:nth-last-child(4)),#view table.fs-dt.leases6:has(:where(.tr.table-titles>:nth-child(6))):not(.fs-stacked) .td:is(:nth-last-child(6),:nth-last-child(5),:nth-last-child(4)),#view table.fs-dt:is(.leases,.lases):not(:has(:where(.tr.table-titles>:nth-child(6)))):not(.fs-stacked) .td:nth-last-child(5),#view table.fs-dt.leases6:not(:has(:where(.tr.table-titles>:nth-child(6)))):not(.fs-stacked) .td:is(:nth-last-child(5),:nth-last-child(4),:nth-last-child(3)){white-space:normal}
+#content_syslog>div:has(>label){display:flex;flex-wrap:wrap;align-items:center;row-gap:var(--fs-space-2)}
+#content_syslog>div:has(>label)>label{white-space:nowrap}
+#content_syslog>div:has(>label)>:is(select,input){flex:0 1 auto;min-width:0;max-width:100%}
+@container fs-view (width>560px){#content_syslog>div:has(>label)>input:not([type]){flex:1 1 12rem}
+}
+@container fs-view (width <= 560px){#content_syslog>div:has(>label)>:is(select,input:not([type="checkbox"])){flex-basis:100%}
+}
+#syslog{width:100%;min-height:500px;color:var(--fs-text);margin-bottom:var(--fs-space-4);font-family:var(--fs-font-mono);font-size:var(--fs-type);line-height:var(--fs-leading);white-space:pre;resize:vertical}
+.fs-ap{display:flex;flex-direction:column;gap:var(--fs-space-5)}
+.fs-ap-note p:first-child{margin-top:0}
+.fs-ap-note p:last-child{margin-bottom:0}
+button.fs-ap-fold{display:flex;align-items:center;justify-content:space-between;gap:var(--fs-space-3);inline-size:100%;padding:0;margin:0;background:none;border:0;cursor:pointer;text-align:start;color:inherit}
+button.fs-ap-fold:hover h4{color:var(--fs-accent)}
+button.fs-ap-fold:focus-visible{outline:none;box-shadow:var(--fs-focus-ring-solo);border-radius:var(--fs-radius-sm)}
+.fs-ap-chev{flex:0 0 auto;inline-size:26px;block-size:26px;display:inline-flex;align-items:center;justify-content:center;color:var(--fs-dim);border-radius:var(--fs-radius-sm)}
+.fs-ap-chev::after{content:"";width:7px;height:7px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;border-radius:1px;transform:rotate(45deg);margin-top:-2px;transition:transform var(--fs-dur-move)}
+button.fs-ap-fold[aria-expanded="true"] .fs-ap-chev::after{transform:rotate(-135deg);margin-top:var(--fs-space-0-5)}
+button.fs-ap-fold:hover .fs-ap-chev{color:var(--fs-accent)}
+.fs-ap-sub{margin-top:var(--fs-space-4)}
+.fs-ap-head h4{margin:0;font-size:var(--fs-type);font-weight:var(--fs-weight-bold);color:var(--fs-text)}
+.fs-ap-section + .fs-ap-section{padding-top:var(--fs-space-4);border-top:var(--fs-hairline)}
+:root[data-layout="top"] .fs-ap-submenus{display:none}
+.fs-ap-actrow{display:flex;flex-wrap:wrap;gap:var(--fs-space-2)}
+.fs-ap-actgap{flex:0 0 var(--fs-space-4)}
+.fs-ap-actrow .btn{font-size:var(--fs-type);padding:var(--fs-space-2) var(--fs-space-2-5);cursor:pointer}
+.fs-ap-actrow .btn:disabled{cursor:default}
+.fs-ap-armed{color:var(--fs-on-danger);background:var(--fs-danger);border-color:transparent}
+.fs-ap-err{margin-top:var(--fs-space-2);font-size:var(--fs-type-xs);line-height:1.35;color:var(--fs-danger)}
+.fs-colorctl{display:flex;flex-direction:column;gap:var(--fs-space-1-5);max-width:var(--fs-field-max)}
+.fs-color-row{display:flex;align-items:center;gap:var(--fs-space-2)}
+input[type="color"].fs-color-swatch{flex:0 0 auto;inline-size:34px;block-size:34px;padding:0;cursor:pointer;background:none;border:var(--fs-hairline);border-radius:var(--fs-radius-sm)}
+input[type="color"].fs-color-swatch::-webkit-color-swatch-wrapper{padding:0}
+input[type="color"].fs-color-swatch::-webkit-color-swatch{border:0;border-radius:calc(var(--fs-radius-sm) - 1px)}
+input[type="color"].fs-color-swatch::-moz-color-swatch{border:0;border-radius:calc(var(--fs-radius-sm) - 1px)}
+input[type="text"].fs-color-hex{flex:1 1 auto;min-inline-size:0;font-family:var(--fs-font-mono);font-size:var(--fs-type-xs);font-variant-numeric:tabular-nums;text-transform:lowercase}
+.fs-color-clear{flex:0 0 auto;font-size:var(--fs-type-xs);padding:var(--fs-space-1-5) var(--fs-space-2);cursor:pointer}
+.fs-color-clear:disabled{cursor:default}
+.fs-color-contrast{font-variant-numeric:tabular-nums}
+.fs-contrast-aa{color:var(--fs-good)}
+.fs-contrast-aa-large{color:var(--fs-warn)}
+.fs-contrast-low{color:var(--fs-danger)}
+.fs-ap-bgprev{display:block;width:100%;max-width:var(--fs-field-max);height:84px;object-fit:cover;border:var(--fs-hairline);border-radius:var(--fs-radius);margin-top:var(--fs-space-2)}
+.fs-ap-bgrow{display:flex;flex-wrap:wrap;gap:var(--fs-space-2)}
+.fs-ap-bgrow .btn{font-size:var(--fs-type);padding:var(--fs-space-2) var(--fs-space-2-5);cursor:pointer}
+.fs-ap-bgrow .btn:disabled{cursor:default}
+:root[data-wallpaper="file"] .fs-ap-tint{display:none}
+:root:not([data-tint="hue"]) .fs-ap-tintstr{display:none}
+.fs-ap-footer{display:flex;flex-direction:column;gap:var(--fs-space-2);margin-top:var(--fs-space-0-5);padding-top:var(--fs-space-2-5);border-top:var(--fs-hairline)}
+.fs-ap-verrow{display:flex;align-items:center;justify-content:space-between;gap:var(--fs-space-2-5)}
+.fs-ap-version{position:relative;font-size:var(--fs-type-xs);color:var(--fs-dim);font-variant-numeric:tabular-nums;text-decoration:none}
+.fs-ap-version:hover{color:var(--fs-accent);text-decoration:underline}
+.fs-ap-version::before{content:"";position:absolute;inset:-4px -2px}
+.fs-ap-i18n{flex-wrap:wrap;font-size:var(--fs-type-xs);color:var(--fs-dim)}
+.fs-ap-i18n code{font-size:inherit}
+}
+
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js
index 601e28c16..65352fa30 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js
@@ -56,10 +56,7 @@ function probeColor(expr) {
 		 * shared with `luci-app-*`, and an app's unlayered `span { color: … !important }` outranks
 		 * a layer and a plain inline style alike. A probe that loses its own colour reports the
 		 * app's, which then becomes the admin's saved axis on the next confirm. */
-		/* the id is this probe's escape from fs-router.js's strayBodyNode() (issue #56): it is
-		 * appended once and never removed, so without an fs-* id it would read as body litter and
-		 * force every navigation away from this page into a full load. */
-		_probe = E('span', { 'id': 'fs-appearance-probe', 'aria-hidden': 'true' });
+		_probe = E('span', { 'aria-hidden': 'true' });
 		_probe.style.cssText = 'position:fixed!important;left:-9999px!important;top:0!important;'
 			+ 'width:0!important;height:0!important;overflow:hidden!important;'
 			+ 'pointer-events:none!important;';
@@ -76,28 +73,37 @@ function probeColor(expr) {
  * the space it was authored in, so `oklch(0.54 0.19 300)` would parse as three numbers in the
  * wrong units and produce a colour nobody chose — measured: #010078, graded "Too faint to read",
  * in the hex field, the swatch and the contrast readout alike. Painting one pixel makes the engine
- * convert instead (tools/export-tier.mjs uses the same method). Canvas 2D is universal on every
- * engine 24.10 ships against, so there is no second path. */
+ * convert instead (tools/export-tier.mjs uses the same method). The string parse remains only as
+ * the fallback for an engine with no 2D context, where only the legacy `rgb()`/`color(srgb …)`
+ * forms can appear. */
 let _cx = null;
 function rasterCtx() {
-	if (!_cx) {
+	if (_cx !== null) return _cx;
+	try {
 		const cv = document.createElement('canvas');
 		cv.width = cv.height = 1;
-		_cx = cv.getContext('2d', { willReadFrequently: true });
-	}
+		_cx = cv.getContext('2d', { willReadFrequently: true }) || false;
+	} catch (e) { _cx = false; }
 	return _cx;
 }
 function parseColor(s) {
+	const str = String(s || '');
 	const cx = rasterCtx();
-	/* fillStyle keeps the last value it could parse, so a colour this engine rejects would
-	 * report the previous one as a fresh reading — the trap probeColor() clears for */
-	cx.fillStyle = '#000';
-	cx.fillStyle = String(s || '');
-	cx.clearRect(0, 0, 1, 1);
-	cx.fillRect(0, 0, 1, 1);
-	const d = cx.getImageData(0, 0, 1, 1).data;
-	/* translucent: composite over nothing is meaningless for a readout */
-	return d[3] === 255 ? [ d[0], d[1], d[2] ] : null;
+	if (cx) {
+		/* fillStyle keeps the last value it could parse, so a colour this engine rejects would
+		 * report the previous one as a fresh reading — the trap probeColor() clears for */
+		cx.fillStyle = '#000';
+		cx.fillStyle = str;
+		cx.clearRect(0, 0, 1, 1);
+		cx.fillRect(0, 0, 1, 1);
+		const d = cx.getImageData(0, 0, 1, 1).data;
+		if (d[3] === 255) return [ d[0], d[1], d[2] ];
+		/* translucent: composite over nothing is meaningless for a readout, so fall through */
+	}
+	const nums = str.match(/[\d.]+/g);
+	if (!nums || nums.length < 3) return null;
+	const unit = (/^color\(/i).test(str) ? 255 : 1;
+	return nums.slice(0, 3).map((n) => Math.max(0, Math.min(255, parseFloat(n) * unit)));
 }
 
 /* WCAG 2.x relative luminance and contrast ratio, on sRGB. Used only to report: the theme states
@@ -135,8 +141,8 @@ function toHex(s) {
  * `opts.probe` is the live token the effective colour is read back from, so the field shows the
  * palette's colour while the axis is off without a copy of the palette in JS. `opts.contrast` is
  * the pair whose ratio is reported under the row. */
-function colorControl(onPick, label, opts) {
-	const o = opts;
+function colorControl(current, onPick, label, opts) {
+	const o = opts || {};
 
 	/* type=color leaves the picker to the browser: accessible without reimplementing a colour
 	 * wheel, and native on a phone. The text field beside it takes a pasted hex and is the
@@ -149,6 +155,10 @@ function colorControl(onPick, label, opts) {
 	const clear = E('button', { 'class': 'btn fs-color-clear', 'type': 'button' }, [ _('Palette', 'footstrap') ]);
 	const ratio = o.contrast ? E('div', { 'class': 'cbi-value-description fs-color-contrast' }) : null;
 
+	/* what the axis holds right now: the page can change it behind this control (a preset, Reset
+	 * to default), so a private copy would go stale. `current` is only the build-time value. */
+	const currentOf = o.read || (() => current);
+
 	/* Repaint everything that mirrors the axis. Called after every edit, and through the returned
 	 * refresh() after a preset, palette switch or dark-mode flip — each changes what the palette's
 	 * own colour is while this axis stays off. */
@@ -197,9 +207,7 @@ function colorControl(onPick, label, opts) {
 	const commit = () => {
 		const v = field.value.trim().toLowerCase();
 		if ((/^#[0-9a-f]{6}$/).test(v)) pick(v);
-		/* re-read rather than a cached copy: the page can change what the axis holds behind this
-		 * control (a preset, Reset to default), which a snapshot taken at build time would miss */
-		else reflect(o.read());
+		else reflect(currentOf());
 	};
 	field.addEventListener('blur', commit);
 	field.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); commit(); } });
@@ -210,7 +218,7 @@ function colorControl(onPick, label, opts) {
 	].concat(ratio ? [ ratio ] : []));
 	/* the caller decides when this runs: probeColor() needs the document, and this control is not
 	 * in it yet */
-	wrap.fsRefresh = () => reflect(o.read());
+	wrap.fsRefresh = () => reflect(currentOf());
 	return wrap;
 }
 
@@ -301,15 +309,14 @@ function build() {
 		return node;
 	};
 
-	/* one colour axis: `axis` is the `{current, apply}` object fs-axes.js exports it as; `probe` is
-	 * the live token the control reads the effective colour back from, `contrast` the pair it
-	 * reports */
-	const colourGroup = (label, axis, probe, contrast, opts = {}) => group(label, (lbl) => {
-		const ctl = colorControl(bump(axis.apply), lbl, {
+	/* one colour axis: `probe` is the live token the control reads the effective colour back from,
+	 * `contrast` the pair it reports */
+	const colourGroup = (label, axis, probe, contrast, opts) => group(label, (lbl) => {
+		const ctl = colorControl(axis.current(), bump(axis.apply), lbl, {
 			probe: probe,
 			read: axis.current,
 			contrast: contrast,
-			cls: opts.cls
+			cls: (opts && opts.cls) || ''
 		});
 		colourCtls.push(ctl);
 		return ctl;
@@ -337,7 +344,7 @@ function build() {
 			dark:  _('Dark', 'footstrap')
 		}, bump(repaint(prefs.applyMode)), label)),
 
-		group(_('Palette', 'footstrap'), (label) => selectCtl(axes.palette.current(), {
+		group(_('Palette', 'footstrap'), (label) => selectCtl(axes.currentPalette(), {
 			footstrap:  'Footstrap',
 			hicontrast: 'Hi-Contrast',
 			/* names the OTHER package, luci-theme-bootstrap, whose colours this palette is —
@@ -348,7 +355,7 @@ function build() {
 			/* names the OpenWrt forum (forum.openwrt.org), whose Discourse colourway this is —
 			 * a proper noun like the three above it, not the English common noun "forum" */
 			forum:      'Forum'
-		}, bump(repaint(axes.palette.apply)), label)),
+		}, bump(repaint(axes.applyPalette)), label)),
 
 		group(_('Density', 'footstrap'), (label) => selectCtl(prefs.currentDensity(), {
 			compact: _('Compact', 'footstrap'),
@@ -363,11 +370,11 @@ function build() {
 		 * --fs-content-min (500px, what the sidebar-to-bar fold is measured against) out of reach
 		 * by construction rather than by a rule someone has to remember. */
 		group(_('Content width', 'footstrap'),
-			(label) => sliderCtl(axes.contentWidth.current(), 1280, 3840,
-				bump(axes.contentWidth.apply), label, { step: 40, live: false })),
+			(label) => sliderCtl(axes.currentContentWidth(), 1280, 3840,
+				bump(axes.applyContentWidth), label, { step: 40, live: false })),
 
 		group(_('Rounding', 'footstrap'),
-			(label) => sliderCtl(axes.radius.current(), 0, 20, bump(axes.radius.apply), label)),
+			(label) => sliderCtl(axes.currentRadius(), 0, 20, bump(axes.applyRadius), label)),
 
 		/* The top layout has no accordion, so this switch is meaningless there: always built,
 		 * hidden by CSS (:root[data-layout="top"] .fs-ap-submenus). Do not wrap it in an
@@ -385,7 +392,9 @@ function build() {
 	const colours = [
 		/* the caption says what the axis is for: "Tint" alone reads as decoration, and nobody
 		 * would look for the router-identity cue under it */
-		colourGroup(_('Tint (router identification)', 'footstrap'), axes.tint, 'var(--fs-bg)', {
+		colourGroup(_('Tint (router identification)', 'footstrap'), {
+			current: axes.currentTint, apply: axes.applyTint
+		}, 'var(--fs-bg)', {
 			/* the canvas is the one axis with no derived ink: its text is --fs-text, a palette
 			 * token this axis must not move, so the ratio is reported instead of corrected */
 			fg: 'var(--fs-text)', bg: 'var(--fs-bg)', label: _('on the canvas', 'footstrap')
@@ -397,7 +406,7 @@ function build() {
 		 * Not called "Density": that is the select above, and this string is both the caption and
 		 * the aria-label, so a screen reader would announce two rows under one name. */
 		group(_('Tint strength', 'footstrap'),
-			(label) => sliderCtl(axes.tintStrength.current(), 0, 200, bump(repaint(axes.tintStrength.apply)), label, {
+			(label) => sliderCtl(axes.currentTintStrength(), 0, 200, bump(repaint(axes.applyTintStrength)), label, {
 				step: 5
 			}), { cls: 'fs-ap-tint fs-ap-tintstr' }),
 
@@ -410,12 +419,12 @@ function build() {
 		 * eight times between here and the surfaces below, they cost their repeated literals in
 		 * full — a string is not mangled — so the rows are data and the row is stated once. */
 		...[
-			[ _('Accent', 'footstrap'),  axes.accent, 'var(--fs-accent)' ],
-			[ _('Good', 'footstrap'),    axes.good,   'var(--fs-good)' ],
-			[ _('Warning', 'footstrap'), axes.warn,   'var(--fs-warn)' ],
-			[ _('Danger', 'footstrap'),  axes.danger, 'var(--fs-danger)' ]
-		].map(([ label, axis, ink ]) =>
-			colourGroup(label, axis, ink, { fg: ink, bg: CARD_BG, label: ON_CARD }))
+			[ _('Accent', 'footstrap'),  axes.currentAccent, axes.applyAccent, 'var(--fs-accent)' ],
+			[ _('Good', 'footstrap'),    axes.currentGood,   axes.applyGood,   'var(--fs-good)' ],
+			[ _('Warning', 'footstrap'), axes.currentWarn,   axes.applyWarn,   'var(--fs-warn)' ],
+			[ _('Danger', 'footstrap'),  axes.currentDanger, axes.applyDanger, 'var(--fs-danger)' ]
+		].map(([ label, current, apply, ink ]) =>
+			colourGroup(label, { current, apply }, ink, { fg: ink, bg: CARD_BG, label: ON_CARD }))
 	];
 
 	/* ---- the surfaces: the sheet the UI is drawn on ----
@@ -430,12 +439,12 @@ function build() {
 		/* Same rows, one column wider: a surface reports the ink read ON it, which is --fs-text
 		 * for the three that carry body text and the hairline itself for the border. */
 		...[
-			[ _('Cards', 'footstrap'),           axes.card,    CARD_BG,             INK,                  CARD_BG,             ON_CARD ],
-			[ _('Controls', 'footstrap'),        axes.control, 'var(--fs-panel2)',  INK,                  'var(--fs-panel2)',  _('on a control', 'footstrap') ],
-			[ _('Sidebar and bar', 'footstrap'), axes.bar,     'var(--fs-bar-bg)',  INK,                  'var(--fs-bar-bg)',  _('in the sidebar', 'footstrap') ],
-			[ _('Borders', 'footstrap'),         axes.line,    'var(--fs-border)',  'var(--fs-border)',   CARD_BG,             ON_CARD, 'shape' ]
-		].map(([ label, axis, probe, fg, bg, where, kind ]) =>
-			colourGroup(label, axis, probe, { fg, bg, label: where, kind }))
+			[ _('Cards', 'footstrap'),           axes.currentCard,    axes.applyCard,    CARD_BG,             INK,                  CARD_BG,             ON_CARD ],
+			[ _('Controls', 'footstrap'),        axes.currentControl, axes.applyControl, 'var(--fs-panel2)',  INK,                  'var(--fs-panel2)',  _('on a control', 'footstrap') ],
+			[ _('Sidebar and bar', 'footstrap'), axes.currentBar,     axes.applyBar,     'var(--fs-bar-bg)',  INK,                  'var(--fs-bar-bg)',  _('in the sidebar', 'footstrap') ],
+			[ _('Borders', 'footstrap'),         axes.currentLine,    axes.applyLine,    'var(--fs-border)',  'var(--fs-border)',   CARD_BG,             ON_CARD, 'shape' ]
+		].map(([ label, current, apply, probe, fg, bg, where, kind ]) =>
+			colourGroup(label, { current, apply }, probe, { fg, bg, label: where, kind }))
 	];
 
 	/* ---- section 3: the wallpaper and the rows each value brings ----
@@ -484,30 +493,30 @@ function build() {
 			group(_('Pattern', 'footstrap'),
 				() => E('div', { 'class': 'fs-ap-bgrow' }, [ patChoose, patRemove ]),
 				{ extra: [ patInput, patPreview, patErr ] }),
-			group(scaleLabel, (lbl) => sliderCtl(axes.patternSize.current(), 40, 1600,
-				bump(axes.patternSize.apply), lbl, { step: 20 })),
-			group(strengthLabel, (lbl) => sliderCtl(axes.patternStrength.current(), 0, 100,
-				bump(axes.patternStrength.apply), lbl, { step: 5 })),
-			group(inkLabel, (lbl) => selectCtl(axes.patternInk.current(), {
+			group(scaleLabel, (lbl) => sliderCtl(axes.currentPatternSize(), 40, 1600,
+				bump(axes.applyPatternSize), lbl, { step: 20 })),
+			group(strengthLabel, (lbl) => sliderCtl(axes.currentPatternStrength(), 0, 100,
+				bump(axes.applyPatternStrength), lbl, { step: 5 })),
+			group(inkLabel, (lbl) => selectCtl(axes.currentPatternInk(), {
 				theme:    _('Theme', 'footstrap'),
 				original: _('As in file', 'footstrap')
-			}, bump(axes.patternInk.apply), lbl))
+			}, bump(axes.applyPatternInk), lbl))
 		];
 		/* …and the rows the FILE photo brings. */
 		const fileRows = [
 			group(_('File', 'footstrap'),
 				() => E('div', { 'class': 'fs-ap-bgrow' }, [ chooseBtn, removeBtn ]),
 				{ extra: [ fileInput, preview, err ] }),
-			group(dimLabel, (lbl) => sliderCtl(axes.photoDim.current(), 0, 100,
-				bump(axes.photoDim.apply), lbl, { step: 5 }))
+			group(dimLabel, (lbl) => sliderCtl(axes.currentPhotoDim(), 0, 100,
+				bump(axes.applyPhotoDim), lbl, { step: 5 }))
 		];
 
 		function reflect(tok) {
-			if (tok) { preview.src = axes.loginBg.url(tok); preview.hidden = false; removeBtn.hidden = false; }
+			if (tok) { preview.src = axes.loginBgUrl(tok); preview.hidden = false; removeBtn.hidden = false; }
 			else { preview.removeAttribute('src'); preview.hidden = true; removeBtn.hidden = true; }
 		}
 		function reflectPattern(tok) {
-			if (tok) { patPreview.src = axes.pattern.url(tok); patPreview.hidden = false; patRemove.hidden = false; }
+			if (tok) { patPreview.src = axes.patternUrl(tok); patPreview.hidden = false; patRemove.hidden = false; }
 			else { patPreview.removeAttribute('src'); patPreview.hidden = true; patRemove.hidden = true; }
 		}
 		/* `hidden` on the row, which 80-appearance.css restates at a specificity beating
@@ -518,11 +527,11 @@ function build() {
 			patRows.forEach((r) => { r.hidden = (v !== 'pattern'); });
 			fileRows.forEach((r) => { r.hidden = (v !== 'file'); });
 		}
-		reflect(axes.loginBg.current());
-		reflectPattern(axes.pattern.current());
-		togglePanel(axes.wallpaper.current());
+		reflect(axes.currentLoginBg());
+		reflectPattern(axes.currentPattern());
+		togglePanel(axes.currentWallpaper());
 
-		const setWallpaper = (v) => { axes.wallpaper.apply(v); refreshSave(); togglePanel(v); refreshColours(); };
+		const setWallpaper = (v) => { axes.applyWallpaper(v); refreshSave(); togglePanel(v); refreshColours(); };
 
 		/* Both uploads present the same three controls and the same four states — pick, upload,
 		 * report, remove — so the wiring is stated once. What differs is `after`: the pattern also
@@ -570,7 +579,7 @@ function build() {
 
 		let seg;
 		const wallRow = group(_('Wallpaper', 'footstrap'), (label) => {
-			seg = selectCtl(axes.wallpaper.current(), {
+			seg = selectCtl(axes.currentWallpaper(), {
 				off:     _('Off', 'footstrap'),
 				pattern: _('Pattern', 'footstrap'),
 				file:    _('File', 'footstrap')
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-assets.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-assets.js
index 78497335a..c7cbaf2c9 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-assets.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-assets.js
@@ -12,7 +12,7 @@
  * write fails after the bytes have landed.
  *
  * It is a module of its own because of WHERE it is needed: this machinery is reached only from the
- * Appearance tab, one page out of nearly two hundred, and it was ~4 KB of DOMParser, canvas and rpc
+ * Appearance page, one out of nearly two hundred, and it was ~4 KB of DOMParser, canvas and rpc
  * plumbing downloaded to a router's browser on the way to the DHCP page.
  *
  * The token accessors and the two live appliers live in `fs-axes` beside the axes themselves — the
@@ -212,8 +212,20 @@ function _sanitizeSvg(text) {
 	 * (Chromium and WebKit decline to run XSLT on an image/svg+xml document). A tile has no use for
 	 * one, and without the PI the embedded stylesheet is never applied. */
 
-	/* An external-DTD DOCTYPE prologue is left unvisited on purpose: verified-safe residual finding,
-	 * docs/architecture.md "The SVG sanitizer's residual, verified-safe findings". */
+	/* Residual, verified NOT exploitable here (2026-09 security review): a prologue of
+	 * `` is a document-level construct this walk
+	 * never visits — `doc.doctype` is left as parsed, the same as everything else here that is not an
+	 * element or an attribute — and it survives `XMLSerializer` byte for byte on the same round trip
+	 * that runs below when anything else on the document needs cleaning. It costs nothing today
+	 * because no shipping engine resolves an external DTD for any document at all, `image/svg+xml`
+	 * included (external entity resolution has been off since the XXE era); a browser that ever did
+	 * would only reach that fetch by opening the file directly, which the CGI answers with
+	 * `Content-Security-Policy: default-src 'none'; sandbox` — every other consumer sees this bundle
+	 * only as a `mask-image`/`background-image`/`` source, none of which parses a DOCTYPE at all.
+	 * Not closed: dropping `doc.doctype` would carve out one more node type in a walk that otherwise
+	 * judges only elements and their attributes, for a fetch nothing in this project's support matrix
+	 * performs. Starts mattering the day some consumer of this bundle DOES resolve an external DTD, or
+	 * the file is ever served as a navigable document without that header. */
 	for (const n of [ ...doc.childNodes ])
 		if (n.nodeType === Node.PROCESSING_INSTRUCTION_NODE) { n.remove(); elements++; }
 
@@ -253,9 +265,21 @@ function _sanitizeSvg(text) {
 			 * one the spec gives them — a change to how SVG itself is interpreted, not something a
 			 * check here could see coming or catch. */
 			if ((/^on[a-z]+$/).test(n)) { el.removeAttribute(a.name); refs++; continue; }
-			/* A TAB-obfuscated `javascript:` (`href="javas	cript:alert(1)"`) passes this regex:
-			 * verified-safe residual finding, docs/architecture.md "The SVG sanitizer's residual,
-			 * verified-safe findings". */
+			/* Residual, verified NOT exploitable here (2026-09 security review): XML attribute-value
+			 * normalisation does not fold a numeric character reference the way HTML does, so
+			 * `href="javas	cript:alert(1)"` reaches `v` as a DOM string with a LITERAL TAB still in
+			 * it — `/^javascript:/i` does not match a value starting with a control character, and the
+			 * fixture round-trips through `XMLSerializer` byte for byte, so it survives both this
+			 * filter and the serializer. It still resolves to `javascript:` in a real browser, because
+			 * a URL parser strips TAB/LF/CR from the scheme before comparing it (WHATWG URL, "scheme
+			 * start state") — the same class of hole every naive `^scheme:` regex has. Not closed:
+			 * matching every control character a URL parser discards is the grammar-guessing this file
+			 * exists to avoid, for a value that never resolves as a URL here in the first place — this
+			 * bundle only ever leaves the router as `mask-image`/`background-image`/``, none of
+			 * which reads `href` as navigable, and the CGI serving it back for a direct open sends
+			 * `Content-Security-Policy: default-src 'none'; sandbox` regardless. Starts mattering the
+			 * day the tile is served without that header, or inlined into a page rather than
+			 * referenced as an image. */
 			if ((/^javascript:/i).test(v)) { el.removeAttribute(a.name); refs++; continue; }
 			/* Security review finding (LOW): `xml:base` (XML Base, W3C) is not itself a reference —
 			 * it changes what every RELATIVE one in the subtree UNDER IT resolves against. A perfectly
@@ -272,9 +296,22 @@ function _sanitizeSvg(text) {
 			 * prefix to the XML namespace just to carry it is a narrower attack this does not chase,
 			 * consistent with this file avoiding a second grammar it would have to maintain. */
 			if (n === 'xml:base') { el.removeAttribute(a.name); refs++; continue; }
-			/* A presentation attribute's `url(` (`fill`, `stroke`, `filter`, …) is only checked below
-			 * for `style`, not here: verified-safe residual finding, docs/architecture.md "The SVG
-			 * sanitizer's residual, verified-safe findings". */
+			/* Residual, verified NOT exploitable here (2026-09 security review): the branch below is
+			 * the ONLY place a `url(` is ever inspected. A PRESENTATION ATTRIBUTE that takes the same
+			 * function — `fill`, `stroke`, `filter`, `clip-path`, `mask`, `cursor` (SVG 1.1 §11, §15) —
+			 * is a plain attribute value to this walk, not a CSS declaration, so `` matches nothing above and passes with its ORIGINAL
+			 * bytes untouched. What it can do is start a second fetch — no script, no navigation,
+			 * exactly the reach `_externalStyleRef` already closes for `style` — and that fetch runs
+			 * only where the SVG is rendered with its own resource-loading context, which this bundle
+			 * never is: it is only ever a `mask-image`/`background-image`/`` source, and a direct
+			 * open is answered by the CGI's `Content-Security-Policy: default-src 'none'; sandbox`
+			 * regardless. Not closed: doing here what `_sanitizeStyleValue` does for `style` means the
+			 * same check against every presentation attribute the SVG spec lets carry a `url()`, a list
+			 * this file would then have to track as the spec grows it — the grammar this file exists to
+			 * avoid guessing at, moved from one attribute to a dozen. Starts mattering the day a
+			 * presentation `url()` gets a fetch path this parser-only pass does not already block by
+			 * other means, or the tile stops being image-only. */
 			if (n === 'style') {
 				const cleaned = _sanitizeStyleValue(v);
 				if (cleaned.removed) {
@@ -300,6 +337,17 @@ function _sanitizeSvg(text) {
 	return { text: new XMLSerializer().serializeToString(doc), elements, refs };
 }
 
+/* read the picked file as text so it can be inspected before upload, and so what reaches the
+ * router is exactly the bytes that were checked */
+function _readText(file) {
+	return new Promise((resolve, reject) => {
+		const fr = new FileReader();
+		fr.onload = () => resolve(String(fr.result || ''));
+		fr.onerror = () => reject(new Error(_('That file could not be read.', 'footstrap')));
+		fr.readAsText(file);
+	});
+}
+
 /* ---- login/page background upload: router-side, and deliberately not an axis ----
  * The other axes are per-browser with a router default; this one has no browser layer. An admin
  * uploads an image once, it becomes the router-wide background for every device and shows
@@ -442,7 +490,7 @@ function assetAxis(o) {
 				.catch((e) => _rollbackUpload(o.path, e))
 				.then(() => {
 					/* switch this browser onto it: the ordinary axis path, localStorage only */
-					axes.wallpaper.apply(o.wallpaper);
+					axes.applyWallpaper(o.wallpaper);
 					o.apply(tok);
 					return tok;
 				});
@@ -462,16 +510,13 @@ function assetAxis(o) {
  * back a raster, so the parsed-document check above stands in for it. */
 const PATTERN = assetAxis({
 	path: PAT_PATH, filename: 'pattern.svg', field: 'pattern', wallpaper: 'pattern',
-	apply: (tok) => axes.pattern.apply(tok),
+	apply: (tok) => axes.applyPattern(tok),
 	prepare: (file) => {
 		if (!file) return Promise.reject(new Error(MSG_PICK_SVG));
 		const isSvg = (/(^image\/svg\+xml$)/i).test(file.type || '') || (/\.svg$/i).test(file.name || '');
 		if (!isSvg) return Promise.reject(new Error(MSG_PICK_SVG));
 		if (file.size > PAT_MAX) return Promise.reject(new Error(_('That file is too large.', 'footstrap')));
-		/* the catch keeps the same message, so what reaches the router is still exactly the bytes
-		 * that were checked */
-		const readErr = () => Promise.reject(new Error(_('That file could not be read.', 'footstrap')));
-		return file.text().catch(readErr).then((text) => {
+		return _readText(file).then((text) => {
 			const cleaned = _sanitizeSvg(text);
 			if (cleaned.error) return Promise.reject(new Error(cleaned.error));
 			/* Told once, here, regardless of whether the upload that follows succeeds — what left
@@ -490,7 +535,7 @@ const PATTERN = assetAxis({
  * grant for BG_PATH. */
 const LOGIN_BG = assetAxis({
 	path: BG_PATH, filename: 'login-bg', field: 'login_bg', wallpaper: 'file',
-	apply: (tok) => axes.loginBg.apply(tok),
+	apply: (tok) => axes.applyLoginBg(tok),
 	prepare: (file) => {
 		if (!file || !(/^image\//).test(file.type || ''))
 			return Promise.reject(new Error(_('Please choose an image file.', 'footstrap')));
@@ -506,5 +551,4 @@ return baseclass.extend({
 	removePattern: PATTERN.remove,
 	uploadLoginBg: LOGIN_BG.upload,
 	removeLoginBg: LOGIN_BG.remove,
-	_sanitizeSvg,	/* fs:probe */
 });
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-axes.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-axes.js
index c6822e864..b9af1626c 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-axes.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-axes.js
@@ -10,7 +10,7 @@
  * chrome, the menu and the search palette, so it is fetched on every admin page — and of its
  * sixty-one exports the cold path called eight. Everything here is reached only from
  * `fs-appearance` (the form) and `fs-assets` (the two uploads), both page modules, so the router
- * fetches it on the Appearance tab and nowhere else. Measured: 6.6 KB off what every page
+ * fetches it on the Appearance page and nowhere else. Measured: 6.6 KB off what every page
  * downloads.
  *
  * What stayed behind in `fs-prefs`, and why:
@@ -121,6 +121,7 @@ function propAxis(key, sdKey, prop, min, max, dfl, fmt, after) {
 
 const PALETTES = [ 'hicontrast', 'bootstrap', '2020', 'forum' ];	/* the non-default values; 'footstrap' = bare :root */
 const PALETTE = prefs.listAxis('fs-palette', 'data-palette', PALETTES, 'footstrap');
+const currentPalette = PALETTE.current, applyPalette = PALETTE.apply;
 
 /* Wallpaper is a multi-value axis: off (bare canvas), pattern (the admin-uploaded SVG, tiled and
  * recoloured — 15-wallpaper.css) or file (the admin-uploaded photo, 16-login-bg.css).
@@ -133,6 +134,7 @@ const PALETTE = prefs.listAxis('fs-palette', 'data-palette', PALETTES, 'footstra
  * in the list falls back to 'off'. */
 const WALLPAPERS = [ 'pattern', 'file' ];		/* the non-off values; 'off' = bare :root */
 const WALLPAPER = prefs.listAxis('fs-wallpaper', 'data-wallpaper', WALLPAPERS, 'off');
+const currentWallpaper = WALLPAPER.current, applyWallpaper = WALLPAPER.apply;
 
 /* Density: how much air the UI uses. A three-value axis like wallpaper, and a pure token axis —
  * 02-tokens.css multiplies the type and space ladders and every size follows, with no layout switch
@@ -143,12 +145,14 @@ const WALLPAPER = prefs.listAxis('fs-wallpaper', 'data-wallpaper', WALLPAPERS, '
  * otherwise the bar stays stacked — or stays unstacked and overflows — until the next resize. */
 
 const TINT = colorAxis('fs-tint', 'data-tint', '--fs-tint-h', '--fs-bg');
+const currentTint = TINT.current, applyTint = TINT.apply;
 
 /* Accent axis: the UI accent (solid buttons, toggle knobs, sliders, focus rings, accented links)
  * while canvas, cards and status colours stay put. On a hue, CSS rotates --fs-accent and keeps the
  * palette's lightness and chroma, so --fs-on-accent stays legible unrecomputed; on a hex the ink is
  * recomputed from the entered colour's lightness (03-palettes.css). 0 = off. */
 const ACCENT = colorAxis('fs-accent', 'data-accent', '--fs-accent-h', '--fs-accent');
+const currentAccent = ACCENT.current, applyAccent = ACCENT.apply;
 
 /* The three status colours are the same axis pointed at --fs-good / --fs-warn / --fs-danger, kept
  * separate because they carry separate meanings and every derived tint is a color-mix() of the
@@ -193,16 +197,25 @@ function surfaceAxis(key, sdKey, prop) {
 	};
 }
 const CARD = surfaceAxis('fs-card', 'card', '--fs-panel-base');
+const currentCard = CARD.current, applyCard = CARD.apply;
 const CONTROL = surfaceAxis('fs-control', 'control', '--fs-panel2-base');
+const currentControl = CONTROL.current, applyControl = CONTROL.apply;
 const BAR = surfaceAxis('fs-bar', 'bar', '--fs-bar-bg');
+const currentBar = BAR.current, applyBar = BAR.apply;
 const LINE = surfaceAxis('fs-line', 'line', '--fs-border-base');
+const currentLine = LINE.current, applyLine = LINE.apply;
+
 
 const GOOD = colorAxis('fs-good', 'data-good', '--fs-good-h', '--fs-good');
+const currentGood = GOOD.current, applyGood = GOOD.apply;
 const WARN = colorAxis('fs-warn', 'data-warn', '--fs-warn-h', '--fs-warn');
+const currentWarn = WARN.current, applyWarn = WARN.apply;
 const DANGER = colorAxis('fs-danger', 'data-danger', '--fs-danger-h', '--fs-danger');
+const currentDanger = DANGER.current, applyDanger = DANGER.apply;
 
 /* Rounding: the propAxis instance (default const and rationale up top), --fs-radius-base in px. */
 const RADIUS = propAxis('fs-radius', 'rounding', '--fs-radius-base', 0, 20, FS_RADIUS_DEFAULT, (v) => (v + 'px'));
+const currentRadius = RADIUS.current, applyRadius = RADIUS.apply, radiusDefault = RADIUS.def;
 
 /* Content width (issue #44): how far the reader lets the column grow past --fs-content-max's own
  * 1280px. A propAxis like Rounding, pointed at that one token — the range comment lives in
@@ -215,6 +228,7 @@ const RADIUS = propAxis('fs-radius', 'rounding', '--fs-radius-base', 0, 20, FS_R
 const FS_CWIDTH_DEFAULT = 1280;
 const CWIDTH = propAxis('fs-content-width', 'content_width', '--fs-content-max', 1280, 3840,
 	FS_CWIDTH_DEFAULT, (v) => (v + 'px'), () => fit.schedule());
+const currentContentWidth = CWIDTH.current, applyContentWidth = CWIDTH.apply, contentWidthDefault = CWIDTH.def;
 
 /* Layout axis: horizontal top bar (the default) vs vertical sidebar. One template, one renderer —
  * CSS morphs the chrome off :root[data-layout] and toggling re-renders nothing; menu-footstrap.js
@@ -242,12 +256,14 @@ const AXIS_KEYS = [
  * module throws, taking the chrome and the menu with it. */
 const FS_TSTR_DEFAULT = 100;
 const TSTR = propAxis('fs-tint-strength', 'tint_strength', '--fs-tint-strength', 0, 200, FS_TSTR_DEFAULT, (v) => String(v / 100));
+const currentTintStrength = TSTR.current, applyTintStrength = TSTR.apply, tintStrengthDefault = TSTR.def;
 
 /* Photo dim: the scrim opacity over the FILE photo (0–100%). The photo is shared; how strongly
  * this browser dims it is not, and it reaches the router through Save-as-default. Only bites while
  * the wallpaper is 'file'. Declared up here for the TDZ reason above. */
 const FS_PDIM_DEFAULT = 74;
 const PDIM = propAxis('fs-photo-dim', 'photo_dim', '--fs-photo-dim', 0, 100, FS_PDIM_DEFAULT, (v) => (v + '%'));
+const currentPhotoDim = PDIM.current, applyPhotoDim = PDIM.apply, photoDimDefault = PDIM.def;
 
 /* The pattern's two live knobs, and the third that is an enum. All three bite only while the
  * wallpaper is 'pattern'; the FILE is shared, how this browser draws it is not.
@@ -257,11 +273,14 @@ const PDIM = propAxis('fs-photo-dim', 'photo_dim', '--fs-photo-dim', 0, 100, FS_
  * the file would put out of CSS's reach. Declared up here for the TDZ reason above. */
 const FS_PSIZE_DEFAULT = 440;
 const PSIZE = propAxis('fs-pattern-size', 'pattern_size', '--fs-pattern-size', 40, 1600, FS_PSIZE_DEFAULT, (v) => (v + 'px'));
+const currentPatternSize = PSIZE.current, applyPatternSize = PSIZE.apply, patternSizeDefault = PSIZE.def;
 const FS_PSTR_DEFAULT = 20;
 const PSTR = propAxis('fs-pattern-strength', 'pattern_strength', '--fs-pattern-strength', 0, 100, FS_PSTR_DEFAULT, (v) => String(v / 100));
+const currentPatternStrength = PSTR.current, applyPatternStrength = PSTR.apply, patternStrengthDefault = PSTR.def;
 /* Ink: 'theme' (the file's alpha, the theme's colour) or 'original' (the file's own colours, no
  * mask). Two-valued with the default as a bare :root, i.e. the enumAxis shape. */
 const PINK = prefs.enumAxis('fs-pattern-ink', 'data-pattern-ink', 'original', 'theme');
+const currentPatternInk = PINK.current, applyPatternInk = PINK.apply;
 /* `reject: true` is load-bearing: without it a refused write arrives as SUCCESS. rpc.js raises on
  * the ubus status code only when the declaration asks it to, and otherwise hands the code back as
  * the resolved value — measured on the router, a per-config ACL refusal resolves with 6
@@ -275,30 +294,30 @@ function snapshotAxes() {
 	return {
 		layout: prefs.currentLayout(),
 		darkmode: prefs.currentMode(),
-		palette: PALETTE.current(),
-		wallpaper: WALLPAPER.current(),
-		tint: String(TINT.current()),
-		accent: String(ACCENT.current()),
-		good: String(GOOD.current()),
-		warn: String(WARN.current()),
-		danger: String(DANGER.current()),
-		card: String(CARD.current()),
-		control: String(CONTROL.current()),
-		bar: String(BAR.current()),
-		line: String(LINE.current()),
-		rounding: String(RADIUS.current()),
+		palette: currentPalette(),
+		wallpaper: currentWallpaper(),
+		tint: String(currentTint()),
+		accent: String(currentAccent()),
+		good: String(currentGood()),
+		warn: String(currentWarn()),
+		danger: String(currentDanger()),
+		card: String(currentCard()),
+		control: String(currentControl()),
+		bar: String(currentBar()),
+		line: String(currentLine()),
+		rounding: String(currentRadius()),
 		autocollapse: prefs.currentAutoCollapse() ? 'on' : 'off',
-		tint_strength: String(TSTR.current()),
+		tint_strength: String(currentTintStrength()),
 		density: prefs.currentDensity(),
-		photo_dim: String(PDIM.current()),
-		pattern_size: String(PSIZE.current()),
-		pattern_strength: String(PSTR.current()),
-		pattern_ink: PINK.current(),
-		content_width: String(CWIDTH.current())
+		photo_dim: String(currentPhotoDim()),
+		pattern_size: String(currentPatternSize()),
+		pattern_strength: String(currentPatternStrength()),
+		pattern_ink: currentPatternInk(),
+		content_width: String(currentContentWidth())
 	};
 }
 /* The resolved router default (the uci value if set, else the built-in) in snapshotAxes() string
- * form, so the Appearance tab can grey the Save button when this browser already shows exactly it.
+ * form, so the Appearance page can grey the Save button when this browser already shows exactly it.
  * Seeded from window.__fsSD at load and replaced with the just-saved snapshot, so a save flips the
  * match without a reload.
  *
@@ -322,15 +341,15 @@ function _resolvedDefault() {
 		control: String(CONTROL.def()),
 		bar: String(BAR.def()),
 		line: String(LINE.def()),
-		rounding: String(RADIUS.def()),
+		rounding: String(radiusDefault()),
 		autocollapse: (prefs.autoCollapseDefault() ? 'on' : 'off'),
-		tint_strength: String(TSTR.def()),
+		tint_strength: String(tintStrengthDefault()),
 		density: prefs.densityDefault(),
-		photo_dim: String(PDIM.def()),
-		pattern_size: String(PSIZE.def()),
-		pattern_strength: String(PSTR.def()),
+		photo_dim: String(photoDimDefault()),
+		pattern_size: String(patternSizeDefault()),
+		pattern_strength: String(patternStrengthDefault()),
 		pattern_ink: PINK.def(),
-		content_width: String(CWIDTH.def())
+		content_width: String(contentWidthDefault())
 	};
 }
 let _savedDefault = _resolvedDefault();
@@ -386,39 +405,64 @@ function resetToBuiltin() {
 	 * first click — the one state no static gate can see and the live check does. */
 	prefs.applyLayout('top');
 	prefs.applyMode('auto');
-	PALETTE.apply('footstrap');
-	WALLPAPER.apply('off');
-	TINT.apply(0);
-	ACCENT.apply(0);
-	GOOD.apply(0);
-	WARN.apply(0);
-	DANGER.apply(0);
-	CARD.apply(0);
-	CONTROL.apply(0);
-	BAR.apply(0);
-	LINE.apply(0);
-	RADIUS.apply(FS_RADIUS_DEFAULT);
+	applyPalette('footstrap');
+	applyWallpaper('off');
+	applyTint(0);
+	applyAccent(0);
+	applyGood(0);
+	applyWarn(0);
+	applyDanger(0);
+	applyCard(0);
+	applyControl(0);
+	applyBar(0);
+	applyLine(0);
+	applyRadius(FS_RADIUS_DEFAULT);
 	prefs.applyAutoCollapse('off');
-	TSTR.apply(FS_TSTR_DEFAULT);
+	applyTintStrength(FS_TSTR_DEFAULT);
 	prefs.applyDensity('normal');
-	PDIM.apply(FS_PDIM_DEFAULT);
-	PSIZE.apply(FS_PSIZE_DEFAULT);
-	PSTR.apply(FS_PSTR_DEFAULT);
-	PINK.apply('theme');
-	CWIDTH.apply(FS_CWIDTH_DEFAULT);
+	applyPhotoDim(FS_PDIM_DEFAULT);
+	applyPatternSize(FS_PSIZE_DEFAULT);
+	applyPatternStrength(FS_PSTR_DEFAULT);
+	applyPatternInk('theme');
+	applyContentWidth(FS_CWIDTH_DEFAULT);
 }
 
 /* ---- the two uploaded wallpapers, browser side ----
  *
  * What the router last saved, what URL that is, and how to paint it. Putting the file THERE is
  * fs-assets.js: a DOMParser pass, a canvas re-encode, a chmod and a rollback, reached only from the
- * Appearance tab and so not worth downloading on every admin page. The token accessors stay here
+ * Appearance page and so not worth downloading on every admin page. The token accessors stay here
  * because `prefs.sd()` is private to this module and because head.ut's pre-paint reads the same fields.
  *
  * Neither is an axis: an axis is per-browser with a router default, and these have no browser
  * layer — one admin uploads once and every device sees it, pre-login included. So they are absent
 
  * from AXIS_KEYS, snapshotAxes() and matchesSavedDefault(), and must not move the Save button. */
+/* NOT a /www symlink, unlike BG_SERVE below: an SVG served as a plain static file is a same-origin
+ * document that runs script opened directly (OpenWrt forum thread 251930). This CGI handler reads
+ * the fixed /etc/footstrap/pattern.svg path itself and answers with CSP 'none' + sandbox and
+ * nosniff, headers uhttpd cannot attach to a static file — see the uci-default and the handler for
+ * the rest of the reasoning. Painting stays exactly as before: a URL is a URL to a mask-image. */
+const PAT_SERVE = '/cgi-bin/luci-theme-footstrap-pattern';
+function currentPattern() {
+	const t = prefs.sd('pattern');
+	return (typeof t === 'string' && BG_TOKEN_RE.test(t)) ? t : '';
+}
+function patternUrl(tok) { return PAT_SERVE + '?v=' + tok; }
+
+/* set/clear the tile URL live. This only supplies the url(); whether it PAINTS is the Wallpaper
+ * axis (data-wallpaper="pattern"). Exported because fs-assets.js applies the token it just wrote. */
+function applyPattern(tok) {
+	const root = document.documentElement;
+	if (tok) root.style.setProperty('--fs-pattern-url', 'url("' + patternUrl(tok) + '")');
+	else root.style.removeProperty('--fs-pattern-url');
+	prefs.setSD('pattern', tok || '');
+}
+
+/* Still the plain uhttpd symlink the uci-default makes, unlike PAT_SERVE above: fs-assets.js
+ * re-encodes every login-bg upload to a JPEG on a canvas before it is sent, so the stored bytes
+ * are pixels only — there is no script grammar left in a raster for a direct hit to run. */
+const BG_SERVE = '/luci-static/footstrap/bg';
 /* the cache-bust token charset, an md5/sha hex string. One copy here; head.ut's ucode sanitiser
  * and the pre-paint inline script keep their own identical copies unavoidably, running before this
  * module — see the axes contract in head.ut. */
@@ -427,79 +471,45 @@ const BG_TOKEN_RE = /^[a-f0-9]{6,64}$/;
  * A predicate rather than the pattern itself, so the charset stays stated once. */
 function tokenOk(t) { return BG_TOKEN_RE.test(t); }
 
-/* The pattern and the login-bg photo are the same shape twice: a router-saved cache-bust TOKEN,
- * read back through prefs.sd(), that sets or clears one url() custom property. Neither is an axis
- * (see the comment above) — this factory is what a bare url() setter shares with a real axis's
- * apply(), not membership in AXIS_KEYS. `serve` differs for a reason stated at each call below. */
-function tokenAsset(sdKey, serve, prop) {
-	const current = () => {
-		const t = prefs.sd(sdKey);
-		return (typeof t === 'string' && BG_TOKEN_RE.test(t)) ? t : '';
-	};
-	const url = (tok) => serve + '?v=' + tok;
-	/* set/clear the url() live; whether it PAINTS is the Wallpaper axis (data-wallpaper="pattern"
-	 * or "file"). Exported because fs-assets.js applies the token it just wrote. */
-	const apply = (tok) => {
-		const root = document.documentElement;
-		if (tok) root.style.setProperty(prop, 'url("' + url(tok) + '")');
-		else root.style.removeProperty(prop);
-		prefs.setSD(sdKey, tok || '');
-	};
-	return { current, url, apply };
+/* the token the server last saved, validated to the same hex charset head.ut's sanitiser and
+ * pre-paint use, so the Appearance page can build a cache-busted preview src. '' = none. */
+function currentLoginBg() {
+	const t = prefs.sd('login_bg');
+	return (typeof t === 'string' && BG_TOKEN_RE.test(t)) ? t : '';
+}
+function loginBgUrl(tok) { return BG_SERVE + '?v=' + tok; }
+
+/* applyPattern's twin for the photo; data-wallpaper="file" decides whether it paints. */
+function applyLoginBg(tok) {
+	const root = document.documentElement;
+	if (tok) root.style.setProperty('--fs-login-bg-url', 'url("' + loginBgUrl(tok) + '")');
+	else root.style.removeProperty('--fs-login-bg-url');
+	prefs.setSD('login_bg', tok || '');
 }
 
-/* NOT a /www symlink, unlike LOGIN_BG's plain static path below: an SVG served as a plain static
- * file is a same-origin document that runs script opened directly (OpenWrt forum thread 251930).
- * This CGI handler reads the fixed /etc/footstrap/pattern.svg path itself and answers with CSP
- * 'none' + sandbox and nosniff, headers uhttpd cannot attach to a static file — see the uci-default
- * and the handler for the rest of the reasoning. Painting stays exactly as before: a URL is a URL
- * to a mask-image. */
-const PATTERN = tokenAsset('pattern', '/cgi-bin/luci-theme-footstrap-pattern', '--fs-pattern-url');
-
-/* Still the plain uhttpd symlink the uci-default makes, unlike PATTERN's CGI handler above:
- * fs-assets.js re-encodes every login-bg upload to a JPEG on a canvas before it is sent, so the
- * stored bytes are pixels only — there is no script grammar left in a raster for a direct hit to
- * run. */
-const LOGIN_BG = tokenAsset('login_bg', '/luci-static/footstrap/bg', '--fs-login-bg-url');
-
-/* Every axis exported as the OBJECT itself — `{ current, apply, def? }` — not a flat
- * currentX()/applyX() pair per axis: a caller wanting both already had to carry them as a pair
- * (fs-appearance.js's colourGroup() takes `{ current, apply }`, built ad hoc from two separate
- * lookups before this), and a flat name is one more thing every consumer of THIS FILE has to keep
- * spelled the same way fs-axes.js does — 18 axis names doubled to 36 lookups, now 18. `pattern`
- * and `loginBg` are not axes (no AXIS_KEYS entry, no router-default `def()`) but the same
- * `tokenAsset()` shape, so they export the same way. `tokenOk`, `matchesSavedDefault`,
- * `saveAsDefault`, `resetToSaved` and `resetToBuiltin` stay flat: none of them is a
- * `{current, apply}` pair. `snapshotAxes` stays module-private: fs-appearance.js, its only would-be
- * caller, builds the Appearance form from the per-axis exports above, not from the router-default
- * snapshot matchesSavedDefault()/saveAsDefault() read internally.
- *
- * No flat `currentX`/`applyX` fallback kept: grepped `../luci-app-footstrap-*` (the sibling
- * checkouts) and this repo's docs for either spelling — nothing outside fs-appearance.js and
- * fs-assets.js, both updated alongside this file, ever called one. */
 return baseclass.extend({
-	palette: PALETTE,
-	wallpaper: WALLPAPER,
-	tint: TINT,
-	accent: ACCENT,
-	good: GOOD,
-	warn: WARN,
-	danger: DANGER,
-	card: CARD,
-	control: CONTROL,
-	bar: BAR,
-	line: LINE,
-	radius: RADIUS,
-	tintStrength: TSTR,
-	photoDim: PDIM,
-	patternSize: PSIZE,
-	patternStrength: PSTR,
-	patternInk: PINK,
-	contentWidth: CWIDTH,
-	pattern: PATTERN,
-	loginBg: LOGIN_BG,
+	currentPalette, applyPalette,
+	currentWallpaper, applyWallpaper,
+	currentTint, applyTint,
+	currentAccent, applyAccent,
+	currentGood, applyGood,
+	currentWarn, applyWarn,
+	currentDanger, applyDanger,
+	currentCard, applyCard,
+	currentControl, applyControl,
+	currentBar, applyBar,
+	currentLine, applyLine,
+	currentRadius, applyRadius,
+	currentTintStrength, applyTintStrength,
+	currentPhotoDim, applyPhotoDim,
+	currentPatternSize, applyPatternSize,
+	currentPatternStrength, applyPatternStrength,
+	currentPatternInk, applyPatternInk,
+	currentContentWidth, applyContentWidth, contentWidthDefault,
 
+	currentPattern, patternUrl, applyPattern,
+	currentLoginBg, loginBgUrl, applyLoginBg,
 	tokenOk,
 
-	matchesSavedDefault, saveAsDefault, resetToSaved, resetToBuiltin
+	snapshotAxes, matchesSavedDefault, saveAsDefault, resetToSaved, resetToBuiltin
 });
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js
index f94292f37..b62a0e585 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js
@@ -4,7 +4,6 @@
 'require fs-fit as fit';
 'require fs-prefs as prefs';
 'require fs-menutree as tree';
-'require fs-widgets as widgets';
 
 /* The chrome around the content: the mode menu, the section tabs, the rail toggle and the
  * measurements deciding how much room each gets. The main menu is injected by menu-footstrap.js as
@@ -19,8 +18,11 @@ function setRenderMain(fn) {
 
 /* section tabs -> #tabmenu (horizontal) */
 function renderTabMenu(node, url, level) {
-	/* #tabmenu is emitted whenever this module loads (notices.ut, !blank_page) */
 	const container = document.querySelector('#tabmenu');
+	/* a template without the container must not reject: an unhandled rejection here kills the
+	 * whole ui.menu.load() chain, i.e. every menu */
+	if (!container)
+		return E([]);
 	const ul = E('ul', { 'class': 'tabs' });
 	const children = ui.menu.getChildren(node);
 	let activeNode = null;
@@ -37,13 +39,15 @@ function renderTabMenu(node, url, level) {
 	});
 
 	if (ul.children.length === 0)
-		return;
+		return E([]);
 
 	container.appendChild(ul);
 	container.style.display = '';
 
 	if (activeNode)
 		renderTabMenu(activeNode, url + '/' + activeNode.name, (level || 0) + 1);
+
+	return ul;
 }
 
 /* ---- tab-strip auto-fit ----
@@ -110,12 +114,6 @@ let _probe = null;
 function resolveLen(token, dflt) {
 	if (!_probe) {
 		_probe = document.createElement('div');
-		/* the id is fs-router.js's strayBodyNode() (issue #56) reading an fs-* id as "ours", which is
-		 * a different question from the chrome mark above: this stays unmarked (no data-fs-chrome,
-		 * so the fence still does not spare it from a hostile !important) but still needs SOME fs-*
-		 * name, or an element appended once and never removed would read as body litter and force
-		 * every navigation off this page into a full load. */
-		_probe.id = 'fs-chrome-geom-probe';
 		_probe.setAttribute('aria-hidden', 'true');
 		/* out of flow, no box, no ink: it must never affect layout, scroll extent or hit-testing */
 		_probe.style.cssText = 'position:absolute!important;visibility:hidden!important;' +
@@ -152,16 +150,17 @@ function shellGeometry() {
 	/* the gutter is re-asked even on a memo hit: it moves with the width, not the density */
 	if (_geom && _geomDensity === key) return _geom;
 	_geomDensity = key;
+	const px = (name, dflt) => resolveLen(name, dflt);
 	const g = {
-		contentMin: resolveLen('--fs-content-min', GEOM_DFLT.contentMin),
-		sidebarW:   resolveLen('--fs-sidebar-w', GEOM_DFLT.sidebarW),
-		railW:      resolveLen('--fs-rail-w', GEOM_DFLT.railW),
+		contentMin: px('--fs-content-min', GEOM_DFLT.contentMin),
+		sidebarW:   px('--fs-sidebar-w', GEOM_DFLT.sidebarW),
+		railW:      px('--fs-rail-w', GEOM_DFLT.railW),
 		/* the token is one side's padding; the column loses it twice. It is only the fallback —
 		 * measureShell() overwrites this with the gutter the column actually got, which nothing
 		 * has measured before the first fitter (and the login page has no `.fs-content`). */
-		contentPad: resolveLen('--fs-content-pad', GEOM_DFLT.contentPad / 2) * 2,
+		contentPad: px('--fs-content-pad', GEOM_DFLT.contentPad / 2) * 2,
 		/* where the column stops growing, i.e. where surplus becomes margin — see columnWidth() */
-		contentMax: resolveLen('--fs-content-max', GEOM_DFLT.contentMax)
+		contentMax: px('--fs-content-max', GEOM_DFLT.contentMax)
 	};
 	/* Plausibility, at the cost of one comparison: the rail is the sidebar collapsed, so
 	 * 0 < railW < sidebarW holds by construction. Both known failures destroy it — a hijacked probe
@@ -282,8 +281,8 @@ function fitChrome() {
 	 * 123px on owrt2512 at 767px — 107px of growth a floor never sees, on top of the shrink it does
 	 * — each step landing between two of the poll's own separate section refreshes, so the browser
 	 * paints in between and the reader is moved by exactly as much, on Chromium and Firefox as well
-	 * as Safari (`tools/fit-quiet.mjs`, `../tmp/task-toplayout/pass-probe.mjs`). `min-height` alone
-	 * was measured to `accc451`'s WebKit-only diagnosis instead — WebKit's own scroll anchoring
+	 * as Safari (`tools/fit-quiet.mjs`, plus a probe that pauses the layout mid-pass). `min-height`
+	 * alone was measured to `accc451`'s WebKit-only diagnosis instead — WebKit's own scroll anchoring
 	 * looked like the whole story only because it is the one engine with no anchoring at all to hide
 	 * this walk behind; Chromium and Firefox absorb it the same way they absorb any other layout
 	 * change, which is not the same as not producing it.
@@ -402,7 +401,6 @@ function clusterFitsBrandRow(bar, menu) {
 
 /* modes -> #modemenu; drives the injected renderMainMenu for the active mode */
 function renderModeMenu(node, renderMainMenu) {
-	/* #modemenu is emitted whenever this module loads (header.ut, !blank_page) */
 	const ul = document.querySelector('#modemenu');
 	const children = ui.menu.getChildren(node);
 
@@ -411,14 +409,18 @@ function renderModeMenu(node, renderMainMenu) {
 			? child.name === L.env.requestpath[0]
 			: index === 0;
 
-		ul.appendChild(E('li', { 'class': isActive ? 'active' : '' }, [
-			E('a', { 'href': L.url(child.name) }, [ _(child.title) ])
-		]));
+		/* the main menu must render even where a template has no #modemenu */
+		if (ul)
+			ul.appendChild(E('li', { 'class': isActive ? 'active' : '' }, [
+				E('a', { 'href': L.url(child.name) }, [ _(child.title) ])
+			]));
 
 		if (isActive)
 			renderMainMenu(child, child.name);
 	});
 
+	if (!ul)
+		return;
 	if (children.length <= 1)
 		ul.classList.add('single');
 	if (ul.children.length > 1)
@@ -429,15 +431,13 @@ function renderModeMenu(node, renderMainMenu) {
  * every SPA nav. Containers are cleared first so a re-render does not stack duplicates. */
 function renderChrome() {
 	const root = tree.tree();
-	/* #modemenu/#topmenu/#tabmenu are emitted whenever this module loads (header.ut/notices.ut,
-	 * !blank_page) */
 	const modemenu = document.querySelector('#modemenu');
 	const topmenu  = document.querySelector('#topmenu');
 	const tabmenu  = document.querySelector('#tabmenu');
 
-	modemenu.innerHTML = ''; modemenu.style.display = 'none'; modemenu.classList.remove('single');
-	topmenu.innerHTML = '';
-	tabmenu.innerHTML = ''; tabmenu.style.display = 'none';
+	if (modemenu) { modemenu.innerHTML = ''; modemenu.style.display = 'none'; modemenu.classList.remove('single'); }
+	if (topmenu)  topmenu.innerHTML = '';
+	if (tabmenu)  { tabmenu.innerHTML = ''; tabmenu.style.display = 'none'; }
 
 	renderModeMenu(root, _renderMain);
 
@@ -461,8 +461,8 @@ function renderChrome() {
  *  (head.ut re-applies it before paint) and in localStorage; everything else is
  * CSS keyed off that attribute. */
 function wireRail() {
-	/* #fs-rail-toggle is emitted whenever this module loads (header.ut, !blank_page) */
 	const btn = document.getElementById('fs-rail-toggle');
+	if (!btn) return;
 
 	function sync() {
 		const on = prefs.currentRail();
@@ -494,23 +494,38 @@ function wireRail() {
  * stays in the label for screen readers, and in `title` for the pointer. */
 const IND_DOT = '•';
 
+/* Idempotent attribute write, so a poll tick that finds nothing changed touches no DOM — same
+ * shape as `fsSyncAttr` in menu-footstrap-common.js, restated rather than imported (that file does
+ * not export it). */
+function syncIndAttr(el, name, value) {
+	if (value === null) {
+		if (el.hasAttribute(name)) el.removeAttribute(name);
+	} else if (el.getAttribute(name) !== value) {
+		el.setAttribute(name, value);
+	}
+}
+
 /* A CLICKABLE `[data-indicator]` (the poll pill, "Unsaved Changes: N", …) ships as a bare
  * span: no role, name or tabindex, so Tab skips it and a screen reader
  * announces a run of text with no name, role or state (WCAG 2.1.1, 4.1.2). ui.showIndicator's own
  * click handler already lives on this exact element — this only adds the second, W3C-APG way to
  * reach it; it does not add a competing one. The name is the pill's own prose ("Refreshing"),
- * never invented. widgets.wireActivate() calls el.click() because a , unlike an , gets
- * neither Enter nor Space for free; calling click() cannot double-fire the mouse handler, since a
- * keydown is not a click. */
+ * never invented. Enter/Space call el.click() because a , unlike an , gets neither key
+ * for free (contrast fs-widgets.js's wireSpaceKey, written for an ); calling
+ * click() cannot double-fire the mouse handler, since a keydown is not a click. */
 function wireIndicatorKeyboard(el) {
 	if (el.dataset.fsWired) return;
 	el.dataset.fsWired = '1';
-	widgets.wireActivate(el, () => el.click());
+	el.addEventListener('keydown', (ev) => {
+		if (ev.key !== 'Enter' && ev.key !== ' ' && ev.key !== 'Spacebar') return;
+		ev.preventDefault();
+		el.click();
+	});
 }
 
 function wireIndicatorCounts() {
-	/* #indicators is emitted whenever this module loads (header.ut, !blank_page) */
 	const box = document.getElementById('indicators');
+	if (!box) return;
 
 	function stamp() {
 		box.querySelectorAll('[data-indicator]').forEach((el) => {
@@ -526,9 +541,9 @@ function wireIndicatorCounts() {
 			 * replaces. The ring and the pointer cursor in theme/20-shell.css are scoped to
 			 * `[data-clickable]` for the same reason; the two must not disagree. */
 			if (el.hasAttribute('data-clickable')) {
-				widgets.syncAttr(el, 'role', 'button');
-				widgets.syncAttr(el, 'tabindex', '0');
-				widgets.syncAttr(el, 'aria-label', txt.trim() || null);
+				syncIndAttr(el, 'role', 'button');
+				syncIndAttr(el, 'tabindex', '0');
+				syncIndAttr(el, 'aria-label', txt.trim() || null);
 				wireIndicatorKeyboard(el);
 			}
 		});
@@ -585,9 +600,9 @@ return baseclass.extend({
 		 * resize is exactly what starts that window (fs-fit.js's resize observer feeds the same
 		 * motion sampler `scrolling()` reads). A caller landing in that window, most of all
 		 * fs-select's, got the width the PREVIOUS viewport had: at 568px settling to 390px, model
-		 * stayed 568 for up to 220ms of the 400 (measured: −178px, exactly 568−390;
-		 * ../tmp/task-vnstat/probe2.mjs, probe3.mjs; live-audit's `geometry|fs-content` finding on
-		 * owrt2410, CI run 34364446910).
+		 * stayed 568 for up to 220ms of the 400 (measured: −178px, exactly 568−390, against a probe
+		 * resizing the window mid-settle, and live-audit's `geometry|fs-content` finding on owrt2410,
+		 * CI run 34364446910).
 		 *
 		 * So the window's own width is compared fresh on every call, not only when `_shellOuter`
 		 * is still zero. `clientWidth` is the one read the old "no layout read" promise here was
@@ -609,6 +624,5 @@ return baseclass.extend({
 	/* exported for tests/chrome-geometry.test.mjs (no tests ship in the package): driving the
 	 * arithmetic directly is the only way to cover every combination of layout, rail and width
 	 * without a browser */
-	columnWidth,	/* fs:probe */
 	wireRail
 });
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js
index 568332eae..67224e46e 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js
@@ -65,34 +65,23 @@ function holdFloor(records) {
 	if (scrolling()) return;
 	const host = document.getElementById('view');
 	if (!host) return;			/* the login page has no view */
-	/* boxes stays an array — order feeds the parallel `hs` below and dirty's own .filter() — with a
-	 * Set alongside it purely for the O(1) "already have this one" check .indexOf() was doing in
-	 * O(n). */
-	const boxes = [], boxSeen = new Set(), hs = [];
+	const boxes = [], hs = [];
 	host.querySelectorAll(SHRINKS).forEach((el) => {
 		let box = el, cs = window.getComputedStyle(el);
 		while (box && box !== host && cs.display.startsWith('table')) {
 			box = box.parentElement;
 			if (box) cs = window.getComputedStyle(box);
 		}
-		if (!box || box === host || boxSeen.has(box)) return;
-		boxSeen.add(box);
-		boxes.push(box);
-	});
-	host.querySelectorAll(FLOORED).forEach((box) => {
-		if (boxSeen.has(box)) return;
-		boxSeen.add(box);
+		if (!box || box === host || boxes.indexOf(box) !== -1) return;
 		boxes.push(box);
 	});
+	host.querySelectorAll(FLOORED).forEach((box) => { if (boxes.indexOf(box) === -1) boxes.push(box); });
 
 	let dirty = boxes;
 	if (records && records.length) {
-		const targets = new Set();
-		for (const r of records) if (r.target) targets.add(r.target);
-		dirty = boxes.filter((box) => {
-			for (const t of targets) if (box.contains(t) || t.contains(box)) return true;
-			return false;
-		});
+		const targets = [];
+		for (const r of records) if (r.target && targets.indexOf(r.target) === -1) targets.push(r.target);
+		dirty = boxes.filter((box) => targets.some((t) => box.contains(t) || t.contains(box)));
 	}
 	if (!dirty.length) return;
 
@@ -374,28 +363,14 @@ function anchorRef() {
 		sec: keep, secTop: keep ? keep.getBoundingClientRect().top : 0 };
 }
 
-/* Two independent diagnostic trails — one per correction path (the frame-coalesced anchor below,
- * the late drift correction further down) — same shape twice: the last reason recorded, and the
- * last 8 with a timestamp, for tools/scroll-anchor.mjs to read back after a probe run. One factory,
- * so the cap and the timestamp format cannot drift between the two. */
-const TRAIL_MAX = 8;
-function makeTrail() {
-	let last = null;
-	const entries = [];
-	return {
-		record(w) {
-			last = w;
-			entries.push(w + '@' + Math.round(performance.now()));
-			if (entries.length > TRAIL_MAX) entries.shift();
-		},
-		last: () => last,
-		entries: () => entries
-	};
-}
-
 let _anchorPending = null;
-const _anchor = makeTrail();
-function awhy(w) { _anchor.record(w); }
+let _anchorWhy = null;
+const _anchorTrail = [];
+function awhy(w) {
+	_anchorWhy = w;
+	_anchorTrail.push(w + '@' + Math.round(performance.now()));
+	if (_anchorTrail.length > 8) _anchorTrail.shift();
+}
 let _anchorFrame = 0;
 function anchorEnabled() {
 	try { return localStorage.getItem('fsAnchor') !== 'off'; }
@@ -405,8 +380,14 @@ function anchorEnabled() {
  * It keeps a reference still; it does not promise that a section can vanish and come back. An
  * older WebKit moved 180px for 120px of growth, so the element is asked where it is now. */
 let _lateFrame = 0;
-const _late = makeTrail();
-function why(w) { _late.record(w); }
+/* why the last late correction did or did not write — eight exits, one symptom from outside */
+let _lateWhy = null;
+const _lateTrail = [];
+function why(w) {
+	_lateWhy = w;
+	_lateTrail.push(w + '@' + Math.round(performance.now()));
+	if (_lateTrail.length > 8) _lateTrail.shift();
+}
 
 function lateDrift(ref, grow, floorShrink) {
 	if (_lateFrame) return why('busy');
@@ -626,11 +607,11 @@ return baseclass.extend({
 	},
 
 	scrolling,
-	/* unmarked, for tools/scroll-anchor.mjs — see `makeTrail()` above */
-	lateWhy: () => _late.last(),
-	lateTrail: () => _late.entries().slice(),
-	anchorWhy: () => _anchor.last(),
-	anchorTrail: () => _anchor.entries().slice(),
+	/* unmarked, for tools/scroll-anchor.mjs — see `_lateWhy` */
+	lateWhy: () => _lateWhy,
+	lateTrail: () => _lateTrail.slice(),
+	anchorWhy: () => _anchorWhy,
+	anchorTrail: () => _anchorTrail.slice(),
 	deferMeasurement,
 
 	/* -> the offset this file last took a reference at, so a probe does not measure the guard instead
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-menutree.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-menutree.js
index cb1427d87..c7e870c91 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-menutree.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-menutree.js
@@ -19,7 +19,7 @@ function setTree(tree) {
  * reserved for a path outside LuCI's scriptname. */
 function segsFromPath(pathname) {
 	const base = L.env.scriptname || '';
-	if (base && !pathname.startsWith(base))
+	if (base && pathname.indexOf(base) !== 0)
 		return null;
 	const rest = pathname.slice(base.length).replace(/^\/+|\/+$/g, '');
 	return rest.length ? rest.split('/') : [];
@@ -159,6 +159,10 @@ function currentNode() {
 return baseclass.extend({
 	setTree,
 	tree: () => _tree,
+	/* raw presence, no alias or firstchild resolution: fs-commands gates each command on the menu
+	 * node whose `depends.acl` names the group that command needs, and a node that resolves
+	 * elsewhere would answer for a permission the session may not hold */
+	nodeForSegs,
 	segsFromPath,
 	currentNode,
 	resolveSegs,
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js
index 6d97450b1..50ed59d41 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js
@@ -3,7 +3,6 @@
 'require dom';
 'require network';
 'require fs-fit as fit';
-'require fs-widgets as widgets';
 'require menu-footstrap-common as common';
 
 /* Overview layout only: renders nothing of its own, it re-arranges the STOCK System / Memory /
@@ -22,6 +21,8 @@
 /* section title -> grid role. _() with no msgctxt on purpose: these must resolve to exactly what
  * luci-mod-status resolves to, or the titles stop matching. Built once, not per poll tick. */
 const ROLES = { [_('System')]: 'sys', [_('Memory')]: 'mem', [_('Storage')]: 'sto' };
+/* the data-page value four call sites compare against; a string literal is not mangled, so a
+ * repeat is paid in full on flash every time (measured: 24 B x4 -> 37 B, 59 B saved) */
 
 function headerEl(sec) {
 	/* two title markups, one per release: 25.12 wraps the heading (`.cbi-title > h3`), 24.10 emits
@@ -49,11 +50,23 @@ function sectionTitle(sec) {
  * topbar poll pill plus one Hide/Show toggle per card — and every one is a bare : no
  * tabindex, no role, no aria-expanded. Tab never reaches one and a screen reader announces a run
  * of text with no name, role or state. WCAG 2.1.1 Keyboard (A), 4.1.2 Name, Role, Value (A).
+ * docs/findings.md, "A card cannot be collapsed from the keyboard".
  *
  * index.js's own pill keeps doing the actual show/hide — untouched, so mouse behaviour for anyone
  * clicking it does not change. The header becomes a second, W3C-APG way to reach the SAME handler,
  * not a competing one. */
 
+/* Idempotent attribute write, so a poll tick that finds nothing changed touches no DOM and fires
+ * no mutation record. Same shape as `fsSyncAttr` in menu-footstrap-common.js — restated rather than
+ * imported, since that file does not export it. */
+function syncAttr(el, name, value) {
+	if (value === null) {
+		if (el.hasAttribute(name)) el.removeAttribute(name);
+	} else if (el.getAttribute(name) !== value) {
+		el.setAttribute(name, value);
+	}
+}
+
 /* index.js's own attribute: "inactive" is expanded (the pill reads "Hide"), "active" is collapsed
  * (it reads "Show") — the chevron mirror in pages/20-overview.css reads the same attribute the
  * same way. */
@@ -83,14 +96,14 @@ function wireDisclosure(sec) {
 	const panel = panelFor(h);
 	if (panel && !panel.id) panel.id = 'fs-ovl-panel-' + (_panelSeq++);
 
-	widgets.syncAttr(h, 'role', 'button');
-	widgets.syncAttr(h, 'tabindex', '0');
-	widgets.syncAttr(h, 'aria-controls', panel ? panel.id : null);
-	widgets.syncAttr(h, 'aria-expanded', pillExpanded(label) ? 'true' : 'false');
+	syncAttr(h, 'role', 'button');
+	syncAttr(h, 'tabindex', '0');
+	syncAttr(h, 'aria-controls', panel ? panel.id : null);
+	syncAttr(h, 'aria-expanded', pillExpanded(label) ? 'true' : 'false');
 	/* the header now carries the pill's name, role and state; a screen reader user tabbing past it
 	 * to a second, unlabelled clickable span would hear an unexplained duplicate control — same
 	 * reasoning as the aria-hidden on svgIcon()'s output, fs-widgets.js:12 */
-	widgets.syncAttr(label, 'aria-hidden', 'true');
+	syncAttr(label, 'aria-hidden', 'true');
 
 	if (h.dataset.fsWired) return;
 	h.dataset.fsWired = '1';
@@ -99,18 +112,22 @@ function wireDisclosure(sec) {
 		/* a click landing on the pill itself already ran index.js's own handler; forwarding here
 		 * too would toggle the card twice */
 		if (ev.target.closest?.('[data-indicator="poll-status"]') !== label) label.click();
-		widgets.syncAttr(h, 'aria-expanded', pillExpanded(label) ? 'true' : 'false');
+		syncAttr(h, 'aria-expanded', pillExpanded(label) ? 'true' : 'false');
 	});
 	/* the pill is an -less , so neither key is native here — contrast
-	 * menu-footstrap.js's wireSpaceKey, written for an , which gets Enter for
-	 * free and needs only Space added */
-	widgets.wireActivate(h, () => label.click());
+	 * fs-widgets.js's wireSpaceKey, written for an , which gets Enter for free
+	 * and needs only Space added */
+	h.addEventListener('keydown', (ev) => {
+		if (ev.key !== 'Enter' && ev.key !== ' ' && ev.key !== 'Spacebar') return;
+		ev.preventDefault();
+		label.click();
+	});
 }
 
 /* A `.cbi-section` LuCI still renders when a stock include has nothing to show this tick: title
  * "-", the poll pill its only content, 72px tall on the live Overview — seventh there (a fixture is
- * not proof of position). Real and cosmetic, so it is suppressed rather than left as a rung in the
- * tab order with nothing behind its own name. */
+ * not proof of position: docs/playground.html puts it first). Real and cosmetic, so it is
+ * suppressed rather than left as a rung in the tab order with nothing behind its own name. */
 function hideEmptyCard(sec) {
 	if (sectionTitle(sec) === '-') sec.classList.add('fs-ovl-empty');
 }
@@ -199,13 +216,15 @@ function arrange() {
 	_wrapEl = wrap;
 }
 
-/* Stock sections render async and repaint every poll, so watch #maincontent and re-run arrange(),
- * coalesced. #maincontent (unlike #view) outlives every SPA swap, so one observer, attached once,
- * is never stale and never needs to be re-attached. */
-let _observer = null, _routeObserver = null;
+/* Stock sections render async and repaint every poll, so watch #view and re-run arrange(),
+ * coalesced and one observer per #view node. The SPA router may replace #view between visits, so
+ * re-attach when the observed node is no longer the current one — a singleton bound to the first
+ * #view would watch a detached tree and the grid would never apply again. */
+let _observer = null, _observedRoot = null, _routeObserver = null;
 function stopWatch() {
 	if (_observer) _observer.disconnect();
 	_observer = null;
+	_observedRoot = null;
 	_wrapEl = null;	/* the grid belongs to the #view we are leaving */
 }
 function watch() {
@@ -215,12 +234,16 @@ function watch() {
 	 * first — so the observer bound here read `isConnected: false` after one round trip and the
 	 * grid stopped being re-arranged on every poll tick, silently, until the next full load. The
 	 * shell's column outlives every swap. Same fault, same fix as fs-appearance.js. */
-	const root = document.getElementById('maincontent');
+	const root = document.getElementById('maincontent') || view;
+	if (_observer && _observedRoot !== root)
+		stopWatch();
 	arrange();
-	/* the caller (onOverview(), via wire()'s data-page observer) already confirmed the overview
-	 * route before reaching here; !view alone covers the mid-navigation race above */
-	if (_observer || !view)
+	/* a chrome module is alive on every page, so without the route check an observer would attach
+	 * to #view on, say, the firewall page and re-run arrange() for every table mutation */
+	if (_observer || !view ||
+	    (document.body.getAttribute('data-page') || '') !== 'admin-status-overview')
 		return;
+	_observedRoot = root;
 	/* one arrange() per frame, however many mutations a poll tick delivers (fit.frame — the
 	 * theme's shared coalescer, fs-fit.js) */
 	_observer = new MutationObserver(fit.frame(arrange));
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js
index 6aa6f0481..2840c6bdf 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js
@@ -1,5 +1,6 @@
 'use strict';
 'require baseclass';
+'require rpc';
 'require fs-fit as fit';
 
 /* The Appearance axes this file owns; the controls that present them are fs-appearance.js. The
@@ -43,15 +44,13 @@ function lsGetArr(k) {
 }
 
 /* the router-wide defaults the server stamped (head.ut), read at runtime so current*() reports the
- * effective default when this browser has no localStorage. No try/catch: unlike localStorage,
- * reading or writing a plain object property cannot throw, and window.__fsSD is always one (head.ut
- * stamps it as an object literal). */
-function sd(k) { return (window.__fsSD || {})[k]; }
+ * effective default when this browser has no localStorage */
+function sd(k) { try { return (window.__fsSD || {})[k]; } catch (e) { return undefined; } }
 
 /* …and the write back: an applier that persists to the router must update the blob the server
  * stamped, or current*() keeps reporting the old router default until the next full load and
  * matchesSavedDefault() lies about whether anything is left to save */
-function setSD(field, val) { (window.__fsSD = window.__fsSD || {})[field] = val; }
+function setSD(field, val) { try { (window.__fsSD = window.__fsSD || {})[field] = val; } catch (e) {} }
 
 /* ---- every axis owns its ROUTER DEFAULT, and nothing else may restate it ----
  * `def()` is the sd() branch of current() alone: the effective value with no localStorage. Exposed
@@ -66,6 +65,7 @@ function currentMode() {
 	const s = lsGet('fs-darkmode');
 	if (s === 'true') return 'dark';
 	if (s === 'false') return 'light';
+	if (s === 'auto') return 'auto';
 	if (s === null) return modeDefault();
 	return 'auto';
 }
@@ -169,11 +169,18 @@ function paintThemeColor() {
 }
 
 function watchThemeColor() {
+	let queued = false;
+	const paint = () => {
+		queued = false;
+		paintThemeColor();
+	};
+	const schedule = () => {
+		if (queued) return;
+		queued = true;
+		window.requestAnimationFrame(paint);
+	};
 	paintThemeColor();
-	/* fit.frame() is the same next-frame coalescer every geometry fitter schedules through: the
-	 * tint and strength sliders write on every input event, and getComputedStyle forces style
-	 * resolution, so a dozen mutations in one frame must still repaint the meta tag once. */
-	new MutationObserver(fit.frame(paintThemeColor)).observe(document.documentElement,
+	new MutationObserver(schedule).observe(document.documentElement,
 		{ attributes: true, attributeFilter: [ 'style', 'class', 'data-darkmode', 'data-palette', 'data-wallpaper' ] });
 }
 
@@ -185,6 +192,28 @@ _mqDark.addEventListener('change', () => {
 		stampDark(root, intendedDark());
 	}
 });
+/* Corner radius: the card radius (0–20px) as an inline --fs-radius-base on :root, from which
+ * 02-tokens derives every other radius. head.ut pre-paints it and tools/axes.mjs holds JS/CSS/head
+ * to this one number, hence the named const. */
+
+/* ---- the four axis shapes, each written once ----
+ *
+ * Sixteen axes are four shapes, so the shape lives in a factory and each instance is one line:
+ * enumAxis (pattern ink), colorAxis (tint, accent, good, warn, danger), surfaceAxis (cards,
+ * controls, bar, borders), propAxis (rounding, tint strength, photo dim, pattern size, pattern
+ * strength, content width). Same contract throughout: `current()` is localStorage ?? def(), `def()` is the router
+ * default alone, `apply()` stores the choice explicitly. None use `this` — every export is a
+ * detached method reference, so a `this` here would throw on the first call.
+ *
+ * Each factory takes its localStorage key as the first argument, and tools/axes.mjs matches the
+ * call by its literal args: an axis built by a factory has no lsGet('fs-…') call site for the gate
+ * to find.
+ *
+ * The remaining axes stay separate, each with a quirk a shared table would need an option for:
+ * `mode` stores a value it does not apply and owns an MQL listener, `layout` reads the attribute,
+ * `wallpaper` and `density` are three-valued, `palette` outgrew the two-value shape when the third
+ * one landed, `autoCollapse` has no :root attribute. */
+
 /* An axis whose values are a list, with one of them stamped as nothing.
  *
  * `values` are the names that become `attr=""`; `dflt` is the one that leaves :root bare, and
@@ -206,6 +235,7 @@ function listAxis(key, attr, values, dflt, after) {
 		current() {
 			const s = lsGet(key);
 			if (ok(s)) return s;
+			if (s === dflt) return dflt;
 			if (s === null) return def();
 			return dflt;	/* a stray value reads as the built-in default */
 		},
@@ -228,6 +258,22 @@ function enumAxis(key, attr, on, off) {
 	return listAxis(key, attr, [ on ], off);
 }
 
+/* A colour axis — Tint, Accent and the three status colours are one axis pointed at five tokens:
+ * same validation, same "0 is off", same ordering rule (set the custom property BEFORE the
+ * attribute, or a fresh load paints one frame in the previous colour).
+ *
+ * A value is one of three things, and the attribute says which (03-palettes.css matches on it):
+ *
+ *   0            off — no attribute, the palette as it shipped
+ *   1–360        a HUE: CSS rotates the palette's own colour through oklch(from … l c H), so
+ *                lightness, chroma and every contrast margin stay the palette's
+ *   '#rrggbb'    a COLOUR, stamped inline on :root as the live token; the ink over it is derived
+ *                from its lightness in CSS
+ *
+ * Both live in one localStorage key rather than a colour key beside a hue key: two keys would need
+ * a third to say which is in effect, and that third is the one a pre-paint script forgets.
+ * `hueProp` carries the degrees, `colorProp` the live token a hex value overwrites; each mode
+ * clears the other's property, so the two can never both be half-applied. */
 const DENSITIES = [ 'compact', 'large' ];	/* the two non-default values; 'normal' = bare :root */
 const DENSITY = listAxis('fs-density', 'data-density', DENSITIES, 'normal', () => fit.schedule());
 const currentDensity = DENSITY.current, applyDensity = DENSITY.apply,
@@ -235,8 +281,13 @@ const currentDensity = DENSITY.current, applyDensity = DENSITY.apply,
 
 /* Content width lives entirely in fs-axes.js now (a propAxis like Rounding, issue #44): unlike
  * Density it sets no attribute, so nothing here or in fs-chrome.js needs to read one, and
- * currentContentWidth()/applyContentWidth() have no caller outside the Appearance tab — the one
+ * currentContentWidth()/applyContentWidth() have no caller outside the Appearance page — the one
  * thing that kept Density's shape in this cold-path file. */
+/* Background-tint axis: the canvas the cards float on (--fs-bg), so a whole install reads as one
+ * colour and a tab or a screenshot says which router it belongs to. Cards, chrome and the status
+ * colours keep the palette's values — the cue colours the paper, not the UI. On a hue it is mixed
+ * in CSS (03-palettes.css explains why that stays contrast-safe at every angle); on a hex it IS the
+ * canvas. 0 is off rather than red, a hue wheel wrapping, so one end of the range is free. */
 function currentLayout() {
 	return document.documentElement.getAttribute('data-layout') === 'top' ? 'top' : 'sidebar';
 }
@@ -291,6 +342,23 @@ function currentRail() {
 	return document.documentElement.getAttribute('data-rail') === 'true';
 }
 
+/* ---- Save to router: write the current effective axes to /etc/config/footstrap ----
+ * The scoped rpcd ACL (config 'footstrap' only) lets the admin's session set and commit those
+ * options; rpcd validates the config/section/option names, so no value reaches a shell. The server
+ * reads them back on the next load and head.ut's sanitiser clamps each before it becomes
+ * window.__fsSD.
+ *
+ * snapshotAxes() reads the effective values, which already fold in this browser's localStorage, so
+ * Save captures what the user sees. It does not touch localStorage: this browser keeps overriding,
+ * and the saved default is for other devices. resetToSaved() drops this browser back onto it. */
+/* Every saved axis's localStorage key: what Save-as-default clears and what a reset walks.
+ *
+ * Tried as one table of [key, field, def] with the resolved defaults derived from it: correct,
+ * and 188 B larger after minification — three lists of short literals compress better than
+ * twenty-one rows of data, because a function name is mangled and a row is not. The copies are
+ * held together by tools/axes.mjs instead, which reads snapshotAxes()'s body and holds every
+ * field against header.ut's FS_AXES. */
+
 return baseclass.extend({
 	/* the storage wrappers and the router-default reader: fs-axes.js is built on these */
 	lsGet, lsSet, lsDel, lsGetArr, storageBroken, sd, setSD,
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js
index d65f2bcde..70fdb7bb2 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js
@@ -43,9 +43,9 @@ const _viewIntervals = (window.__fsViewIntervals || (window.__fsViewIntervals =
 	 * keyed by that first id and the entry carries `live`, the id armed right now (null while
 	 * paused), so a view holding its handle can still stop its own poller after a trip through a
 	 * hidden tab. The arguments are kept because a `setInterval` id carries none of them back. */
-	window.setInterval = function (fn, ms, ...rest) {
+	window.setInterval = function (fn, ms) {
 		const id = _si.apply(window, arguments);
-		_viewIntervals.set(id, { fn, ms, rest, live: id });
+		_viewIntervals.set(id, { fn, ms, rest: Array.prototype.slice.call(arguments, 2), live: id });
 		return id;
 	};
 	window.clearInterval = function (id) {
@@ -102,19 +102,28 @@ const _viewIntervals = (window.__fsViewIntervals || (window.__fsViewIntervals =
  *
  * `L.Poll.timer` is that id, and it is private state: `add`/`remove`/`start`/`stop`/`active` are the
  * documented surface, and the whole `L.Poll` alias is already deprecated (`'require poll'` replaces
- * it, but no supported release ships poll.js yet). `active()` below is called unguarded: this
- * caller runs from the visibilitychange listener at module eval (:69-81), before CONTRACT_FNS ever
- * checks anything, so what backs it is the floor, not the boot contract — `L.Poll.active` is
- * unconditional in every 24.10+ luci.js. Read blind, a renamed `timer` field would make LuCI's tick
- * look like a view's — cleared on the next navigation, every poll on every later page silently
- * dead. So a missing field is a reason to do nothing, once, loudly: `active()` says whether the
- * tick is running, and `timer` is deleted by `stop()`, so an absent field is the ordinary "nothing
- * to protect" case. The anomaly worth reporting is the pair disagreeing — a tick running while the
- * id it runs on has no name we know. */
+ * it, but no supported release ships poll.js yet). Read blind, a renamed field would make LuCI's
+ * tick look like a view's — cleared on the next navigation, every poll on every later page silently
+ * dead. So a missing field is a reason to do nothing, once, loudly.
+ *
+ * Asked through the documented half first: `active()` says whether the tick is running, and `timer`
+ * is deleted by `stop()`, so an absent field is the ordinary "nothing to protect" case. The anomaly
+ * worth reporting is the pair disagreeing — a tick running while the id it runs on has no name we
+ * know.
+ *
+ * The alias itself is guarded for the same reason: the sweep runs inside the staged render, and a
+ * TypeError there would leave every click showing the previous page's content under the new page's
+ * title. */
 /* -> the tick's id; null when LuCI is not polling; false when the two cannot be told apart, which
  * every caller reads as "leave every interval alone" */
 function pollTickId() {
-	const running = L.Poll.active();
+	if (!L.Poll) {
+		warnPollUnreadable('footstrap: L.Poll is gone from this luci-base, so LuCI\'s own tick cannot be '
+			+ 'told apart from a view\'s timers — leaving view intervals alone. fs-router.js needs '
+			+ 'updating for this luci-base.');
+		return false;
+	}
+	const running = (typeof L.Poll.active === 'function') ? L.Poll.active() : (L.Poll.timer != null);
 	if (running && L.Poll.timer == null) {
 		warnPollUnreadable('footstrap: LuCI is polling but L.Poll.timer is not readable — leaving view '
 			+ 'intervals alone rather than risking its tick. fs-router.js needs updating for this '
@@ -129,16 +138,6 @@ function clearViewIntervals() {
 	/* Map, not Set: the key is the timer id and the value is what it takes to re-arm it */
 	_viewIntervals.forEach((spec, id) => { if (id !== keep) window.clearInterval(id); });
 }
-/* The "Refreshing"/"Paused" pill must not outlive the poll it reports on — see the `poll-stop`
- * listener below, which is the only caller in the shipped file. Exported (fs:probe) too, because
- * navigate()'s own async chain (require(), staging into a real `.fs-content`) needs more DOM than
- * tests/lib's fakes give it; this is the one check that actually matters for the ordering bug and is
- * exercised directly, without the listener's deferral, by tests/poll-status.test.mjs. */
-function hidePollIndicatorIfEmpty() {
-	if (!(L.Poll && L.Poll.queue && L.Poll.queue.length === 0)) return;
-	try { ui.hideIndicator('poll-status'); }
-	catch (e) { console.error('footstrap: hideIndicator threw on the poll-status teardown', e); }
-}
 /* one line per document: this runs on every navigation, and a router that cannot read L.Poll
  * cannot read it on the next click either */
 let _pollWarned = false;
@@ -329,75 +328,17 @@ function watchSession() {
  * which also takes it out of the live tree. Public API only; no reaching into `dom.registry`. */
 function discard(el) {
 	try {
+		const dom = window.L ? window.L.dom : null;
+		if (!dom || typeof dom.content !== 'function') { el.remove(); return; }
 		const bin = document.createElement('div');
 		bin.appendChild(el);
-		window.L.dom.content(bin, null);
+		dom.content(bin, null);
 	}
 	catch (e) {
 		el.remove();
 	}
 }
 
-/* ---- a page that leaves nodes as direct children of  hands the next page over by full load,
- * the same way an invasive foreign sheet does (docs/third-party-apps.md, Rule 2 extended from CSS to
- * the DOM — issue #56) ----
- *
- * luci-app-bandix's render() appends 7 tooltip/modal nodes to document.body on every visit,
- * unconditionally (reproduced on owrt2512 and owrt2410) — nothing here sweeps them; the reasoning is
- * docs/spa-router.md, "Foreign view DOM". The recorder is header.ut's first